From 36b7d74c74a8e4eba1d471423dd0a77b9fa9e41a Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Fri, 16 May 2025 10:33:12 +0100 Subject: [PATCH 1/4] ydb-platformGH-180 Introduced YdbType complement type-safe variant --- .../tech/ydb/data/core/convert/YQLType.java | 86 +++++++++++++++---- .../tech/ydb/data/core/convert/YdbConst.java | 21 +++++ .../core/convert/YdbMappingJdbcConverter.java | 38 +++++++- .../ydb/data/core/convert/YdbSqlType.java | 40 +++++++++ .../tech/ydb/data/core/convert/YdbType.java | 11 +++ .../data/core/convert/annotation/YdbType.java | 39 +++++++++ 6 files changed, 213 insertions(+), 22 deletions(-) create mode 100644 spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbConst.java create mode 100644 spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbSqlType.java create mode 100644 spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/annotation/YdbType.java diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YQLType.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YQLType.java index 778d597..73559ae 100644 --- a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YQLType.java +++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YQLType.java @@ -1,25 +1,75 @@ package tech.ydb.data.core.convert; -import java.sql.SQLType; -import tech.ydb.jdbc.YdbConst; -import tech.ydb.table.values.PrimitiveType; - -/** - * @author Madiyar Nurgazin - */ -public record YQLType(PrimitiveType type) implements SQLType { - @Override - public String getName() { - return type.name(); - } - @Override - public String getVendor() { - return "YDB"; +public enum YQLType { + /** Boolean value. */ + Bool(YdbConst.SQL_KIND_PRIMITIVE + 0), + /** A signed integer. Acceptable values: from -2^7 to 2^7–1. Not supported for table columns */ + Int8(YdbConst.SQL_KIND_PRIMITIVE + 1), + /** An unsigned integer. Acceptable values: from 0 to 2^8–1. */ + Uint8(YdbConst.SQL_KIND_PRIMITIVE + 2), + /** A signed integer. Acceptable values: from –2^15 to 2^15–1. Not supported for table columns */ + Int16(YdbConst.SQL_KIND_PRIMITIVE + 3), + /** An unsigned integer. Acceptable values: from 0 to 2^16–1. Not supported for table columns */ + Uint16(YdbConst.SQL_KIND_PRIMITIVE + 4), + /** A signed integer. Acceptable values: from –2^31 to 2^31–1. */ + Int32(YdbConst.SQL_KIND_PRIMITIVE + 5), + /** An unsigned integer. Acceptable values: from 0 to 2^32–1. */ + Uint32(YdbConst.SQL_KIND_PRIMITIVE + 6), + /** A signed integer. Acceptable values: from –2^63 to 2^63–1. */ + Int64(YdbConst.SQL_KIND_PRIMITIVE + 7), + /** An unsigned integer. Acceptable values: from 0 to 2^64–1. */ + Uint64(YdbConst.SQL_KIND_PRIMITIVE + 8), + /** A real number with variable precision, 4 bytes in size. Can't be used in the primary key */ + Float(YdbConst.SQL_KIND_PRIMITIVE + 9), + /** A real number with variable precision, 8 bytes in size. Can't be used in the primary key */ + Double(YdbConst.SQL_KIND_PRIMITIVE + 10), + /** A binary data, synonym for YDB type String */ + Bytes(YdbConst.SQL_KIND_PRIMITIVE + 11), + /** Text encoded in UTF-8, synonym for YDB type Utf8 */ + Text(YdbConst.SQL_KIND_PRIMITIVE + 12), + /** YSON in a textual or binary representation. Doesn't support matching, can't be used in the primary key */ + Yson(YdbConst.SQL_KIND_PRIMITIVE + 13), + /** JSON represented as text. Doesn't support matching, can't be used in the primary key */ + Json(YdbConst.SQL_KIND_PRIMITIVE + 14), + /** Universally unique identifier UUID. Not supported for table columns */ + Uuid(YdbConst.SQL_KIND_PRIMITIVE + 15), + /** Date, precision to the day */ + Date(YdbConst.SQL_KIND_PRIMITIVE + 16), + /** Date/time, precision to the second */ + Datetime(YdbConst.SQL_KIND_PRIMITIVE + 17), + /** Date/time, precision to the microsecond */ + Timestamp(YdbConst.SQL_KIND_PRIMITIVE + 18), + /** Time interval (signed), precision to microseconds */ + Interval(YdbConst.SQL_KIND_PRIMITIVE + 19), + /** Date with time zone label, precision to the day */ + TzDate(YdbConst.SQL_KIND_PRIMITIVE + 20), + /** Date/time with time zone label, precision to the second */ + TzDatetime(YdbConst.SQL_KIND_PRIMITIVE + 21), + /** Date/time with time zone label, precision to the microsecond */ + TzTimestamp(YdbConst.SQL_KIND_PRIMITIVE + 22), + /** JSON in an indexed binary representation. Doesn't support matching, can't be used in the primary key */ + JsonDocument(YdbConst.SQL_KIND_PRIMITIVE + 23), + + // DyNumber(YdbConst.SQL_KIND_PRIMITIVE + 24), -- not supported by JDBC Driver + + Date32(YdbConst.SQL_KIND_PRIMITIVE + 25), + + Datetime64(YdbConst.SQL_KIND_PRIMITIVE + 26), + + Timestamp64(YdbConst.SQL_KIND_PRIMITIVE + 27), + + Interval64(YdbConst.SQL_KIND_PRIMITIVE + 28), + + Decimal(YdbConst.SQL_DEFAULT_DECIMAL); // special case + + private final int sqlType; + + private YQLType(int sqlType) { + this.sqlType = sqlType; } - @Override - public Integer getVendorTypeNumber() { - return YdbConst.SQL_KIND_PRIMITIVE + type.ordinal(); + public int getSqlType() { + return this.sqlType; } } diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbConst.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbConst.java new file mode 100644 index 0000000..e7633ad --- /dev/null +++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbConst.java @@ -0,0 +1,21 @@ +package tech.ydb.data.core.convert; + +/** + * That class contain custom YDB type codes + * @see JDBC Driver constants + * @see Primitive types + * @see Decimal type + * + * @author Aleksandr Gorshenin + */ +final class YdbConst { + public static final int SQL_KIND_PRIMITIVE = 10000; + public static final int SQL_DEFAULT_DECIMAL = ydbDecimal(22, 9); + private static final int SQL_KIND_DECIMAL = 1 << 14; // 16384 + + public static int ydbDecimal(int precision, int scale) { + return SQL_KIND_DECIMAL + (precision << 6) + (scale & 0x111111); + } + + private YdbConst() { }; +} diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbMappingJdbcConverter.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbMappingJdbcConverter.java index 7d8c8ea..57f44ec 100644 --- a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbMappingJdbcConverter.java +++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbMappingJdbcConverter.java @@ -1,18 +1,30 @@ package tech.ydb.data.core.convert; import java.sql.SQLType; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.convert.JdbcTypeFactory; import org.springframework.data.jdbc.core.convert.MappingJdbcConverter; import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; -import tech.ydb.table.values.PrimitiveType; + +import tech.ydb.data.core.convert.annotation.YdbType; + /** * @author Madiyar Nurgazin + * @author Mikhail Polivakha */ +@SuppressWarnings("removal") public class YdbMappingJdbcConverter extends MappingJdbcConverter { + private final static Class ANNOTATION = YdbType.class; + private final static Class OLD_TYPE = tech.ydb.data.core.convert.YdbType.class; + + private final ConcurrentMap typesCache = new ConcurrentHashMap<>(); + public YdbMappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, CustomConversions conversions, JdbcTypeFactory typeFactory) { super(context, relationResolver, conversions, typeFactory); @@ -20,8 +32,26 @@ public YdbMappingJdbcConverter(RelationalMappingContext context, RelationResolve @Override public SQLType getTargetSqlType(RelationalPersistentProperty property) { - return property.isAnnotationPresent(YdbType.class) ? - new YQLType(PrimitiveType.valueOf(property.getRequiredAnnotation(YdbType.class).value())) : - super.getTargetSqlType(property); + return typesCache.computeIfAbsent(property, this::resolveSqlType); + } + + private SQLType resolveSqlType(RelationalPersistentProperty property) { + if (property.isAnnotationPresent(ANNOTATION)) { + tech.ydb.data.core.convert.annotation.YdbType type = property.getRequiredAnnotation(ANNOTATION); + YQLType yql = type.value(); + if (yql == YQLType.Decimal) { + int precision = type.decimalPrecision(); + int scale = type.decimalScale(); + return new YdbSqlType(precision, scale); + } + return new YdbSqlType(yql); + } + + if (property.isAnnotationPresent(OLD_TYPE)) { + String typeName = property.getRequiredAnnotation(OLD_TYPE).value(); + return new YdbSqlType(YQLType.valueOf(typeName)); + } + + return super.getTargetSqlType(property); } } diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbSqlType.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbSqlType.java new file mode 100644 index 0000000..8bc19be --- /dev/null +++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbSqlType.java @@ -0,0 +1,40 @@ +package tech.ydb.data.core.convert; + +import java.io.Serializable; +import java.sql.SQLType; + +/** + * + * @author Aleksandr Gorshenin + */ +class YdbSqlType implements SQLType, Serializable { + private static final long serialVersionUID = -5722445668088782880L; + + private final String name; + private final int vendorCode; + + public YdbSqlType(YQLType type) { + this.name = type.name(); + this.vendorCode = type.getSqlType(); + } + + public YdbSqlType(int decimalPrecision, int decimalScale) { + this.name = "Decimal(" + decimalPrecision + "," + decimalScale + ")"; + this.vendorCode = YdbConst.ydbDecimal(decimalPrecision, decimalScale); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getVendor() { + return "YDB"; + } + + @Override + public Integer getVendorTypeNumber() { + return vendorCode; + } +} diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbType.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbType.java index 1bdeafc..23c76c9 100644 --- a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbType.java +++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/YdbType.java @@ -1,15 +1,26 @@ package tech.ydb.data.core.convert; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** + * The annotation for qualification of the target YDB data type. + * * @author Madiyar Nurgazin + * @author Mikhail Polivakha + * @deprecated Please, use {@link tech.ydb.data.core.convert.annotation.YdbType} instead because of type safety considerations. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) +@Deprecated(forRemoval = true) +@Documented public @interface YdbType { + /** + * The target YDB data type. + * @return name of YDB data type + */ String value(); } diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/annotation/YdbType.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/annotation/YdbType.java new file mode 100644 index 0000000..a8c2b32 --- /dev/null +++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/convert/annotation/YdbType.java @@ -0,0 +1,39 @@ +package tech.ydb.data.core.convert.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import tech.ydb.data.core.convert.YQLType; + + +/** + * The annotation for qualification of the target YDB data type. + * + * @author Mikhail Polivakha + * @author Aleksandr Gorshenin + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +public @interface YdbType { + /** + * The target YDB data type. + * @return The target YDB data type. + */ + YQLType value(); + + /** + * Decimal precision. Applies only to {@link YQLType#Decimal } + * @return Custom decimal type precision. + */ + int decimalPrecision() default 22; + + /** + * Decimal scale. Applies only to {@link YQLType#Decimal } + * @return Custom decimal type scale. + */ + int decimalScale() default 9; +} From 974151d1001ef94a797db01146f6c0d7b543dd8c Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Fri, 16 May 2025 14:24:00 +0100 Subject: [PATCH 2/4] Updated tests --- .../all_types_table/AllTypesTableTest.java | 8 +- .../entity/AllTypesEntity.java | 134 +++++++++++++++--- .../books/RepositoriesIntegrationTest.java | 110 ++++++-------- .../tech/ydb/data/books/entity/Author.java | 59 ++++++-- .../java/tech/ydb/data/books/entity/Book.java | 93 ++++++++++-- .../tech/ydb/data/books/entity/Review.java | 93 ++++++++++-- 6 files changed, 380 insertions(+), 117 deletions(-) diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/AllTypesTableTest.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/AllTypesTableTest.java index ea1e98b..e50df70 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/AllTypesTableTest.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/AllTypesTableTest.java @@ -7,11 +7,13 @@ import java.time.temporal.ChronoUnit; import java.util.List; import java.util.Optional; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.jdbc.core.JdbcAggregateOperations; import org.springframework.data.relational.core.conversion.DbActionExecutionException; + import tech.ydb.data.YdbBaseTest; import tech.ydb.data.all_types_table.entity.AllTypesEntity; import tech.ydb.data.all_types_table.repository.AllTypesEntityRepository; @@ -41,6 +43,7 @@ public void allTypesTableCrudTest() { ); repository.save(expected); + Assertions.assertEquals(expected.getDecimalColumn(), entity1.get().getDecimalColumn()); Assertions.assertEquals(expected, entity1.get()); Assertions.assertEquals(expected.getTextColumn(), repository.findAllByTextColumn("Madiyar Nurgazin").get(0).getTextColumn()); @@ -69,10 +72,11 @@ public void allTypesTableCrudTest() { entities = repository.findAllByDateColumnAfterNow(); Assertions.assertEquals(1, entities.size()); - Assertions.assertEquals(4, entities.get(0).getId()); + Assertions.assertEquals(Integer.valueOf(4), entities.get(0).getId()); entity3.setJsonColumn("Not json"); - Assertions.assertThrows(DbActionExecutionException.class, () -> repository.save(entity3)); + var ex = Assertions.assertThrows(DbActionExecutionException.class, () -> repository.save(entity3)); + Assertions.assertTrue(ex.getMessage().startsWith("Failed to execute DbAction.UpdateRoot")); entity3.setJsonColumn("{\"values\": [1, 2, 3]}"); AllTypesEntity updated = repository.save(entity3); diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/entity/AllTypesEntity.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/entity/AllTypesEntity.java index f1caa6b..9fd5b7d 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/entity/AllTypesEntity.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/entity/AllTypesEntity.java @@ -4,20 +4,20 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; -import lombok.AllArgsConstructor; -import lombok.Data; +import java.util.Arrays; +import java.util.Objects; + import org.springframework.data.annotation.Id; -import org.springframework.data.domain.Persistable; import org.springframework.data.relational.core.mapping.Table; -import tech.ydb.data.core.convert.YdbType; + +import tech.ydb.data.core.convert.YQLType; +import tech.ydb.data.core.convert.annotation.YdbType; /** * @author Madiyar Nurgazin */ -@AllArgsConstructor -@Data @Table("all_types_table") -public class AllTypesEntity implements Persistable { +public class AllTypesEntity { @Id private Integer id; private String textColumn; @@ -29,28 +29,128 @@ public class AllTypesEntity implements Persistable { private double doubleColumn; private BigDecimal decimalColumn; private byte[] binaryColumn; - @YdbType("Date") + @YdbType(YQLType.Date) private LocalDate dateColumn; - @YdbType("Datetime") + @YdbType(YQLType.Datetime) private LocalDateTime datetimeColumn; private Instant timestampColumn; - @YdbType("Json") + @YdbType(YQLType.Json) private String jsonColumn; - @YdbType("JsonDocument") + @YdbType(YQLType.JsonDocument) private String jsonDocumentColumn; - @YdbType("Uint8") + @YdbType(YQLType.Uint8) private byte uint8Column; - @YdbType("Uint16") + @YdbType(YQLType.Uint16) private short uint16Column; - @YdbType("Uint32") + @YdbType(YQLType.Uint32) private int uint32Column; - @YdbType("Uint64") + @YdbType(YQLType.Uint64) private long uint64Column; public AllTypesEntity() { } + + public AllTypesEntity(Integer id, String textColumn, boolean boolColumn, byte tinyintColumn, short smallintColumn, + long bigintColumn, float floatColumn, double doubleColumn, BigDecimal decimalColumn, byte[] binaryColumn, + LocalDate dateColumn, LocalDateTime datetimeColumn, Instant timestampColumn, String jsonColumn, + String jsonDocumentColumn, byte uint8Column, short uint16Column, int uint32Column, long uint64Column) { + this.id = id; + this.textColumn = textColumn; + this.boolColumn = boolColumn; + this.tinyintColumn = tinyintColumn; + this.smallintColumn = smallintColumn; + this.bigintColumn = bigintColumn; + this.floatColumn = floatColumn; + this.doubleColumn = doubleColumn; + this.decimalColumn = decimalColumn; + this.binaryColumn = binaryColumn; + this.dateColumn = dateColumn; + this.datetimeColumn = datetimeColumn; + this.timestampColumn = timestampColumn; + this.jsonColumn = jsonColumn; + this.jsonDocumentColumn = jsonDocumentColumn; + this.uint8Column = uint8Column; + this.uint16Column = uint16Column; + this.uint32Column = uint32Column; + this.uint64Column = uint64Column; + } + + public Integer getId() { + return id; + } + + public String getTextColumn() { + return textColumn; + } + + public BigDecimal getDecimalColumn() { + return decimalColumn; + } + + public void setJsonColumn(String jsonColumn) { + this.jsonColumn = jsonColumn; + } + + public void setJsonDocumentColumn(String jsonDocumentColumn) { + this.jsonDocumentColumn = jsonDocumentColumn; + } + @Override - public boolean isNew() { - return false; + public int hashCode() { + return Objects.hash( + id, + textColumn, + boolColumn, + tinyintColumn, + smallintColumn, + bigintColumn, + floatColumn, + doubleColumn, + decimalColumn, + binaryColumn, + dateColumn, + datetimeColumn, + timestampColumn, + jsonColumn, + jsonDocumentColumn, + uint8Column, + uint16Column, + uint32Column, + uint64Column + ); } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || obj.getClass() != getClass()) { + return false; + } + + AllTypesEntity other = (AllTypesEntity) obj; + return Objects.equals(id, other.id) + && Objects.equals(textColumn, other.textColumn) + && boolColumn == other.boolColumn + && tinyintColumn == other.tinyintColumn + && smallintColumn == other.smallintColumn + && bigintColumn == other.bigintColumn + && floatColumn == other.floatColumn + && doubleColumn == other.doubleColumn + && Objects.equals(decimalColumn, other.decimalColumn) + && Arrays.equals(binaryColumn, other.binaryColumn) + && Objects.equals(dateColumn, other.dateColumn) + && Objects.equals(datetimeColumn, other.datetimeColumn) + && Objects.equals(timestampColumn, other.timestampColumn) + && Objects.equals(jsonColumn, other.jsonColumn) + && Objects.equals(jsonDocumentColumn, other.jsonDocumentColumn) + && uint8Column == other.uint8Column + && uint16Column == other.uint16Column + && uint32Column == other.uint32Column + && uint64Column == other.uint64Column; + } + + } diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/RepositoriesIntegrationTest.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/RepositoriesIntegrationTest.java index 1608a7f..540c6ea 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/RepositoriesIntegrationTest.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/RepositoriesIntegrationTest.java @@ -1,14 +1,17 @@ package tech.ydb.data.books; import java.time.Instant; +import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; + import tech.ydb.data.YdbBaseTest; import tech.ydb.data.books.entity.Author; import tech.ydb.data.books.entity.Book; @@ -32,29 +35,27 @@ public class RepositoriesIntegrationTest extends YdbBaseTest { @Test public void crudTest() { - Assertions.assertEquals(1, authorRepository.findAuthorByName("Leo Tolstoy").get(0).getId()); + Assertions.assertEquals(Long.valueOf(1), authorRepository.findAuthorByName("Leo Tolstoy").get(0).getId()); - Review review1 = createReview( - 1, 1, "Ivan Ivanov", "Masterpiece!", 10, Instant.parse("2024-03-19T15:52:26Z") - ); + Review review1 = reviewRepository.findById(1L).orElseThrow(); + Review review2 = reviewRepository.findById(2L).orElseThrow(); - Review review2 = createReview( - 2, 1, "Sergey Petrov", "Complex work, but I liked it", 9, Instant.parse("2024-03-19T16:14:05Z") - ); + Book book1 = new Book(1, "War and Peace", "1234", 1869); + book1.getReviews().add(review1); + book1.getReviews().add(review2); + book1.getAuthors().add(new BookAuthor(1, 1)); - List expected = List.of( - createBook(1, "War and Peace", "1234", 1869, Set.of(review1, review2), Set.of(new BookAuthor(1, 1))), - createBook(2, "Anna Karenina", "5678", 1878, Set.of(), Set.of(new BookAuthor(1, 2))) - ); + Book book2 = new Book(2, "Anna Karenina", "5678", 1878); + book1.getAuthors().add(new BookAuthor(1, 2)); + + List expected = List.of(book1, book2); Iterable books = bookRepository.findAll(); Assertions.assertEquals(expected, books); Optional bookO = bookRepository.findBookByTitle("War and Peace"); Assertions.assertTrue(bookO.isPresent()); - Review review3 = createReview( - 3, 1, "Madiyar Nurgazin", "Great", 8, Instant.parse("2024-03-19T20:00:00Z") - ); + Review review3 = new Review(3, 1, "Madiyar Nurgazin", "Great", 8, Instant.parse("2024-03-19T20:00:00Z")); Book book = bookO.get(); book.getReviews().add(review3); @@ -63,11 +64,11 @@ public void crudTest() { Assertions.assertEquals(3, reviewRepository.count()); review1.setRating(100); - review1.setNew(false); + review1.setIsNew(false); review2.setRating(90); - review2.setNew(false); + review2.setIsNew(false); review3.setRating(80); - review3.setNew(false); + review3.setIsNew(false); Set reviews = Set.of(review1, review2, review3); reviewRepository.saveAll(reviews); @@ -78,13 +79,15 @@ public void crudTest() { book = bookO.get(); Assertions.assertEquals(reviews, book.getReviews()); - Author author1 = createAuthor(2, "Author 1"); - Author author2 = createAuthor(3, "Author 2"); + Author author1 = new Author(2, "Author 1"); + Author author2 = new Author(3, "Author 2"); authorRepository.saveAll(List.of(author1, author2)); Assertions.assertEquals(3, authorRepository.count()); - book = createBook(3, "Title", "Isbn", 2024, Set.of(), Set.of(new BookAuthor(2, 3), new BookAuthor(3, 3))); + book = new Book(3, "Title", "Isbn", 2024); + book.getAuthors().add(new BookAuthor(2, 3)); + book.getAuthors().add(new BookAuthor(3, 3)); bookRepository.save(book); expected.get(0).setReviews(Set.of(review1, review2, review3)); @@ -97,7 +100,7 @@ public void crudTest() { List authors = authorRepository.findAuthorsByBookId(3); Assertions.assertEquals(Set.of(author1, author2), Set.copyOf(authors)); - Review review = createReview(4, 3, "Reader", "Text", 5, Instant.now()); + Review review = new Review(4, 3, "Reader", "Text", 5, Instant.now()); reviewRepository.save(review); bookRepository.deleteById(3L); @@ -114,16 +117,12 @@ public void crudTest() { @Test public void pagingAndSortingTest() { - Review review1 = createReview( - 1, 1, "Ivan Ivanov", "Masterpiece!", 10, Instant.parse("2024-03-19T15:52:26Z") - ); - Review review2 = createReview( - 2, 1, "Sergey Petrov", "Complex work, but I liked it", 9, Instant.parse("2024-03-19T16:14:05Z") - ); - Review review3 = createReview(3, 1, "Reader", "Text", 100, Instant.parse("2024-03-19T21:00:00Z")); - Review review4 = createReview(4, 1, "Reader", "Text2", 80, Instant.parse("2024-03-19T22:00:00Z")); - Review review5 = createReview(5, 1, "Reader", "Text3", 75, Instant.parse("2024-03-19T23:00:00Z")); - Review review6 = createReview(6, 1, "Reader", "Text4", 50, Instant.parse("2024-03-20T00:00:00Z")); + Review review1 = reviewRepository.findById(1L).orElseThrow(); + Review review2 = reviewRepository.findById(2L).orElseThrow(); + Review review3 = new Review(3, 1, "Reader", "Text", 100, Instant.parse("2024-03-19T21:00:00Z")); + Review review4 = new Review(4, 1, "Reader", "Text2", 80, Instant.parse("2024-03-19T22:00:00Z")); + Review review5 = new Review(5, 1, "Reader", "Text3", 75, Instant.parse("2024-03-19T23:00:00Z")); + Review review6 = new Review(6, 1, "Reader", "Text4", 50, Instant.parse("2024-03-20T00:00:00Z")); reviewRepository.saveAll(List.of(review3, review4, review5, review6)); Iterable reviews = reviewRepository.findByReader( @@ -137,6 +136,21 @@ public void pagingAndSortingTest() { Assertions.assertEquals(List.of(review5, review6), reviews); reviews = reviewRepository.findAll(Sort.by("created").descending()); + + Iterator it = reviews.iterator(); + Assertions.assertTrue(it.hasNext()); + Assertions.assertEquals(review6, it.next()); + Assertions.assertTrue(it.hasNext()); + Assertions.assertEquals(review5, it.next()); + Assertions.assertTrue(it.hasNext()); + Assertions.assertEquals(review4, it.next()); + Assertions.assertTrue(it.hasNext()); + Assertions.assertEquals(review3, it.next()); + Assertions.assertTrue(it.hasNext()); + Assertions.assertEquals(review2, it.next()); + Assertions.assertTrue(it.hasNext()); + Assertions.assertEquals(review1, it.next()); + Assertions.assertEquals(List.of(review6, review5, review4, review3, review2, review1), reviews); reviews = reviewRepository.findAll(PageRequest.of(0, 3, Sort.by("id"))).getContent(); @@ -148,38 +162,4 @@ public void pagingAndSortingTest() { reviews = reviewRepository.findAll(PageRequest.of(2, 2)).getContent(); Assertions.assertEquals(List.of(review5, review6), reviews); } - - private Review createReview(long id, long bookId, String reader, String text, long rating, Instant created) { - Review review = new Review(); - review.setId(id); - review.setBookId(bookId); - review.setReader(reader); - review.setText(text); - review.setRating(rating); - review.setCreated(created); - review.setNew(true); - return review; - } - - private Book createBook( - long id, String title, String isbn, long year, Set reviews, Set authors - ) { - Book book = new Book(); - book.setId(id); - book.setTitle(title); - book.setIsbn(isbn); - book.setYear(year); - book.setReviews(reviews); - book.setAuthors(authors); - book.setNew(true); - return book; - } - - private Author createAuthor(long id, String name) { - Author author = new Author(); - author.setId(id); - author.setName(name); - author.setNew(true); - return author; - } } diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Author.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Author.java index 7f8e15b..c1c0469 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Author.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Author.java @@ -1,9 +1,9 @@ package tech.ydb.data.books.entity; import java.util.HashSet; +import java.util.Objects; import java.util.Set; -import lombok.Data; -import lombok.EqualsAndHashCode; + import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient; import org.springframework.data.domain.Persistable; @@ -13,21 +13,29 @@ /** * @author Madiyar Nurgazin */ -@Data @Table("authors") public class Author implements Persistable { @Id private Long id; private String name; + @MappedCollection(idColumn = "author_id") - private Set books; + private Set books = new HashSet<>(); @Transient - @EqualsAndHashCode.Exclude - private boolean isNew; + private boolean isNew = false; + + public Author() { } + + public Author(long id, String name) { + this.id = id; + this.name = name; + this.isNew = true; + } - public Author() { - this.books = new HashSet<>(); + @Override + public Long getId() { + return id; } @Override @@ -35,7 +43,38 @@ public boolean isNew() { return isNew; } - public void setNew(boolean isNew) { - this.isNew = isNew; + public String getName() { + return name; + } + + public Set getBooks() { + return books; + } + + public void setName(String name) { + this.name = name; + } + + public void setBooks(Set books) { + this.books = books; + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || obj.getClass() != getClass()) { + return false; + } + + Author other = (Author) obj; + return Objects.equals(id, other.id) && Objects.equals(name, other.name); } } diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Book.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Book.java index cd9b89e..9fd4614 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Book.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Book.java @@ -1,10 +1,9 @@ package tech.ydb.data.books.entity; import java.util.HashSet; +import java.util.Objects; import java.util.Set; -import lombok.Data; -import lombok.EqualsAndHashCode; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient; import org.springframework.data.domain.Persistable; @@ -14,35 +13,101 @@ /** * @author Madiyar Nurgazin */ -@Data @Table("books") -public class Book implements Persistable { +public class Book implements Persistable { @Id private Long id; private String title; private String isbn; private long year; + @Transient + private boolean isNew = false; + @MappedCollection(idColumn = "book_id") - private Set reviews; + private Set reviews = new HashSet<>(); @MappedCollection(idColumn = "book_id") - private Set authors; + private Set authors = new HashSet<>(); - @Transient - @EqualsAndHashCode.Exclude - private boolean isNew; + public Book() { } + + public Book(long id, String title, String isbn, long year) { + this.id = id; + this.title = title; + this.isbn = isbn; + this.year = year; + this.isNew = true; + } + + @Override + public Long getId() { + return id; + } @Override public boolean isNew() { return isNew; } - public Book() { - this.reviews = new HashSet<>(); - this.authors = new HashSet<>(); + public String getTitle() { + return title; + } + + public String getIsbn() { + return isbn; + } + + public long getYear() { + return year; + } + + public Set getReviews() { + return reviews; + } + + public Set getAuthors() { + return authors; } - public void setNew(boolean isNew) { - this.isNew = isNew; + public void setTitle(String title) { + this.title = title; + } + + public void setIsbn(String isbn) { + this.isbn = isbn; + } + + public void setYear(long year) { + this.year = year; + } + + public void setReviews(Set reviews) { + this.reviews = reviews; + } + + public void setAuthors(Set authors) { + this.authors = authors; + } + + @Override + public int hashCode() { + return Objects.hash(id, title, isbn, year); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || obj.getClass() != getClass()) { + return false; + } + + Book other = (Book) obj; + return Objects.equals(id, other.id) + && Objects.equals(title, other.title) + && Objects.equals(isbn, other.isbn) + && year == other.year; } } diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Review.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Review.java index deb4cbf..fb87e6e 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Review.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/entity/Review.java @@ -1,8 +1,8 @@ package tech.ydb.data.books.entity; import java.time.Instant; -import lombok.Data; -import lombok.EqualsAndHashCode; +import java.util.Objects; + import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient; import org.springframework.data.domain.Persistable; @@ -11,7 +11,6 @@ /** * @author Madiyar Nurgazin */ -@Data @Table("reviews") public class Review implements Persistable { @Id @@ -22,20 +21,96 @@ public class Review implements Persistable { private long rating; private Instant created; - public Review() { - this.created = Instant.now(); + @Transient + private boolean isNew = false; + + public Review() { } + + public Review(long id, long bookId, String reader, String text, long rating, Instant created) { + this.id = id; + this.bookId = bookId; + this.reader = reader; + this.text = text; + this.rating = rating; + this.created = created; + this.isNew = true; } - @Transient - @EqualsAndHashCode.Exclude - private boolean isNew; + @Override + public Long getId() { + return id; + } @Override public boolean isNew() { return isNew; } - public void setNew(boolean isNew) { + public void setIsNew(boolean isNew) { this.isNew = isNew; } + + public long getBookId() { + return bookId; + } + + public String getReader() { + return reader; + } + + public String getText() { + return text; + } + + public long getRationg() { + return rating; + } + + public Instant getCreated() { + return created; + } + + public void setBookId(long bookId) { + this.bookId = bookId; + } + + public void setReader(String reader) { + this.reader = reader; + } + + public void setText(String text) { + this.text = text; + } + + public void setRating(long rating) { + this.rating = rating; + } + + public void setCreated(Instant created) { + this.created = created; + } + + @Override + public int hashCode() { + return Objects.hash(id, bookId, reader, text, rating, created); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || obj.getClass() != getClass()) { + return false; + } + + Review other = (Review) obj; + return Objects.equals(id, other.id) + && bookId == other.bookId + && Objects.equals(reader, other.reader) + && Objects.equals(text, other.text) + && rating == other.rating + && Objects.equals(created, other.created); + } } From bbe4db0a303c3ca652c4a8b65088d4be4e386605 Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Fri, 16 May 2025 14:24:32 +0100 Subject: [PATCH 3/4] Removed lombok and updated YDB JDBC dependency --- spring-data-jdbc-ydb/pom.xml | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/spring-data-jdbc-ydb/pom.xml b/spring-data-jdbc-ydb/pom.xml index aaa9f63..98eec66 100644 --- a/spring-data-jdbc-ydb/pom.xml +++ b/spring-data-jdbc-ydb/pom.xml @@ -50,13 +50,12 @@ 17 5.10.2 - 1.18.30 3.4.0 4.24.0 - 2.2.9 - 2.2.3 - 0.9.7 + 2.3.13 + 2.3.10 + 1.1.1 @@ -84,12 +83,6 @@ spring-data-jdbc provided - - tech.ydb.jdbc - ydb-jdbc-driver - ${ydb.jdbc.version} - provided - tech.ydb.test @@ -102,9 +95,9 @@ test - org.projectlombok - lombok - ${lombok.version} + tech.ydb.jdbc + ydb-jdbc-driver + ${ydb.jdbc.version} test @@ -126,7 +119,6 @@ org.liquibase liquibase-core - ${liquibase.version} test From 43b00e93759f04f66f14b3ea9a4583a754e37a6b Mon Sep 17 00:00:00 2001 From: Alexandr Gorshenin Date: Fri, 16 May 2025 14:36:40 +0100 Subject: [PATCH 4/4] Fixed tests for Hibernate Dialect --- hibernate-dialect/pom.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/hibernate-dialect/pom.xml b/hibernate-dialect/pom.xml index 617e64f..c9a702c 100644 --- a/hibernate-dialect/pom.xml +++ b/hibernate-dialect/pom.xml @@ -41,8 +41,8 @@ 5.9.3 2.17.2 - 2.3.4 - 2.3.3 + 2.3.13 + 2.3.10 @@ -146,7 +146,6 @@ true enable_parameterized_decimal - cr.yandex/yc/yandex-docker-local-ydb:trunk