diff --git a/.github/workflows/ci-spring-data-jdbc-ydb.yaml b/.github/workflows/ci-spring-data-jdbc-ydb.yaml index d9042d4..8a18d53 100644 --- a/.github/workflows/ci-spring-data-jdbc-ydb.yaml +++ b/.github/workflows/ci-spring-data-jdbc-ydb.yaml @@ -14,13 +14,44 @@ env: MAVEN_ARGS: --batch-mode --update-snapshots -Dstyle.color=always jobs: + prepare: + name: Prepare Maven cache + runs-on: ubuntu-24.04 + + env: + MAVEN_ARGS: --batch-mode -Dstyle.color=always + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'temurin' + cache: 'maven' + + - name: Download dependencies (Default) + working-directory: ./spring-data-jdbc-ydb + run: mvn $MAVEN_ARGS dependency:resolve-plugins dependency:go-offline + + - name: Download dependencies (Spring Boot 3) + working-directory: ./spring-data-jdbc-ydb + run: mvn $MAVEN_ARGS -Pspring-boot3 dependency:resolve-plugins dependency:go-offline + + - name: Download dependencies (Spring Boot 4) + working-directory: ./spring-data-jdbc-ydb + run: mvn $MAVEN_ARGS -Pspring-boot4 dependency:resolve-plugins dependency:go-offline + build: - name: Spring Data JDBC YDB Dialect - runs-on: ubuntu-latest + name: Spring Data JDBC YDB Dialect build & tests + runs-on: ubuntu-24.04 + needs: prepare strategy: matrix: - java: [ '17', '21' ] + java: [ '17', '21', '24' ] steps: - uses: actions/checkout@v4 @@ -32,16 +63,39 @@ jobs: distribution: 'temurin' cache: maven + - name: Build spring-data-jdbc YDB dialect + working-directory: ./spring-data-jdbc-ydb + run: mvn $MAVEN_ARGS package + + - name: Tests with Spring Boot 3 + working-directory: ./spring-data-jdbc-ydb + run: mvn $MAVEN_ARGS -Pspring-boot3 test + + - name: Tests with Spring Boot 4 + working-directory: ./spring-data-jdbc-ydb + run: mvn $MAVEN_ARGS -Pspring-boot4 test + + examples: + name: Spring Data JDBC YDB Dialect Examples + runs-on: ubuntu-24.04 + needs: build + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'temurin' + cache: maven + - name: Extract spring-data-jdbc YDB dialect version working-directory: ./spring-data-jdbc-ydb run: | VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) echo "SPRING_DATA_JDBC_DIALECT_VERSION=$VERSION" >> "$GITHUB_ENV" - - name: Download spring-data-jdbc YDB dialect dependencies - working-directory: ./spring-data-jdbc-ydb - run: mvn $MAVEN_ARGS dependency:go-offline - - name: Build spring-data-jdbc YDB dialect working-directory: ./spring-data-jdbc-ydb run: mvn $MAVEN_ARGS install diff --git a/.github/workflows/publish-spring-data-jdbc-ydb.yaml b/.github/workflows/publish-spring-data-jdbc-ydb.yaml index 8ee0f56..4f2e772 100644 --- a/.github/workflows/publish-spring-data-jdbc-ydb.yaml +++ b/.github/workflows/publish-spring-data-jdbc-ydb.yaml @@ -11,7 +11,7 @@ env: jobs: validate: name: Validate Spring Data JDBC YDB Dialect - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/spring-data-jdbc-ydb/README.md b/spring-data-jdbc-ydb/README.md index 0321637..f5d2dbd 100644 --- a/spring-data-jdbc-ydb/README.md +++ b/spring-data-jdbc-ydb/README.md @@ -6,12 +6,12 @@ ## Overview -This project is an extension for Spring Data JDBC +This project is an extension for Spring Data JDBC that provides support for working with [YDB](https://ydb.tech). ### Features -- Full support for basic operations with Spring Data JDBC +- Full support for basic operations with Spring Data JDBC - Supported VIEW INDEX statement from @ViewIndex annotation on method your Repository - @YdbType explicitly specifies the YDB data type (Json example in String type) @@ -22,7 +22,7 @@ that provides support for working with [YDB](https://ydb.tech). To use this Spring Data JDBC YDB Dialect, you'll need: - Java 17 or above. -- Spring Data JDBC 3+ +- Spring Data JDBC 3.4+ - [YDB JDBC Driver](https://github.com/ydb-platform/ydb-jdbc-driver) - Access to a YDB Database instance @@ -35,7 +35,7 @@ For Maven, add the following dependency to your pom.xml: tech.ydb.dialects spring-data-jdbc-ydb - ${spring.data.jdbc.ydb.version} + ${spring.data.jdbc.ydb.version} ``` diff --git a/spring-data-jdbc-ydb/pom.xml b/spring-data-jdbc-ydb/pom.xml index 08e02cc..b194fd8 100644 --- a/spring-data-jdbc-ydb/pom.xml +++ b/spring-data-jdbc-ydb/pom.xml @@ -50,7 +50,6 @@ 17 5.10.2 - 3.4.0 4.24.0 2.3.13 @@ -166,6 +165,34 @@ + + spring-boot-minimal + + true + + + 3.4.0 + + + + spring-boot3 + + 3.5.7 + + + + spring-boot4 + + 4.0.0 + + + + org.springframework.boot + spring-boot-starter-liquibase + test + + + ossrh-s01 diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/dialect/YdbDialect.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/dialect/YdbDialect.java index e1c1079..40a1f79 100644 --- a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/dialect/YdbDialect.java +++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/core/dialect/YdbDialect.java @@ -4,6 +4,8 @@ import java.util.function.Function; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; +import org.springframework.data.jdbc.core.convert.JdbcArrayColumns; +import org.springframework.data.jdbc.core.dialect.JdbcDialect; import org.springframework.data.relational.core.dialect.AbstractDialect; import org.springframework.data.relational.core.dialect.InsertRenderContext; import org.springframework.data.relational.core.dialect.LimitClause; @@ -19,9 +21,11 @@ * @author Madiyar Nurgazin * @author Mikhail Polivakha */ -public class YdbDialect extends AbstractDialect { +public class YdbDialect extends AbstractDialect implements JdbcDialect { public static final YdbDialect INSTANCE = new YdbDialect(); + private static final IdentifierProcessing.Quoting QUOTING = new IdentifierProcessing.Quoting("`"); + private static final LimitClause LIMIT_CLAUSE = new LimitClause() { @Override @@ -46,10 +50,12 @@ public Position getClausePosition() { }; private static final LockClause LOCK_CLAUSE = new LockClause() { + @Override public String getLock(LockOptions lockOptions) { throw new UnsupportedOperationException("YDB does not support pessimistic locks"); } + @Override public LockClause.Position getClausePosition() { return null; } @@ -99,10 +105,17 @@ public LockClause lock() { @Override public IdentifierProcessing getIdentifierProcessing() { - return IdentifierProcessing.create( - new IdentifierProcessing.Quoting("`"), - IdentifierProcessing.LetterCasing.AS_IS - ); + return new IdentifierProcessing() { + @Override + public String quote(String identifier) { + return QUOTING.apply(identifier); + } + + @Override + public String standardizeLetterCase(String identifier) { + return identifier; + } + }; } @Override @@ -116,4 +129,10 @@ public InsertRenderContext getInsertRenderContext() { public OrderByNullPrecedence orderByNullHandling() { return OrderByNullPrecedence.NONE; } + + @Override + @SuppressWarnings("removal") + public JdbcArrayColumns getArraySupport() { + return JdbcArrayColumns.Unsupported.INSTANCE; + } } diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/config/AbstractYdbJdbcConfiguration.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/config/AbstractYdbJdbcConfiguration.java index bb22e65..a8fdf23 100644 --- a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/config/AbstractYdbJdbcConfiguration.java +++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/config/AbstractYdbJdbcConfiguration.java @@ -8,10 +8,12 @@ import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.RelationResolver; +import org.springframework.data.jdbc.core.dialect.JdbcDialect; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.config.AbstractJdbcConfiguration; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; + import tech.ydb.data.core.convert.YdbMappingJdbcConverter; /** @@ -22,7 +24,24 @@ @Import(JdbcRepositoryBeanPostProcessor.class) public class AbstractYdbJdbcConfiguration extends AbstractJdbcConfiguration { - @Override + // Spring Boot 4 support + @SuppressWarnings({"override", "removal"}) + public JdbcConverter jdbcConverter( + JdbcMappingContext mappingContext, + NamedParameterJdbcOperations operations, + @Lazy RelationResolver relationResolver, + JdbcCustomConversions conversions, + JdbcDialect dialect + ) { + DefaultJdbcTypeFactory jdbcTypeFactory = new DefaultJdbcTypeFactory( + operations.getJdbcOperations(), JdbcArrayColumns.Unsupported.INSTANCE + ); + + return new YdbMappingJdbcConverter(mappingContext, relationResolver, conversions, jdbcTypeFactory); + } + + // Spring Boot 3 support + @SuppressWarnings({"override", "removal"}) public JdbcConverter jdbcConverter( JdbcMappingContext mappingContext, NamedParameterJdbcOperations operations, diff --git a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/config/YdbDialectProvider.java b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/config/YdbDialectProvider.java index 3d81f85..43dfedd 100644 --- a/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/config/YdbDialectProvider.java +++ b/spring-data-jdbc-ydb/src/main/java/tech/ydb/data/repository/config/YdbDialectProvider.java @@ -9,12 +9,13 @@ import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.jdbc.core.ConnectionCallback; import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.lang.Nullable; + import tech.ydb.data.core.dialect.YdbDialect; /** * @author Madiyar Nurgazin */ +@SuppressWarnings("removal") // Spring Boot 3 support public class YdbDialectProvider extends DialectResolver.DefaultDialectProvider { @Override public Optional getDialect(JdbcOperations operations) { @@ -29,7 +30,6 @@ public Optional getDialect(JdbcOperations operations) { return super.getDialect(operations); } - @Nullable private static Dialect getDialect(Connection connection) throws SQLException { if ("ydb".contains(connection.getMetaData().getDatabaseProductName().toLowerCase(Locale.ENGLISH))) { return YdbDialect.INSTANCE; diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbBaseTest.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbBaseTest.java index 93f0a8e..7708793 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbBaseTest.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbBaseTest.java @@ -1,25 +1,24 @@ package tech.ydb.data; import org.junit.jupiter.api.extension.RegisterExtension; -import org.springframework.boot.test.autoconfigure.data.jdbc.AutoConfigureDataJdbc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.transaction.annotation.Transactional; + import tech.ydb.test.junit5.YdbHelperExtension; /** * @author Madiyar Nurgazin */ @SpringBootTest(classes = YdbJdbcConfiguration.class) -@AutoConfigureDataJdbc @Transactional public abstract class YdbBaseTest { @RegisterExtension private static final YdbHelperExtension ydb = new YdbHelperExtension(); @DynamicPropertySource - private static void propertySource(DynamicPropertyRegistry registry) { + public static void propertySource(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", YdbBaseTest::jdbcUrl); } diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbJdbcConfiguration.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbJdbcConfiguration.java index 6f0bca8..0435eae 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbJdbcConfiguration.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/YdbJdbcConfiguration.java @@ -1,9 +1,10 @@ package tech.ydb.data; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; import org.springframework.data.jdbc.repository.config.EnableJdbcAuditing; import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; + import tech.ydb.data.repository.config.AbstractYdbJdbcConfiguration; /** @@ -16,5 +17,5 @@ basePackages = "tech.ydb.data" ) @EnableJdbcAuditing -@Import(AbstractYdbJdbcConfiguration.class) -public class YdbJdbcConfiguration {} \ No newline at end of file +@EnableAutoConfiguration +public class YdbJdbcConfiguration extends AbstractYdbJdbcConfiguration {} \ No newline at end of file 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 e50df70..f181a12 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 @@ -5,6 +5,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.List; import java.util.Optional; @@ -12,7 +13,7 @@ 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 org.springframework.jdbc.UncategorizedSQLException; import tech.ydb.data.YdbBaseTest; import tech.ydb.data.all_types_table.entity.AllTypesEntity; @@ -57,7 +58,8 @@ public void allTypesTableCrudTest() { aggregateOperations.insert(entity2); Assertions.assertEquals(2, repository.countDistinctTextColumn()); - List entities = repository.findAll(); + List entities = new ArrayList<>(); + repository.findAll().forEach(entities::add); Assertions.assertEquals(4, entities.size()); repository.deleteById(1); @@ -75,8 +77,12 @@ public void allTypesTableCrudTest() { Assertions.assertEquals(Integer.valueOf(4), entities.get(0).getId()); entity3.setJsonColumn("Not json"); - var ex = Assertions.assertThrows(DbActionExecutionException.class, () -> repository.save(entity3)); - Assertions.assertTrue(ex.getMessage().startsWith("Failed to execute DbAction.UpdateRoot")); + Throwable ex = Assertions.assertThrows(Exception.class, () -> repository.save(entity3)); + if (ex != null && !(ex instanceof UncategorizedSQLException)) { + ex = ex.getCause(); + } + Assertions.assertTrue(ex instanceof UncategorizedSQLException); + Assertions.assertTrue(ex != null && ex.getMessage().contains("Invalid Json value (S_ERROR)]")); 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/repository/AllTypesEntityRepository.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/AllTypesEntityRepository.java index a267e81..ebdb3ef 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/AllTypesEntityRepository.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/all_types_table/repository/AllTypesEntityRepository.java @@ -1,16 +1,18 @@ package tech.ydb.data.all_types_table.repository; import java.util.List; + import org.springframework.data.jdbc.repository.query.Query; -import org.springframework.data.repository.ListCrudRepository; +import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; + import tech.ydb.data.all_types_table.entity.AllTypesEntity; import tech.ydb.data.repository.ViewIndex; /** * @author Madiyar Nurgazin */ -public interface AllTypesEntityRepository extends ListCrudRepository { +public interface AllTypesEntityRepository extends CrudRepository { @Query("select count(distinct text_column) from all_types_table") long countDistinctTextColumn(); diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/AuthorRepository.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/AuthorRepository.java index 86582a4..c23c28b 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/AuthorRepository.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/AuthorRepository.java @@ -1,16 +1,18 @@ package tech.ydb.data.books.repository; import java.util.List; + import org.springframework.data.jdbc.repository.query.Query; -import org.springframework.data.repository.ListCrudRepository; +import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; + import tech.ydb.data.books.entity.Author; import tech.ydb.data.repository.ViewIndex; /** * @author Madiyar Nurgazin */ -public interface AuthorRepository extends ListCrudRepository { +public interface AuthorRepository extends CrudRepository { @Query("select authors.* from authors join books_authors on authors.id = books_authors.author_id" + " where books_authors.book_id = :bookId") List findAuthorsByBookId(@Param("bookId") long bookId); diff --git a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/ReviewRepository.java b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/ReviewRepository.java index dae7191..ca04672 100644 --- a/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/ReviewRepository.java +++ b/spring-data-jdbc-ydb/src/test/java/tech/ydb/data/books/repository/ReviewRepository.java @@ -1,17 +1,17 @@ package tech.ydb.data.books.repository; import java.util.List; + import org.springframework.data.domain.Pageable; import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; + import tech.ydb.data.books.entity.Review; /** * @author Madiyar Nurgazin */ -public interface ReviewRepository extends ListCrudRepository, - CrudRepository, PagingAndSortingRepository { +public interface ReviewRepository extends CrudRepository, PagingAndSortingRepository { List findByReader(String reader, Pageable pageable); }