diff --git a/documentation/doc-src/pages/client/arch/tuple_pojo_mapping.en.md b/documentation/doc-src/pages/client/arch/tuple_pojo_mapping.en.md index 2d40c15a..2a215c29 100644 --- a/documentation/doc-src/pages/client/arch/tuple_pojo_mapping.en.md +++ b/documentation/doc-src/pages/client/arch/tuple_pojo_mapping.en.md @@ -27,7 +27,7 @@ objects. ## Efficient Mapping (Flatten input, Flatten output) -By default, field mapping in any of the clients (CrudClient, BoxClient), is performed in the most +By default, field mapping in any of the clients (CrudClient, BoxClient) is performed in the most efficient way — by the field's ordinal number. This means that if the field order in Tarantool is: @@ -193,7 +193,7 @@ public class TestClass { ## Flexible Mapping Using Keys -You can also configure flexible mapping and work with keys in several ways. In these ways +You can also configure flexible mapping and work with keys in several ways. In these approaches we will use the same data schema on the Tarantool side that we used for efficient mapping: @@ -225,7 +225,7 @@ public class UnorderedPerson { public Boolean isMarried; public Integer id; - public Person( + public UnorderedPerson( @JsonProperty("is_married") Boolean isMarried, @JsonProperty("id") Integer id, @JsonProperty("name") String name) { @@ -343,8 +343,8 @@ Deserialization is possible in several ways: 1. ``` Flatten output method -- Using standard Tarantool read methods -> receiving Msgpack array \ - Converting array using data format to POJO format - Getting the {field key -> field number} map in any way / + Converting array using data format to POJO format + Getting the {field key -> field number} map in any way / ``` 2. ``` Unflatten output -- Receiving Msgpack Map -> converting Msgpack Map to POJO using Jackson @@ -371,7 +371,7 @@ List persons = routerClient.eval(""" "person", Arrays.asList(Arrays.asList("==", "pk", 1)) ), - new TypeReference>>() {} + new TypeReference>>() {} ).thenApply( tarantoolResponse -> tarantoolResponse.get() // unwrap TarantoolResponse .get(0) // get first object from multi return @@ -424,29 +424,11 @@ public class TestClass { @Test public void test() { space.select(Arrays.asList(1)).thenApply( - list -> { - var result = new ArrayList<>(); - - for (var t : list.get()) { // unwrap tuple struct from select response struct - List dataList = t.get(); // unwrap data from tuple struct - Map map = IntStream // create map {key -> value} - .range(0, dataList.size()) - .boxed() - .collect( - Collectors.toMap( - (i) -> tupleFormat.get(i).getName(), - dataList::get - ) - ); - // use jackson mapper to map from Map to Person POJO - // import static io.tarantool.mapping.BaseTarantoolJacksonMapping.objectMapper; - UnorderedPerson person = objectMapper.convertValue(map, UnorderedPerson.class); - - result.add(person); - } - - return result; - } + list -> TupleMapper.mapToPojoList( + list.get(), + tupleFormat, + UnorderedPerson.class + ) ).join(); // [UnorderedPerson{name='artyom', isMarried=true, id=1}] } @@ -456,7 +438,7 @@ public class TestClass { ##### 2. Using tarantool/crud Response Metadata More information about the tarantool/crud response structure can be found -here [github.com/tarantool/crud](https://github.com/tarantool/crud?tab=readme-ov-file#api). +here [github.com/tarantool/crud](https://github.com/tarantool/crud?tab=readme-ov-file#api). Create a TarantoolCrud client that is a proxy to the tarantool/crud module API. ```java @@ -481,7 +463,30 @@ person:format({ var space = client.space("person"); ``` -If you have a connector version that does not return tarantool/crud response metadata, +###### Connector version > 1.5.0 + +Metadata can be obtained from TUPLE_EXT if the crud method supports TUPLE_EXT format, or from +crud response metadata. + +```java +public class TestClass { + + @Test + public void test() { + // Format is automatically passed from CrudResponse.metadata + List>> tuples = space.select( + Collections.singletonList(Condition.create(EQ, "id", 1)) + ).join(); + + // Map using format from tuple + UnorderedPerson person = TupleMapper.mapToPojo(tuples.get(0), UnorderedPerson.class); + } +} +``` + +###### Connector version <= 1.5.0 + +If you have a connector version that does not return tarantool/crud response metadata, you can call the tarantool/crud methods directly: ```java @@ -510,19 +515,8 @@ public class TestClass { } for (List tuple : tuples) { - Map map = IntStream // create map {key -> value} - .range(0, tuple.size()) - .boxed() - .collect( - Collectors.toMap( - (i) -> metadata.get(i).getName(), - tuple::get - ) - ); - - // use jackson mapper to map from Map to Person POJO - // import static io.tarantool.mapping.BaseTarantoolJacksonMapping.objectMapper; - UnorderedPerson person = objectMapper.convertValue(map, UnorderedPerson.class); + // use TupleMapper to map from tuple data and format to Person POJO + UnorderedPerson person = TupleMapper.mapToPojo(tuple, metadata, UnorderedPerson.class); result.add(person); } @@ -566,19 +560,9 @@ public class TestClass { } for (Tuple> tuple : tuples) { - List format = tuple.getFormat(); - List data = tuple.get(); - Map map = IntStream - .range(0, data.size()) - .boxed() - .collect( - Collectors.toMap( - (i) -> format.get(i).getName(), - data::get - ) - ); + // use TupleMapper to map tuple with embedded format to POJO result.add( - objectMapper.convertValue(map, PersonWithDifferentFieldsOrder.class) + TupleMapper.mapToPojo(tuple, PersonWithDifferentFieldsOrder.class) ); } diff --git a/documentation/doc-src/pages/client/arch/tuple_pojo_mapping.md b/documentation/doc-src/pages/client/arch/tuple_pojo_mapping.md index a6815b3f..4c53bf08 100644 --- a/documentation/doc-src/pages/client/arch/tuple_pojo_mapping.md +++ b/documentation/doc-src/pages/client/arch/tuple_pojo_mapping.md @@ -8,7 +8,7 @@ title: Маппинг данных Jackson. С помощью Jackson можно преобразовывать Java объекты в JSON (сериализация) и наоборот -(десериализация). Также можно использовать расширения Jackson для других форматов сериализация +(десериализация). Также можно использовать расширения Jackson для других форматов сериализации данных. В `tarantool-java-sdk` используется библиотека Jackson с расширением для работы с `Msgpack`, которая @@ -227,7 +227,7 @@ public class UnorderedPerson { public Boolean isMarried; public Integer id; - public Person( + public UnorderedPerson( @JsonProperty("is_married") Boolean isMarried, @JsonProperty("id") Integer id, @JsonProperty("name") String name) { @@ -314,7 +314,7 @@ person:format({ }) ``` -То метод в POJO должен выглядит так: +То метод в POJO должен выглядеть так: ```java public class UnorderedPerson { @@ -340,9 +340,9 @@ public class TestClass { Что позволит передавать POJO как кортеж, при этом, продолжая, работать с объектом UnorderedPerson. -### Чтения POJO из Tarantool +### Чтение POJO из Tarantool -Десериалиация возможна несколькими способами: +Десериализация возможна несколькими способами: 1. ``` Flatten output method -- Использование стандартных методов чтения Tarantool -> получение Msgpack массива \ @@ -374,7 +374,7 @@ List persons = routerClient.eval(""" "person", Arrays.asList(Arrays.asList("==", "pk", 1)) ), - new TypeReference>>() {} + new TypeReference>>() {} ).thenApply( tarantoolResponse -> tarantoolResponse.get() // unwrap TarantoolResponse .get(0) // get first object from multi return @@ -428,29 +428,11 @@ public class TestClass { @Test public void test() { space.select(Arrays.asList(1)).thenApply( - list -> { - var result = new ArrayList<>(); - - for (var t : list.get()) { // unwrap tuple struct from select response struct - List dataList = t.get(); // unwrap data from tuple struct - Map map = IntStream // create map {key -> value} - .range(0, dataList.size()) - .boxed() - .collect( - Collectors.toMap( - (i) -> tupleFormat.get(i).getName(), - dataList::get - ) - ); - // use jackson mapper to map from Map to Person POJO - // import static io.tarantool.mapping.BaseTarantoolJacksonMapping.objectMapper; - UnorderedPerson person = objectMapper.convertValue(map, UnorderedPerson.class); - - result.add(person); - } - - return result; - } + list -> TupleMapper.mapToPojoList( + list.get(), + tupleFormat, + UnorderedPerson.class + ) ).join(); // [UnorderedPerson{name='artyom', isMarried=true, id=1}] } @@ -485,6 +467,29 @@ person:format({ var space = client.space("person"); ``` +###### Connector version > 1.5.0 + +Метаданные могут быть получены из TUPLE_EXT, если метод crud поддерживает TUPLE_EXT формат, или из +metadata ответа crud. + +```java +public class TestClass { + + @Test + public void test() { + // Формат автоматически передается из CrudResponse.metadata + List>> tuples = space.select( + Collections.singletonList(Condition.create(EQ, "id", 1)) + ).join(); + + // Маппим используя формат из tuple + UnorderedPerson person = TupleMapper.mapToPojo(tuples.get(0), UnorderedPerson.class); + } +} +``` + +###### Connector version <= 1.5.0 + Если у вас версия коннектора, которая не выдает метаданные ответа tarantool/crud, то можно вызывать методы tarantool/crud напрямую: @@ -514,19 +519,8 @@ public class TestClass { } for (List tuple : tuples) { - Map map = IntStream // create map {key -> value} - .range(0, tuple.size()) - .boxed() - .collect( - Collectors.toMap( - (i) -> metadata.get(i).getName(), - tuple::get - ) - ); - - // use jackson mapper to map from Map to Person POJO - // import static io.tarantool.mapping.BaseTarantoolJacksonMapping.objectMapper; - UnorderedPerson person = objectMapper.convertValue(map, UnorderedPerson.class); + // use TupleMapper to map from tuple data and format to Person POJO + UnorderedPerson person = TupleMapper.mapToPojo(tuple, metadata, UnorderedPerson.class); result.add(person); } @@ -545,19 +539,19 @@ IPROTO пакета. Отличие от предыдущего варианта ???+ warning "Важно" - В `crud` версии `1.7.1` функционал возврата формата ответа в отдельном поле IPROTO пакета не + В `crud` версии `1.7.1` функционал возврата формата ответа в отдельном поле IPROTO пакета не поддерживается. Воспользуемся Box API клиентом: ```java public class TestClass { - + @Test public void test() { - - // other code - + + // other code + space.select(Arrays.asList(1)) .thenApply( selectResponse -> { @@ -570,19 +564,9 @@ public class TestClass { } for (Tuple> tuple : tuples) { - List format = tuple.getFormat(); - List data = tuple.get(); - Map map = IntStream - .range(0, data.size()) - .boxed() - .collect( - Collectors.toMap( - (i) -> format.get(i).getName(), - data::get - ) - ); + // use TupleMapper to map tuple with embedded format to POJO result.add( - objectMapper.convertValue(map, PersonWithDifferentFieldsOrder.class) + TupleMapper.mapToPojo(tuple, UnorderedPerson.class) ); } diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolBoxClientTest.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolBoxClientTest.java index 027841e9..b8ebf045 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolBoxClientTest.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolBoxClientTest.java @@ -13,14 +13,12 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.IntStream; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -49,7 +47,6 @@ import org.testcontainers.shaded.com.google.common.base.CaseFormat; import static io.tarantool.client.box.TarantoolBoxSpace.WITHOUT_ENABLED_FETCH_SCHEMA_OPTION_FOR_TARANTOOL_LESS_3_0_0; -import static io.tarantool.mapping.BaseTarantoolJacksonMapping.objectMapper; import io.tarantool.client.BaseOptions; import io.tarantool.client.ClientType; import io.tarantool.client.Options; @@ -69,6 +66,7 @@ import io.tarantool.mapping.SelectResponse; import io.tarantool.mapping.TarantoolResponse; import io.tarantool.mapping.Tuple; +import io.tarantool.mapping.TupleMapper; import io.tarantool.schema.NoSchemaException; import io.tarantool.schema.Space; import io.tarantool.schema.TarantoolSchemaFetcher; @@ -399,12 +397,8 @@ public void testMappingWithFormat() { for (Tuple> t : list.get()) { List tupleFormat = formatGetter.apply(t); - List dataList = t.get(); - Map map = - IntStream.range(0, dataList.size()) - .boxed() - .collect(Collectors.toMap((i) -> tupleFormat.get(i).getName(), dataList::get)); - result.add(objectMapper.convertValue(map, PersonWithDifferentFieldsOrder.class)); + result.add( + TupleMapper.mapToPojo(t.get(), tupleFormat, PersonWithDifferentFieldsOrder.class)); } return result; }; diff --git a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientTest.java b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientTest.java index c0df21d8..580fd194 100644 --- a/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientTest.java +++ b/tarantool-client/src/test/java/io/tarantool/client/integration/TarantoolCrudClientTest.java @@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -79,6 +80,7 @@ import io.tarantool.core.protocol.IProtoResponse; import io.tarantool.mapping.TarantoolResponse; import io.tarantool.mapping.Tuple; +import io.tarantool.mapping.TupleMapper; import io.tarantool.mapping.crud.CrudBatchResponse; import io.tarantool.mapping.crud.CrudError; import io.tarantool.mapping.crud.CrudException; @@ -3780,4 +3782,131 @@ private static List mapBatchToType( .sorted(tupleComparator) .collect(Collectors.toList()); } + + @Test + public void testSelectWithFormatFromCrudMetadata() { + // Insert test data + Person testPerson = new Person(9999, true, "CrudFormatTest"); + TarantoolCrudSpace personSpace = client.space("person"); + personSpace.insert(testPerson).join(); + + List>> tuples = + personSpace.select(Collections.singletonList(Condition.create(EQ, "id", 9999))).join(); + + assertFalse(tuples.isEmpty(), "Should return at least one tuple"); + + Tuple> tuple = tuples.get(0); + assertNotNull(tuple, "Tuple should not be null"); + + // Check that format is available from CrudResponse metadata + List format = tuple.getFormat(); + assertNotNull(format, "Format should be available from CrudResponse metadata"); + assertFalse(format.isEmpty(), "Format should not be empty"); + + // Verify format contains expected fields + List fieldNames = + format.stream().map(io.tarantool.mapping.Field::getName).collect(Collectors.toList()); + + assertTrue(fieldNames.contains("id"), "Format should contain 'id' field"); + assertTrue(fieldNames.contains("name"), "Format should contain 'name' field"); + assertTrue(fieldNames.contains("is_married"), "Format should contain 'is_married' field"); + + // Use TupleMapper to convert to POJO + PersonWithDifferentFieldsOrder mappedPerson = + TupleMapper.mapToPojo(tuple, PersonWithDifferentFieldsOrder.class); + + assertNotNull(mappedPerson, "Mapped person should not be null"); + assertEquals(9999, mappedPerson.getId(), "ID should match"); + assertEquals("CrudFormatTest", mappedPerson.getName(), "Name should match"); + assertEquals(true, mappedPerson.getIsMarried(), "isMarried should match"); + } + + @Test + public void testInsertAndGetWithFormatFromCrudMetadata() { + TarantoolCrudSpace personSpace = client.space("person"); + int testId = 9998; + + // Insert test data using List (raw tuple) to get Tuple> + List testPersonList = Arrays.asList(testId, true, "InsertGetFormatTest"); + @SuppressWarnings("unchecked") + Tuple> insertedTuple = personSpace.insert(testPersonList).join(); + + assertNotNull(insertedTuple, "Inserted tuple should not be null"); + + // Check that format is available from insert response + List insertFormat = insertedTuple.getFormat(); + assertNotNull(insertFormat, "Format should be available from insert response"); + assertFalse(insertFormat.isEmpty(), "Format should not be empty"); + + // Use TupleMapper to convert inserted tuple to POJO + PersonWithDifferentFieldsOrder mappedInsertedPerson = + TupleMapper.mapToPojo(insertedTuple, PersonWithDifferentFieldsOrder.class); + + assertNotNull(mappedInsertedPerson, "Mapped inserted person should not be null"); + assertEquals(testId, mappedInsertedPerson.getId(), "ID should match after insert"); + assertEquals( + "InsertGetFormatTest", mappedInsertedPerson.getName(), "Name should match after insert"); + assertEquals(true, mappedInsertedPerson.getIsMarried(), "isMarried should match after insert"); + + // Get the record and check format + @SuppressWarnings("unchecked") + Tuple> gotTuple = personSpace.get(Collections.singletonList(testId)).join(); + + assertNotNull(gotTuple, "Got tuple should not be null"); + + // Check that format is available from get response + List getFormat = gotTuple.getFormat(); + assertNotNull(getFormat, "Format should be available from get response"); + assertFalse(getFormat.isEmpty(), "Format should not be empty"); + + // Use TupleMapper to convert got tuple to POJO + PersonWithDifferentFieldsOrder mappedGotPerson = + TupleMapper.mapToPojo(gotTuple, PersonWithDifferentFieldsOrder.class); + + assertNotNull(mappedGotPerson, "Mapped got person should not be null"); + assertEquals(testId, mappedGotPerson.getId(), "ID should match after get"); + assertEquals("InsertGetFormatTest", mappedGotPerson.getName(), "Name should match after get"); + assertEquals(true, mappedGotPerson.getIsMarried(), "isMarried should match after get"); + } + + @Test + public void testGetWithFieldsFilterAndFormat() { + TarantoolCrudSpace personSpace = client.space("person"); + int testId = 9997; + + // Insert test data first + List testPersonList = Arrays.asList(testId, true, "FieldsFilterTest"); + personSpace.insert(testPersonList).join(); + + // Get with fields filter - only request 'id' and 'name' fields + @SuppressWarnings("unchecked") + Tuple> filteredTuple = + personSpace + .get( + Collections.singletonList(testId), + GetOptions.builder().withFields(Arrays.asList("id", "name")).build()) + .join(); + + assertNotNull(filteredTuple, "Filtered tuple should not be null"); + + // Check that format is available and contains only requested fields + List format = filteredTuple.getFormat(); + assertNotNull(format, "Format should be available from filtered get response"); + + // The format should reflect the requested fields + List fieldNames = + format.stream().map(io.tarantool.mapping.Field::getName).collect(Collectors.toList()); + + assertTrue(fieldNames.contains("id"), "Format should contain 'id' field"); + assertTrue(fieldNames.contains("name"), "Format should contain 'name' field"); + + // Map to POJO using TupleMapper + PersonWithDifferentFieldsOrder mappedPerson = + TupleMapper.mapToPojo(filteredTuple, PersonWithDifferentFieldsOrder.class); + + assertNotNull(mappedPerson, "Mapped person should not be null"); + assertEquals(testId, mappedPerson.getId(), "ID should match"); + assertEquals("FieldsFilterTest", mappedPerson.getName(), "Name should match"); + // isMarried might be null since we filtered the fields + } } diff --git a/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithTargetClass.java b/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithTargetClass.java index 05e1f151..cc0fbe26 100644 --- a/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithTargetClass.java +++ b/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithTargetClass.java @@ -49,6 +49,9 @@ public static Tuple getTupleWithInjectedFormat(TarantoolResponse format = formats.get(formatId); + if (formatId == null && formats.size() == 1) { + format = formats.values().stream().findFirst().get(); + } tuple.setFormat(format); return tuple; } @@ -65,11 +68,10 @@ public static CompletableFuture> convertCrudSingleResultFuture( public static TarantoolResponse>> readCrudSingleResultData( IProtoResponse response, Class entity) { + CrudResponse>> crudResponse = + readData(response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity)))); return new TarantoolResponse<>( - getRows( - readData( - response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity))))), - getFormats(response)); + getRows(crudResponse), getFormats(response, crudResponse.getMetadata())); } public static CompletableFuture>>> convertSelectResultFuture( @@ -94,6 +96,9 @@ private static SelectResponse>> injectFormatIntoTuples( for (Tuple tuple : resp.get()) { Integer formatId = tuple.getFormatId(); List format = formats.get(formatId); + if (formatId == null && formats.size() == 1) { + format = formats.values().stream().findFirst().get(); + } tuple.setFormat(format); } } @@ -109,11 +114,10 @@ public static CompletableFuture>> convertCrudSelectResultFutur public static TarantoolResponse>> readCrudSelectResult( IProtoResponse response, Class entity) { + CrudResponse>> crudResponse = + readData(response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity)))); return new TarantoolResponse<>( - getRows( - readData( - response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity))))), - getFormats(response)); + getRows(crudResponse), getFormats(response, crudResponse.getMetadata())); } public static @@ -141,6 +145,9 @@ private static CrudBatchResponse>> getBatchTuplesWithInjectedF for (Tuple tuple : tuples) { Integer formatId = tuple.getFormatId(); List format = formats.get(formatId); + if (formatId == null && formats.size() == 1) { + format = formats.values().stream().findFirst().get(); + } tuple.setFormat(format); } } @@ -155,6 +162,9 @@ private static List> getTuplesWithInjectedFormat( for (Tuple tuple : tuples) { Integer formatId = tuple.getFormatId(); List format = formats.get(formatId); + if (formatId == null && formats.size() == 1) { + format = formats.values().stream().findFirst().get(); + } tuple.setFormat(format); } } diff --git a/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithTargetTypeReference.java b/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithTargetTypeReference.java index 7723102f..26b5d3f3 100644 --- a/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithTargetTypeReference.java +++ b/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithTargetTypeReference.java @@ -57,11 +57,10 @@ public static CompletableFuture> convertCrudSingleResultFuture( public static TarantoolResponse>> readCrudSingleResultData( IProtoResponse response, TypeReference entity) { + CrudResponse>> crudResponse = + readData(response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity)))); return new TarantoolResponse<>( - getRows( - readData( - response, wrapIntoType(CrudResponse.class, wrapIntoList(wrapIntoTuple(entity))))), - getFormats(response)); + getRows(crudResponse), getFormats(response, crudResponse.getMetadata())); } public static CompletableFuture> convertSelectResultFuture( @@ -82,9 +81,9 @@ public static CompletableFuture> convertCrudSelectResul public static TarantoolResponse readCrudSelectResult( IProtoResponse response, TypeReference entity) { + CrudResponse crudResponse = readData(response, wrapIntoType(CrudResponse.class, entity)); return new TarantoolResponse<>( - getRows(readData(response, wrapIntoType(CrudResponse.class, entity))), - getFormats(response)); + getRows(crudResponse), getFormats(response, crudResponse.getMetadata())); } public static TarantoolResponse fromEventData( diff --git a/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithoutTargetType.java b/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithoutTargetType.java index 2010478e..1c390005 100644 --- a/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithoutTargetType.java +++ b/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TarantoolJacksonMappingWithoutTargetType.java @@ -56,6 +56,9 @@ private static Tuple> getTupleWithInjectedFormat( } Integer formatId = tuple.getFormatId(); List format = formats.get(formatId); + if (formatId == null && formats.size() == 1) { + format = formats.values().stream().findFirst().get(); + } tuple.setFormat(format); return tuple; } @@ -72,9 +75,10 @@ public static CompletableFuture>> convertCrudSingleResultFuture( public static TarantoolResponse>>> readCrudSingleResultData( IProtoResponse response) { + CrudResponse>>> crudResponse = + readData(response, wrapIntoType(CrudResponse.class, LIST_TUPLE_LIST)); return new TarantoolResponse<>( - getRows(readData(response, wrapIntoType(CrudResponse.class, LIST_TUPLE_LIST))), - getFormats(response)); + getRows(crudResponse), getFormats(response, crudResponse.getMetadata())); } public static T getRows(CrudResponse response) { @@ -100,6 +104,9 @@ private static SelectResponse>>> injectFormatIntoTuples( for (Tuple tuple : resp.get()) { Integer formatId = tuple.getFormatId(); List format = formats.get(formatId); + if (formatId == null && formats.size() == 1) { + format = formats.values().stream().findFirst().get(); + } tuple.setFormat(format); } } @@ -115,6 +122,38 @@ public static Map> getFormats(IProtoResponse response) { return formats; } + /** + * Gets formats from IPROTO response or falls back to CRUD metadata. + * + *

