Skip to content

tarantool/spring-petclinic-tarantool

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tarantool Spring PetClinic Sample Application

Understanding the original Spring Petclinic application with a few diagrams

See the presentation here

Running petclinic locally

First you need to install tarantool on the system. Well, then you need to start the tarantool cluster:

git clone git@github.com:tarantool/spring-petclinic-tarantool.git
cd spring-petclinic-tarantool/cluster
cartridge build                                                                         # Install modules
cartridge start -d                                                                      # Start cluster in deamonize mode
cartridge replicasets setup --bootstrap-vshard                                          # Setup replica sets described in a file replicasets.yml
curl -X POST http://localhost:8081/migrations/up                                        # Run cluster-wide migrations for your data
echo "require('data')" | tarantoolctl connect admin:secret-cluster-cookie@0.0.0.0:3301  # Fill the cluster with initial data by passing in router

Petclinic is a Spring Boot application built using Maven. You can build a jar file and run it from the command line:

cd ..
./mvnw package
java -jar target/*.jar

You can then access petclinic here: http://localhost:8080/

petclinic-screenshot

Or you can run it from Maven directly using the Spring Boot Maven plugin. If you do this it will pick up changes that you make in the project immediately (changes to Java source files require a compile as well - most people use an IDE for this):

./mvnw spring-boot:run

Version restrictions
For this example, version restrictions were found: The cartridge replicasets setup command works only when cartridge-cli >= 2.5.0.
In the future, the example will be improved to work without limiting versions.

About tarantool cluster

Tarantool is Reliable NoSQL DBMS. This example is run using the cartridge framework, which is a powerful orchestrator for Tarantool. Data sharding is performed on the principle of virtual buckets using the vshard module. Therefore, to begin with, we must install all the necessary modules, they are described in the testserver-scm-1.rockspec file using the command tarantoolctl rocks make


All the code with the cluster logic is in the cluster folder. Using the cartridge start -d command, the cartridge framework starts several tarantool instances, the configuration of which is described in the instances.yml file:

---
testserver.router:
  advertise_uri: localhost:3301
  http_port: 8081

testserver.s1-master:
  advertise_uri: localhost:3302
  http_port: 8082

testserver.s1-replica:
  advertise_uri: localhost:3303
  http_port: 8083

testserver.s2-master:
  advertise_uri: localhost:3304
  http_port: 8084

testserver.s2-replica:
  advertise_uri: localhost:3305
  http_port: 8085

testserver-stateboard:
  listen: localhost:3310
  password: passwd

It follows that we are launching 6 tarantool instances.


Next, we create a topology from these instances using the command: cartridge replicasets setup --bootstrap-vshard
The cluster configuration is located in the replicasets.yml file:

r1:
  instances:
    - router
  roles:
    - vshard-router
    - crud-router
    - app.roles.api_router
  all_rw: false
s1:
  instances:
    - s1-master
    - s1-replica
  roles:
    - vshard-storage
    - crud-storage
    - app.roles.api_storage
  weight: 1
  all_rw: false
  vshard_group: default
s2:
  instances:
    - s2-master
    - s2-replica
  roles:
    - vshard-storage
    - crud-storage
    - app.roles.api_storage
  weight: 1
  all_rw: false
  vshard_group: default


Next, using the migration module can run cluster-wide migrations for your data curl -X POST http://localhost:8081/migrations/up

and fill in the data using a tarantool router connection: echo "require('data')" | tarantoolctl connect admin:secret-cluster-cookie@0.0.0.0:3301 ``

Database configuration

This default configuration assumes the use of tarantool noSQL database. Tarantool and spring work using a special module for cartridge-springdata. Therefore, the database configuration is somewhat different from the default h2.

Data Model diagram

Let's look at the data model:
Here we can immediately see that there are no familiar relationships using foreign keys, and the datatype of the primary keys is uuid. To keep data normalization, data splitting and joining are being did in a special way:

  1. We use uuid as it is much faster than using any auto-increment on the storage cluster. UUID can be generated both on the client and on the side of tarantool application scripts.
  2. Join happens predominantly on storages, if possible, and is aggregated using map reduce.
  3. Fields where the data type is indicated with a question mark means that this field can be nullable, this is done so that we can return data nested using custom joins.
  4. Since there are no foreign keys, secondary indexes were added to quickly fetch data. In the diagram, they are indicated by a graph tree.

Data nesting and joining

You can nest data from one space into another. To do this, you need to place an empty field in space_object: format. When transferring data from the database to the client, add data to this stub. E.g.:

owners:format({
    { name = "id", type = "uuid" },
    { name = "first_name", type = "string" },
    { name = "last_name", type = "string" },
    { name = "address", type = "string" },
    { name = "city", type = "string" },
    { name = "telephone", type = "string" },
    { name = "bucket_id", type = "unsigned" },
    { name = "pets", type = "map", is_nullable = true }
})

We will return the owner's data along with his pets. For embedding data, we have a pets field. NULL is stored in these field on storages.

-- OneToMany = Owners -> Pets
local function find_owner_by_id_without_pet_type(id)
    local owner = crud.select("owners", {{'=', 'id', id}})
    local pets = crud.select("pets", {{'=', 'owner_id', owner.rows[1][1]}})
    pets = crud.unflatten_rows(pets.rows, pets.metadata)
    owner.rows[1][8] = pets
    return owner
end

As we can see, the data join occurs using the crud module, which allows us to make quieries to the cluster. If we do not have enough crud functionality, then we can use cartridge.pool.map_call. Just to demonstrate this, let's see how the many to many case is implemented:

The join of two tables vets and vet_specialties occurs on the storages, the result is returned to the router, and on the router, data from the 3rd table is pulled up afterwards:

storage:

local function get_vets_with_specialties_id()
    local vets = {}
    for _, vet in box.space.vets:pairs() do
        local specialties_ids = {}
        local specialties = box.space.vet_specialties.index.vet_id:select(vet[1])
        for _, specialty in pairs(specialties) do
            local specialty_id = specialty[2]
            table.insert(specialties_ids, specialty_id)
        end
        vet = vet:totable()
        table.insert(vet, specialties_ids)
        table.insert(vets, vet)
    end
    return vets
end

router:

local function get_vets_with_specialties()
    local vets_list = {}
    local vets_by_storage, err =
    cartridge_pool.map_call('get_vets_with_specialties_id', {}, { uri_list = get_uriList() })
    if err then
        return nil, err
    end
    for _, vets in pairs(vets_by_storage) do
        for _, vet in pairs(vets) do
            local specialties = {}
            -- one vet may has many specialties
            for _, specialty_id in pairs(vet[#vet]) do
                local specialty = crud.get("specialties", specialty_id)
                table.insert(specialties, crud.unflatten_rows(specialty.rows, specialty.metadata)[1])
            end
            -- replace specialty id by specialty name
            vet[5] = specialties
            table.insert(vets_list, vet)
        end
    end
    return vets_list
end

Work from java client

Interaction with Tarantool can be done by using Tarantool function binding via Query annotation indicated above or by using a standard CrudRepository functions (like findById, save, etc...).

// Binding tarantool function
@Query(function = "find_owner_by_id")
Owner findOwnerById(UUID id);

// Using default CrudRepository operations
Owner save(Owner owner);

Data mapping from tarantool spaces to java entities is implemented with @tuple and @field annotations. See tarantool-springdata module for details.

@Tuple("visits")
public class Visit extends BaseEntity {

    @Field(name = "visit_date")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate date;

    @NotEmpty
    @Field(name = "description")
    private String description;
...
}

Data is obtained from cluster via custom functions lua functions or 'crud' library that execute on 'router' instance. Here is an example of configuration to establish connection to tarantool router instance:

@Configuration
@EnableTarantoolRepositories(basePackageClasses = { VetRepository.class, OwnerRepository.class, PetRepository.class,
        PetTypeRepository.class, VisitRepository.class })
public class TarantoolConfiguration extends AbstractTarantoolDataConfiguration {

    // localhost
    @Value("${tarantool.host}")
    protected String host;

    // 3301 is our router
    @Value("${tarantool.port}")
    protected int port;

    // admin
    @Value("${tarantool.username}")
    protected String username;

    // secret-cluster-cookie
    @Value("${tarantool.password}")
    protected String password;

    @Override
    protected void configureClientConfig(TarantoolClientConfig.Builder builder) {
        builder.withConnectTimeout(1000 * 5).withReadTimeout(1000 * 5).withRequestTimeout(1000 * 5);
    }

    @Override
    public TarantoolCredentials tarantoolCredentials() {
        return new SimpleTarantoolCredentials(username, password);
    }

    @Override
    protected TarantoolServerAddress tarantoolServerAddress() {
        return new TarantoolServerAddress(host, port);
    }

    @Override
    public TarantoolClient<TarantoolTuple, TarantoolResult<TarantoolTuple>> tarantoolClient(
            TarantoolClientConfig tarantoolClientConfig,
            TarantoolClusterAddressProvider tarantoolClusterAddressProvider) {
        return new ProxyTarantoolTupleClient(
                super.tarantoolClient(tarantoolClientConfig, tarantoolClusterAddressProvider));
    }

}

License

The Spring PetClinic sample application is released under version 2.0 of the Apache License.