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/
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.
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
``
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.
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:
- 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.
- Join happens predominantly on storages, if possible, and is aggregated using map reduce.
- 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.
- Since there are no foreign keys, secondary indexes were added to quickly fetch data. In the diagram, they are indicated by a graph tree.
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
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));
}
}
The Spring PetClinic sample application is released under version 2.0 of the Apache License.