This method first tries to get formats from IPROTO_TUPLE_FORMATS field. If not available, it + * uses the CRUD metadata to construct the format map. CRUD metadata is preferred when both are + * available. + * + * @param response the IPROTO response + * @param crudMetadata the metadata from CRUD response (can be null) + * @return map of format ID to list of fields + */ + public static Map> getFormats( + IProtoResponse response, List crudMetadata) { + // First try to get from IPROTO + Map> formats = getFormats(response); + + boolean formatsEmpty = + formats == null + || formats.isEmpty() + || (formats.size() == 1 && formats.get(0) != null && formats.get(0).isEmpty()); + + boolean crudNotEmpty = crudMetadata != null && !crudMetadata.isEmpty(); + + // If IPROTO format is empty and we have CRUD metadata, use it + if (formatsEmpty && crudNotEmpty) { + // CRUD metadata doesn't have format ID, so we use 0 as default + formats = Collections.singletonMap(0, crudMetadata); + } + + return formats; + } + public static byte[] getPosition(IProtoResponse response) { byte[] position = null; ByteBodyValueWrapper rawPosition = response.getByteBodyValue(IPROTO_POSITION); @@ -137,9 +176,10 @@ public static CompletableFuture>>> convertCrudSelectResultFut public static TarantoolResponse>>> readCrudSelectResult( IProtoResponse response) { + CrudResponse>>> crudResponse = + readData(response, wrapIntoType(CrudResponse.class, LIST_TUPLE_LIST)); return new TarantoolResponse<>( - getRows(readData(response, wrapIntoType(CrudResponse.class, LIST_TUPLE_LIST))), - getFormats(response)); + getRows(crudResponse), getFormats(response, crudResponse.getMetadata())); } private static List>> getTuplesWithInjectedFormat( @@ -150,6 +190,9 @@ private static List>> getTuplesWithInjectedFormat( for (Tuple tuple : tuples) { Integer formatId = tuple.getFormatId(); List format = formats.get(formatId); + if (formatId == null && formats.size() == 1) { + format = formats.values().stream().findFirst().get(); + } tuple.setFormat(format); } } @@ -179,6 +222,9 @@ private static CrudBatchResponse>>> getBatchTuplesWithInjecte for (Tuple tuple : tuples) { Integer formatId = tuple.getFormatId(); List format = formats.get(formatId); + if (formatId == null && formats.size() == 1) { + format = formats.values().stream().findFirst().get(); + } tuple.setFormat(format); } } diff --git a/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TupleMapper.java b/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TupleMapper.java new file mode 100644 index 00000000..38a49a9e --- /dev/null +++ b/tarantool-jackson-mapping/src/main/java/io/tarantool/mapping/TupleMapper.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package io.tarantool.mapping; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.fasterxml.jackson.core.type.TypeReference; + +import static io.tarantool.mapping.BaseTarantoolJacksonMapping.objectMapper; + +/** + * Utility class for mapping Tarantool tuples to POJOs using field format metadata. + * + *

This class provides convenient methods to convert flat tuple arrays (List) into structured + * POJO objects using the field format information from Tarantool space schema or CRUD response + * metadata. + * + *

Usage example: + * + *

{@code
+ * // Using with CRUD response
+ * CrudResponse>> response = client.call(
+ *     "crud.select", args, new TypeReference>>>() {}
+ * ).join().get();
+ *
+ * List persons = TupleMapper.mapToPojoList(
+ *     response.getRows(),
+ *     response.getMetadata(),
+ *     Person.class
+ * );
+ *
+ * // Using with Box API and Tuple response
+ * List>> tuples = space.select(Arrays.asList(1)).join().get();
+ * List persons = TupleMapper.mapToPojoList(tuples, Person.class);
+ * }
+ * + * @author Artyom Dubinin + */ +public final class TupleMapper { + + private TupleMapper() { + // utility class + } + + /** + * Converts a flat tuple list to a map using field format metadata. + * + *

The method creates a map where keys are field names from the format and values are + * corresponding elements from the tuple list. + * + * @param tuple the flat tuple as a list of values + * @param format the field format metadata with field names + * @return a map with field names as keys and tuple values as values + * @throws IllegalArgumentException if tuple and format sizes don't match + */ + public static Map toMap(List tuple, List format) { + if (tuple == null || format == null) { + return new HashMap<>(); + } + + if (tuple.size() != format.size()) { + throw new IllegalArgumentException( + String.format( + "Tuple size (%d) doesn't match format size (%d)", tuple.size(), format.size())); + } + + return IntStream.range(0, tuple.size()) + .boxed() + .collect( + Collectors.toMap(i -> format.get(i).getName(), tuple::get, (a, b) -> a, HashMap::new)); + } + + /** + * Converts a map to a POJO using Jackson object mapper. + * + * @param map the map with field names and values + * @param entityClass the target POJO class + * @param the type of the target POJO + * @return the mapped POJO object + */ + public static T mapToPojo(Map map, Class entityClass) { + return objectMapper.convertValue(map, entityClass); + } + + /** + * Converts a map to a POJO using Jackson object mapper with TypeReference. + * + * @param map the map with field names and values + * @param typeReference the target type reference + * @param the type of the target POJO + * @return the mapped POJO object + */ + public static T mapToPojo(Map map, TypeReference typeReference) { + return objectMapper.convertValue(map, typeReference); + } + + /** + * Converts a flat tuple list directly to a POJO using format metadata. + * + *

This is a convenience method that combines {@link #toMap(List, List)} and {@link + * #mapToPojo(Map, Class)} into a single operation. + * + * @param tuple the flat tuple as a list of values + * @param format the field format metadata with field names + * @param entityClass the target POJO class + * @param the type of the target POJO + * @return the mapped POJO object + */ + public static T mapToPojo(List tuple, List format, Class entityClass) { + Map map = toMap(tuple, format); + return mapToPojo(map, entityClass); + } + + /** + * Converts a flat tuple list directly to a POJO using format metadata. + * + *

This is a convenience method that combines {@link #toMap(List, List)} and {@link + * #mapToPojo(Map, TypeReference)} into a single operation. + * + * @param tuple the flat tuple as a list of values + * @param format the field format metadata with field names + * @param typeReference the target type reference + * @param the type of the target POJO + * @return the mapped POJO object + */ + public static T mapToPojo(List tuple, List format, TypeReference typeReference) { + Map map = toMap(tuple, format); + return mapToPojo(map, typeReference); + } + + /** + * Converts a Tuple with flat list data to a POJO. + * + *

This method extracts format from the Tuple object (if available) and uses it for mapping to + * the POJO. + * + * @param tuple the Tuple object containing flat list data and format + * @param entityClass the target POJO class + * @param the type of the target POJO + * @return the mapped POJO object, or null if tuple is null + * @throws IllegalStateException if tuple doesn't have format information + */ + public static T mapToPojo(Tuple> tuple, Class entityClass) { + if (tuple == null) { + return null; + } + + List format = tuple.getFormat(); + if (format == null) { + throw new IllegalStateException( + "Tuple doesn't contain format information. " + + "Use mapToPojo(List, List, Class) method with explicit format parameter."); + } + + return mapToPojo(tuple.get(), format, entityClass); + } + + /** + * Converts a Tuple with flat list data to a POJO. + * + *

This method extracts format from the Tuple object (if available) and uses it for mapping to + * the POJO. + * + * @param tuple the Tuple object containing flat list data and format + * @param typeReference the target type reference + * @param the type of the target POJO + * @return the mapped POJO object, or null if tuple is null + * @throws IllegalStateException if tuple doesn't have format information + */ + public static T mapToPojo(Tuple> tuple, TypeReference typeReference) { + if (tuple == null) { + return null; + } + + List format = tuple.getFormat(); + if (format == null) { + throw new IllegalStateException( + "Tuple doesn't contain format information. " + + "Use mapToPojo(List, List, TypeReference) method with explicit format parameter."); + } + + return mapToPojo(tuple.get(), format, typeReference); + } + + /** + * Converts a list of flat tuples to a list of POJOs using format metadata. + * + * @param tuples the list of flat tuples + * @param format the field format metadata with field names + * @param entityClass the target POJO class + * @param the type of the target POJO + * @return the list of mapped POJO objects + */ + public static List mapToPojoList( + List> tuples, List format, Class entityClass) { + if (tuples == null || tuples.isEmpty()) { + return new ArrayList<>(); + } + + return tuples.stream() + .map(tuple -> mapToPojo(tuple, format, entityClass)) + .collect(Collectors.toList()); + } + + /** + * Converts a list of flat tuples to a list of POJOs using format metadata. + * + * @param tuples the list of flat tuples + * @param format the field format metadata with field names + * @param typeReference the target type reference + * @param the type of the target POJO + * @return the list of mapped POJO objects + */ + public static List mapToPojoList( + List> tuples, List format, TypeReference typeReference) { + if (tuples == null || tuples.isEmpty()) { + return new ArrayList<>(); + } + + return tuples.stream() + .map(tuple -> mapToPojo(tuple, format, typeReference)) + .collect(Collectors.toList()); + } + + /** + * Converts a list of Tuple objects to a list of POJOs. + * + *

This method extracts format from each Tuple object (if available) and uses it for mapping to + * POJOs. + * + * @param tuples the list of Tuple objects + * @param entityClass the target POJO class + * @param the type of the target POJO + * @return the list of mapped POJO objects + */ + public static List mapToPojoListFromTuples( + List>> tuples, Class entityClass) { + if (tuples == null || tuples.isEmpty()) { + return new ArrayList<>(); + } + + return tuples.stream().map(tuple -> mapToPojo(tuple, entityClass)).collect(Collectors.toList()); + } + + /** + * Converts a list of Tuple objects to a list of POJOs. + * + *

This method extracts format from each Tuple object (if available) and uses it for mapping to + * POJOs. + * + * @param tuples the list of Tuple objects + * @param typeReference the target type reference + * @param the type of the target POJO + * @return the list of mapped POJO objects + */ + public static List mapToPojoListFromTuples( + List>> tuples, TypeReference typeReference) { + if (tuples == null || tuples.isEmpty()) { + return new ArrayList<>(); + } + + return tuples.stream() + .map(tuple -> mapToPojo(tuple, typeReference)) + .collect(Collectors.toList()); + } +} diff --git a/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/TupleMapperTest.java b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/TupleMapperTest.java new file mode 100644 index 00000000..d3b64377 --- /dev/null +++ b/tarantool-jackson-mapping/src/test/java/io/tarantool/mapping/TupleMapperTest.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2025 VK DIGITAL TECHNOLOGIES LIMITED LIABILITY COMPANY + * All Rights Reserved. + */ + +package io.tarantool.mapping; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +public class TupleMapperTest { + + @Test + public void testToMap() { + List tuple = Arrays.asList(1, "John", true); + List format = + Arrays.asList( + new Field().setName("id"), new Field().setName("name"), new Field().setName("active")); + + Map result = TupleMapper.toMap(tuple, format); + + assertEquals(3, result.size()); + assertEquals(1, result.get("id")); + assertEquals("John", result.get("name")); + assertEquals(true, result.get("active")); + } + + @Test + public void testToMapWithNullInputs() { + Map result1 = TupleMapper.toMap(null, Collections.emptyList()); + assertTrue(result1.isEmpty()); + + Map result2 = TupleMapper.toMap(Collections.emptyList(), null); + assertTrue(result2.isEmpty()); + } + + @Test + public void testToMapWithMismatchedSizes() { + List tuple = Arrays.asList(1, "John"); + List format = + Arrays.asList( + new Field().setName("id"), new Field().setName("name"), new Field().setName("active")); + + assertThrows(IllegalArgumentException.class, () -> TupleMapper.toMap(tuple, format)); + } + + @Test + public void testMapToPojoFromTupleAndFormat() { + List tuple = Arrays.asList(1, "John", true); + List format = + Arrays.asList( + new Field().setName("id"), new Field().setName("name"), new Field().setName("active")); + + TestPerson result = TupleMapper.mapToPojo(tuple, format, TestPerson.class); + + assertNotNull(result); + assertEquals(1, result.id); + assertEquals("John", result.name); + assertEquals(true, result.active); + } + + @Test + public void testMapToPojoFromTupleWithFormat() { + List data = Arrays.asList(1, "John", true); + List format = + Arrays.asList( + new Field().setName("id"), new Field().setName("name"), new Field().setName("active")); + Tuple> tuple = new Tuple<>(data, 0, format); + + TestPerson result = TupleMapper.mapToPojo(tuple, TestPerson.class); + + assertNotNull(result); + assertEquals(1, result.id); + assertEquals("John", result.name); + assertEquals(true, result.active); + } + + @Test + public void testMapToPojoFromTupleWithoutFormat() { + List data = Arrays.asList(1, "John", true); + Tuple> tuple = new Tuple<>(data, 0); // no format + + assertThrows(IllegalStateException.class, () -> TupleMapper.mapToPojo(tuple, TestPerson.class)); + } + + @Test + public void testMapToPojoFromNullTuple() { + TestPerson result = TupleMapper.mapToPojo((Tuple>) null, TestPerson.class); + assertNull(result); + } + + @Test + public void testMapToPojoList() { + List> tuples = + Arrays.asList(Arrays.asList(1, "John", true), Arrays.asList(2, "Jane", false)); + List format = + Arrays.asList( + new Field().setName("id"), new Field().setName("name"), new Field().setName("active")); + + List result = TupleMapper.mapToPojoList(tuples, format, TestPerson.class); + + assertEquals(2, result.size()); + assertEquals(1, result.get(0).id); + assertEquals("John", result.get(0).name); + assertEquals(2, result.get(1).id); + assertEquals("Jane", result.get(1).name); + } + + @Test + public void testMapToPojoListFromTuples() { + List format = + Arrays.asList( + new Field().setName("id"), new Field().setName("name"), new Field().setName("active")); + List>> tuples = + Arrays.asList( + new Tuple<>(Arrays.asList(1, "John", true), 0, format), + new Tuple<>(Arrays.asList(2, "Jane", false), 0, format)); + + List result = TupleMapper.mapToPojoListFromTuples(tuples, TestPerson.class); + + assertEquals(2, result.size()); + assertEquals(1, result.get(0).id); + assertEquals("John", result.get(0).name); + assertEquals(2, result.get(1).id); + assertEquals("Jane", result.get(1).name); + } + + @Test + public void testMapToPojoListWithEmptyInput() { + List result1 = + TupleMapper.mapToPojoList(null, Collections.emptyList(), TestPerson.class); + assertTrue(result1.isEmpty()); + + List result2 = + TupleMapper.mapToPojoList( + Collections.emptyList(), Collections.emptyList(), TestPerson.class); + assertTrue(result2.isEmpty()); + } + + // Test POJO class + public static class TestPerson { + public Integer id; + public String name; + public Boolean active; + + public TestPerson() {} + } +} diff --git a/tarantool-shared-resources/vshard_cluster/Dockerfile b/tarantool-shared-resources/vshard_cluster/Dockerfile index 8178cd42..5b17e584 100644 --- a/tarantool-shared-resources/vshard_cluster/Dockerfile +++ b/tarantool-shared-resources/vshard_cluster/Dockerfile @@ -8,6 +8,8 @@ ENV TARANTOOL_WORKDIR=$TARANTOOL_WORKDIR WORKDIR $TARANTOOL_WORKDIR COPY "cluster" "$TARANTOOL_WORKDIR" +RUN sed -i 's|http://archive.ubuntu.com/ubuntu/|http://mirror.yandex.ru/ubuntu/|g; s|http://security.ubuntu.com/ubuntu/|http://mirror.yandex.ru/ubuntu/|g' /etc/apt/sources.list + # install dependencies RUN apt-get -y update && \ apt-get -y install build-essential cmake make gcc git unzip cartridge-cli && \