| title | Маппинг данных |
|---|
Работа с Plain Old Java Objects (POJO) в Tarantool происходит с помощью библиотеки
Jackson.
С помощью Jackson можно преобразовывать Java объекты в JSON (сериализация) и наоборот (десериализация). Также можно использовать расширения Jackson для других форматов сериализации данных.
В tarantool-java-sdk используется библиотека Jackson с расширением для работы с Msgpack, которая
позволяет работать с Java сущностями используя API библиотеки Jackson.
Отличие же заключается в том, что выходной тип данных будет не JSON, а Msgpack. Msgpack - формат сериализации, используемый в Tarantool для передачи данных между клиентом и Tarantool. В tarantool-java-sdk SDK есть модуль, который будет сериализовывать POJO с помощью Jackson в Msgpack и передавать их по протоколу Tarantool.
Далее описываются практики, которые позволят понять как взаимодействовать с методами Tarantool c помощью библиотеки Jackson.
Рассмотрим следующие примеры работы с Tarantool и Jackson. Эти примеры не являются подробным руководством, но помогут вам понять принципы взаимодействия. Освоив их, вы сможете эффективно работать с базой данных в любых сценариях, будь то кластер или одиночный экземпляр, и с любыми POJO объектами.
По умолчанию маппинг полей в любом из клиентов(CrudClient, BoxClient), выполняется наиболее эффективным путём — по порядковому номеру поля.
Это означает, что если порядок полей в Tarantool такой:
person = box.schema.create_space('person')
person:create_index('pri')
person:format({
{name = 'id', type = 'integer'},
{name = 'is_married', type = 'boolean'},
{name = 'name', type = 'string'},
})Поля в Java POJO должны располагаться в таком же порядке:
@JsonFormat(shape = JsonFormat.Shape.ARRAY)
@JsonIgnoreProperties(ignoreUnknown = true) // for example bucket_id
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Person {
public Integer id;
public Boolean isMarried;
public String name;
}???+ note "Заметка"
Приведенное создание таблицы можно использовать как на одиночном экземпляре, так и на кластере.
Но для корректного поведения vshard в кластере необходимо добавить поле `bucket_id` с типом
`unsigned` и индекс на это поле.
???+ warning "Важно"
`JsonFormat.Shape.ARRAY` обеспечивает конвертацию POJO в массив и наоборот при
сериализации/десериализации jackson.
Такой подход эффективен, поскольку отсутствуют накладные расходы на работу со схемой
ключ-значение, и именно так Tarantool передает данные кортежей по умолчанию (без
ключей) по протоколу.
public class TestClass {
@Test
public void test() {
var space = client.space("person");
Person insertedTuple = space.insert(new Person(1, true, "Artyom"), Person.class).join().get();
// ... [Tuple(..., data = Person{id=1, isMarried=true, name='Artyom'})] ...
space.select(CONDITION, Person.class).join();
}
}???+ warning "Важно"
CONDITION может отличаться у CrudClient и BoxClient, поэтому его инициализация скрыта.
В этом формате можно опускать bucket_id, если он находится в конце кортежа.
person:
format( {
{
name = 'id', type = 'integer'
},
{
name = 'is_married', type = 'boolean'
},
{
name = 'name', type = 'string'
},
{
name = 'bucket_id', type = 'unsigned', is_nullable = true
},
})Получим кортеж с bucket_id как список или как POJO:
public class TestClass {
@Test
public void test() {
// ... [Tuple(..., data = [1, true, Artyom, 123])] ...
space.select(CONDITION).join();
// ... [Tuple(..., data = Person{id=1, isMar ... e='Artyom'})] ...
space.select(CONDITION, Person.class).join();
}
}В противном случае(когда bucket_id не находится в конце формата).
person:drop()
person = box.schema.create_space('person')
person:create_index('pri')
person:format({
{name = 'id', type = 'integer'},
{name = 'is_married', type = 'boolean'},
{name = 'bucket_id', type = 'unsigned', is_nullable = true},
{name = 'name', type = 'string'},
})bucket_id необходимо указать, как поле в POJO:
@JsonFormat(shape = JsonFormat.Shape.ARRAY)
@JsonIgnoreProperties(ignoreUnknown = true) // for example bucket_id
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Person {
public Integer id;
public Boolean isMarried;
public Integer bucketId;
public String name;
}Получим кортеж с bucket_id как POJO, где в ответе будет bucket_id:
public class TestClass {
@Test
public void test() {
var space = client.space("person");
// ...[
// Tuple(...,
// data = Person{
// id=1,
// isMarried=true,
// bucketId=123,
// name='Artyom'
// }
// )
// ] ...
space.select(CONDITION, Person.class).join().get();
}
}???+ note "Заметка"
На запись кортежа в Tarantool действует аналогичная чтению кортежа логика.
Вы также можете настроить гибкий маппинг и работать с ключами несколькими способами. В этих способах мы будем использовать ту же схему данных на стороне Tarantool, что использовали и для эффективного маппинга:
person = box.schema.create_space('person')
person:create_index('pri')
person:format({
{name = 'id', type = 'integer'},
{name = 'name', type = 'string'},
{name = 'is_married', type = 'boolean'},
})Но поля в POJO у нас будут отличаться по порядку:
@JsonIgnoreProperties(ignoreUnknown = true) // for example bucket_id
@EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@Builder(toBuilder = true)
public class UnorderedPerson {
public String name;
@JsonProperty("is_married")
public Boolean isMarried;
public Integer id;
public UnorderedPerson(
@JsonProperty("is_married") Boolean isMarried,
@JsonProperty("id") Integer id,
@JsonProperty("name") String name) {
this.id = id;
this.isMarried = isMarried;
this.name = name;
}
}Это необходимо, чтобы показать что работа с POJO происходит, как с объектом1.
???+ warning "Важно"
`JsonFormat.Shape.ARRAY` отсутствует в декларации POJO. Поэтому данный POJO будет
интерпретироваться как объект[^1] в сериализации/десериализации jackson.
Сериализация возможна несколькими способами:
-
Unflatten input -- Сериализация POJO в Msgpack Map с помощью Jackson -> передача Map в Tarantool -> использование метода записи, который принимает Map как input -
Flatten input method -- Конвертация POJO в Java List -> Сериализация Java List в Msgpack Array -> передача массива в Tarantool -> использование стандартного метода записи Tarantool
???+ warning "Важно"
Этот способ записи актуален только для Crud API. Box API не позволяет записывать объекты по
протоколу IPROTO. Поэтому при работе с экземпляром хранилища (storage) напрямую применяйте
следующий `Flatten input method`.
Для сериализации POJO в Msgpack Map с помощью Jackson используйте следующий код:
public class TestClass {
@Test
public void test() {
client.space("person").insertObject(new UnorderedPerson(1, true, "artyom")).join();
}
}В наборе библиотек tarantool-java-sdk до версии 1.1.3 включительно нельзя использовать методы
crud.[method_name]_object с помощью нативного CrudClient.
Чтобы записать UnorderedPerson, необходимо воспользоваться обходными путями.
Можно воспользоваться call/eval методами клиента и вызвать crud.[method_name]_object методы
напрямую.
public class TestClass {
@Test
public void test() {
client.call("crud.insert_object",
Arrays.asList("person", new UnorderedPerson(1, true, "artyom"))).join();
}
}Это нам позволит передать POJO как объект 1.
Альтернативно можно добавить дополнительный метод в POJO UnorderedPerson, который будет возвращать плоский кортеж в правильном порядке с учетом bucket_id. Например, если bucket_id будет присутствовать в формате таблицы так:
person:format({
{ name = 'id', type = 'integer' },
{ name = 'name', type = 'string' },
{ name = 'bucket_id', type = 'unsigned', is_nullable = true },
{ name = 'is_married', type = 'boolean' },
})То метод в POJO должен выглядеть так:
public class UnorderedPerson {
// fields...
public List<Object> asList() {
return new ArrayList<>(Arrays.asList(id, name, null, isMarried));
}
}Тогда при вызове метода insert можно будет использовать этот способ:
public class TestClass {
@Test
public void test() {
space.insert(new UnorderedPerson(2, true, "nikolay").asList()).join();
}
}Что позволит передавать POJO как кортеж, при этом, продолжая, работать с объектом UnorderedPerson.
Десериализация возможна несколькими способами:
-
Flatten output method -- Использование стандартных методов чтения Tarantool -> получение Msgpack массива \ Конвертация массива с использования формата данных в POJO формат Получение карты {ключ поля -> номер поля} каким либо способом / -
Unflatten output -- Получение Msgpack Map -> конвертация Msgpack Map в POJO с использованием Jackson
???+ warning "Важно"
Данный способ записи актуален только для Crud API. Box API не позволяет получать несжатые
кортежи по протоколу IPROTO. Поэтому при работе с экземпляром хранилища (storage) напрямую
применяйте метод `Flatten output method`.
В наборе библиотек tarantool-java-sdk до версии 1.1.3 включительно нельзя использовать метод
crud.unflatten_rows с помощью нативного CrudClient. Следовательно, чтобы прочитать
UnorderedPerson, воспользуемся вызовом lua напрямую через eval метод.
List<UnorderedPerson> persons = routerClient.eval("""
res, err = crud.select(...)
return crud.unflatten_rows(res.rows, res.metadata)
""",
Arrays.asList(
"person",
Arrays.asList(Arrays.asList("==", "pk", 1))
),
new TypeReference<List<List<UnorderedPerson>>>() {}
).thenApply(
tarantoolResponse -> tarantoolResponse.get() // unwrap TarantoolResponse
.get(0) // get first object from multi return
).join();Tarantool предоставляет несколько возможностей для получения формата:
Этот вариант предпочтителен, если вы подключаетесь к инстансу Tarantool,
который хранит данные (storage) и не является роутером (router).
Если вы подключаетесь к хранилищу, то вы используете Box API клиент (TarantoolBoxClient).
TarantoolBoxClient client = TarantoolFactory.box().build();По умолчанию Box API клиент использует модуль tarantool-schema и его основной класс SchemaFetcher,
для получения метаданных из служебного спейса. Клиент проверяет версию схемы на каждом ответе от
Tarantool, и если версия схемы обновилась, то он обновляет свой локальный кеш со схемой с помощью
SchemaFetcher. Таким образом вы можете надежно использовать этот маппинг, не опасаясь, что java
клиент не обновит схему, если схема данных обновилась на стороне Tarantool.
???+ note "Заметка"
При желании можно отключить SchemaFetcher. Например если вы используете эффективный маппинг по
номеру поля. Тогда вам необходимо указать данную опцию при создании клиента
```java
var clientWithoutFetcher = TarantoolFactory.box()
.withFetchSchema(false)
.build();
```
Для получения формата таблицы, необходимо получить метаданные space с помощью SchemaFetcher:
TarantoolSchemaFetcher fetcher = client.getFetcher();
Space spaceInfo = fetcher.getSpace("person");
List<Field> tupleFormat = spaceInfo.getFormat();С помощью полученного формата можно осуществить маппинг по ключу, используя полученные от Tarantool Java List:
public class TestClass {
@Test
public void test() {
space.select(Arrays.asList(1)).thenApply(
list -> TupleMapper.mapToPojoList(
list.get(),
tupleFormat,
UnorderedPerson.class
)
).join();
// [UnorderedPerson{name='artyom', isMarried=true, id=1}]
}
}Подробнее про структуру ответа tarantool/crud можно найти
здесь github.com/tarantool/crud.
Cоздадим клиент TarantoolCrud, который является прокси к API модуля tarantool/crud.
var client = TarantoolFactory.crud().build();Для большей наглядности добавим bucket_id не в конец формата спейса.
person:drop()
person = box.schema.create_space('person')
person:create_index('pri')
person:format({
{ name = 'id', type = 'integer' },
{ name = 'name', type = 'string' },
{ name = 'bucket_id', type = 'unsigned', is_nullable = true },
{ name = 'is_married', type = 'boolean' },
})var space = client.space("person");Метаданные могут быть получены из TUPLE_EXT, если метод crud поддерживает TUPLE_EXT формат, или из metadata ответа crud.
public class TestClass {
@Test
public void test() {
// Формат автоматически передается из CrudResponse.metadata
List<Tuple<List<?>>> tuples = space.select(
Collections.singletonList(Condition.create(EQ, "id", 1))
).join();
// Маппим используя формат из tuple
UnorderedPerson person = TupleMapper.mapToPojo(tuples.get(0), UnorderedPerson.class);
}
}Если у вас версия коннектора, которая не выдает метаданные ответа tarantool/crud,
то можно вызывать методы tarantool/crud напрямую:
public class TestClass {
@Test
public void test() {
client.call(
"crud.select",
Arrays.asList(
"person",
Arrays.asList(Arrays.asList("==", "pk", 1))
),
new TypeReference<CrudResponse<List<List<?>>>>() {}
).thenApply(
tarantoolResponse -> {
var result = new ArrayList<>();
CrudResponse<List<List<?>>> crudResponse = tarantoolResponse.get(); // unwrap tuple struct from select response struct
List<io.tarantool.mapping.Field> metadata = crudResponse.getMetadata(); // get metadata from crud response
List<List<?>> tuples = crudResponse.getRows(); // get flatten tuples from crud response
if (tuples == null) {
return result;
}
for (List<?> tuple : tuples) {
// use TupleMapper to map from tuple data and format to Person POJO
UnorderedPerson person = TupleMapper.mapToPojo(tuple, metadata, UnorderedPerson.class);
result.add(person);
}
return result;
}
).join();
}
}В ответе, где возвращается кортеж, Tarantool позволяет получать и формат ответа в отдельном поле IPROTO пакета. Отличие от предыдущего варианта в том, что формат передается из другого источника.
???+ warning "Важно"
В `crud` версии `1.7.1` функционал возврата формата ответа в отдельном поле IPROTO пакета не
поддерживается.
Воспользуемся Box API клиентом:
public class TestClass {
@Test
public void test() {
// other code
space.select(Arrays.asList(1))
.thenApply(
selectResponse -> {
var result = new ArrayList<>();
List<Tuple<List<?>>> tuples = selectResponse.get();
if (tuples == null) {
return result;
}
for (Tuple<List<?>> tuple : tuples) {
// use TupleMapper to map tuple with embedded format to POJO
result.add(
TupleMapper.mapToPojo(tuple, UnorderedPerson.class)
);
}
return result;
}
).join();
}
}