diff --git a/jdbc/pom.xml b/jdbc/pom.xml index 335d86d..f583ae5 100644 --- a/jdbc/pom.xml +++ b/jdbc/pom.xml @@ -51,6 +51,7 @@ spring-flyway-app spring-liquibase-app shedlock + spring-data-jdbc diff --git a/jdbc/spring-data-jdbc/pom.xml b/jdbc/spring-data-jdbc/pom.xml new file mode 100644 index 0000000..21c09d1 --- /dev/null +++ b/jdbc/spring-data-jdbc/pom.xml @@ -0,0 +1,155 @@ + + 4.0.0 + + tech.ydb.jdbc.examples + ydb-jdbc-examples + 1.1.0-SNAPSHOT + + + + + org.springframework.boot + spring-boot-dependencies + 3.2.1 + import + pom + + + + spring-data-jdbc + Spring Data JDBC Example + Basic example for Spring Boot JDBC + + 17 + 1.9.22 + 0.9.1 + 3.2.1 + 10.7.1 + + + + org.springframework.data + spring-data-jdbc + + + org.springframework.boot + spring-boot-autoconfigure + + + org.flywaydb + flyway-core + ${flyway.version} + + + tech.ydb.dialects + flyway-ydb-dialect + 1.0.0-RC0 + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + tech.ydb.dialects + spring-data-jdbc-ydb + ${spring.data.jdbc.ydb.version} + + + tech.ydb.jdbc + ydb-jdbc-driver + + + com.github.javafaker + javafaker + 1.0.2 + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + 1.7.3 + + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + test + + + tech.ydb.test + ydb-junit5-support + test + + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/test/kotlin + + + org.apache.maven.plugins + maven-surefire-plugin + + + true + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + test-compile + + test-compile + + + + + + -Xjsr305=strict + + + spring + jpa + + + + + org.jetbrains.kotlin + kotlin-maven-allopen + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-maven-noarg + ${kotlin.version} + + + + + + \ No newline at end of file diff --git a/jdbc/spring-data-jdbc/src/main/kotlin/tech/ydb/jdbc/simple/SimpleUserRepository.kt b/jdbc/spring-data-jdbc/src/main/kotlin/tech/ydb/jdbc/simple/SimpleUserRepository.kt new file mode 100644 index 0000000..910e74a --- /dev/null +++ b/jdbc/spring-data-jdbc/src/main/kotlin/tech/ydb/jdbc/simple/SimpleUserRepository.kt @@ -0,0 +1,99 @@ +package tech.ydb.jdbc.simple + +import org.springframework.data.domain.Limit +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Slice +import org.springframework.data.domain.Sort +import org.springframework.data.jdbc.repository.query.Query +import org.springframework.data.repository.ListCrudRepository +import tech.ydb.data.repository.ViewIndex + +/** + * Simple repository interface for {@link User} instances. The interface is used to declare the so-called query methods, + * i.e. methods to retrieve single entities or collections of them. + */ +interface SimpleUserRepository : ListCrudRepository { + + /** + * Uses {@link Optional} as return and parameter type. + * + * @param username + */ + @ViewIndex(indexName = "username_index") + fun findByUsername(username: String): User? + + /** + * Find all users with the given lastname. This method will be translated into a query by constructing it directly + * from the method name as there is no other query declared. + * + * @param lastname + */ + fun findByLastname(lastname: String): List + + /** + * Find at most the number of users defined via maxResults with the given lastname. + * This method will be translated into a query by constructing it directly from the method name as there is no other + * query declared. + * + * @param lastname + * @param maxResults the maximum number of results returned. + */ + + fun findByLastname(lastname: String, maxResults: Limit): List + + /** + * Returns at most the number of users defined via {@link Limit} with the given firstname. This method will be + * translated into a query using the one declared in the {@link Query} annotation declared one. + * + * @param firstname + * @param maxResults the maximum number of results returned. + */ + fun findByFirstname(firstname: String, maxResults: Limit): List + + /** + * Returns all users with the given name as first- or lastname. This makes the query to method relation much more + * refactoring-safe as the order of the method parameters is completely irrelevant. + * + * @param name + */ + @Query("select * from Users u where u.firstname = :name or u.lastname = :name") + fun findByFirstnameOrLastname(name: String): List + + /** + * Returns a {@link Slice} counting a maximum number of {@link Pageable#getPageSize()} users matching given criteria + * starting at {@link Pageable#getOffset()} without prior count of the total number of elements available. + * + * @param lastname + * @param page + */ + fun findByLastnameOrderByUsernameAsc(lastname: String, page: Pageable): Slice + + /** + * Return the first 2 users ordered by their lastname asc. + * + *
+     * Example for findFirstK / findTopK functionality.
+     * 
+ */ + fun findFirst2ByOrderByLastnameAsc(): List + + /** + * Return the first 2 users ordered by the given {@code sort} definition. + * + *
+     * This variant is very flexible because one can ask for the first K results when a ASC ordering
+     * is used as well as for the last K results when a DESC ordering is used.
+     * 
+ * + * @param sort + */ + fun findTop2By(sort: Sort): List + + /** + * Return all the users with the given firstname or lastname. Makes use of SpEL (Spring Expression Language). + * + * @param user + */ + @Query("select * from Users u where u.firstname = :#{#user.firstname} or u.lastname = :#{#user.lastname}") + fun findByFirstnameOrLastname(user: User): Iterable +} diff --git a/jdbc/spring-data-jdbc/src/main/kotlin/tech/ydb/jdbc/simple/User.kt b/jdbc/spring-data-jdbc/src/main/kotlin/tech/ydb/jdbc/simple/User.kt new file mode 100644 index 0000000..2063f3a --- /dev/null +++ b/jdbc/spring-data-jdbc/src/main/kotlin/tech/ydb/jdbc/simple/User.kt @@ -0,0 +1,53 @@ +package tech.ydb.jdbc.simple + +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Transient +import org.springframework.data.domain.Persistable +import org.springframework.data.relational.core.mapping.Table +import org.springframework.data.util.ProxyUtils +import java.util.concurrent.ThreadLocalRandom + +@Table(name = "Users") +class User : Persistable { + + @Id + var id: Long = ThreadLocalRandom.current().nextLong() + + lateinit var username: String + + lateinit var firstname: String + + lateinit var lastname: String + + @Transient + var new = true + + override fun equals(other: Any?): Boolean { + if (null == other) { + return false + } + + if (this === other) { + return true + } + + if (javaClass != ProxyUtils.getUserClass(other)) { + return false + } + + val that: User = other as User + + return this.id == that.id + } + + override fun hashCode(): Int { + var hashCode = 17 + + hashCode += id.hashCode() * 31 + + return hashCode + } + + override fun getId() = id + override fun isNew() = new +} diff --git a/jdbc/spring-data-jdbc/src/main/kotlin/tech/ydb/jdbc/simple/UserApplication.kt b/jdbc/spring-data-jdbc/src/main/kotlin/tech/ydb/jdbc/simple/UserApplication.kt new file mode 100644 index 0000000..a9679a9 --- /dev/null +++ b/jdbc/spring-data-jdbc/src/main/kotlin/tech/ydb/jdbc/simple/UserApplication.kt @@ -0,0 +1,11 @@ +package tech.ydb.jdbc.simple + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories + +/** + * @author Kirill Kurdyukov + */ +@EnableJdbcRepositories +@SpringBootApplication +class UserApplication \ No newline at end of file diff --git a/jdbc/spring-data-jdbc/src/main/resources/application-ydb.properties b/jdbc/spring-data-jdbc/src/main/resources/application-ydb.properties new file mode 100644 index 0000000..51e27c2 --- /dev/null +++ b/jdbc/spring-data-jdbc/src/main/resources/application-ydb.properties @@ -0,0 +1,4 @@ +spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver +spring.datasource.url=jdbc:ydb:grpc://localhost:2136/local + +logging.level.org.springframework.jdbc.core.JdbcTemplate=debug \ No newline at end of file diff --git a/jdbc/spring-data-jdbc/src/test/kotlin/tech/ydb/jdbc/YdbDockerTest.kt b/jdbc/spring-data-jdbc/src/test/kotlin/tech/ydb/jdbc/YdbDockerTest.kt new file mode 100644 index 0000000..3293d25 --- /dev/null +++ b/jdbc/spring-data-jdbc/src/test/kotlin/tech/ydb/jdbc/YdbDockerTest.kt @@ -0,0 +1,29 @@ +package tech.ydb.jdbc + +import org.junit.jupiter.api.extension.RegisterExtension +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.context.DynamicPropertyRegistry +import org.springframework.test.context.DynamicPropertySource +import tech.ydb.test.junit5.YdbHelperExtension + +/** + * @author Kirill Kurdyukov + */ +@ActiveProfiles("test", "ydb") +abstract class YdbDockerTest { + + companion object { + @JvmField + @RegisterExtension + val ydb = YdbHelperExtension() + + @JvmStatic + @DynamicPropertySource + fun propertySource(registry: DynamicPropertyRegistry) { + registry.add("spring.datasource.url") { + "jdbc:ydb:${if (ydb.useTls()) "grpcs://" else "grpc://"}" + + "${ydb.endpoint()}${ydb.database()}${ydb.authToken()?.let { "?token=$it" } ?: ""}" + } + } + } +} \ No newline at end of file diff --git a/jdbc/spring-data-jdbc/src/test/kotlin/tech/ydb/jdbc/simple/SimpleRepositoryTest.kt b/jdbc/spring-data-jdbc/src/test/kotlin/tech/ydb/jdbc/simple/SimpleRepositoryTest.kt new file mode 100644 index 0000000..13168ee --- /dev/null +++ b/jdbc/spring-data-jdbc/src/test/kotlin/tech/ydb/jdbc/simple/SimpleRepositoryTest.kt @@ -0,0 +1,156 @@ +package tech.ydb.jdbc.simple + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.data.domain.Limit +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort +import org.springframework.transaction.annotation.Transactional +import tech.ydb.jdbc.YdbDockerTest + +/** + * @author Kirill Kurdyukov + */ +@SpringBootTest +@Transactional +class SimpleRepositoryTest : YdbDockerTest() { + + @Autowired + lateinit var repository: SimpleUserRepository + + lateinit var user: User + + @BeforeEach + fun setUp() { + user = User().apply { + username = "foobar" + firstname = "firstname" + lastname = "lastname" + } + } + + @Test + fun findSavedUserById() { + user = repository.save(user) + + assertThat(repository.findById(user.id)).hasValue(user) + } + + @Test + fun findSavedUserByLastname() { + user = repository.save(user) + + assertThat(repository.findByLastname("lastname")).contains(user) + } + + @Test + fun findLimitedNumberOfUsersViaDerivedQuery() { + (0..10).forEach { _ -> repository.save(User().apply { lastname = "lastname" }) } + + assertThat(repository.findByLastname("lastname", Limit.of(5))).hasSize(5) + } + + @Test + fun findLimitedNumberOfUsersViaAnnotatedQuery() { + (0..10).forEach { _ -> repository.save(User().apply { firstname = "firstname" }) } + + assertThat(repository.findByFirstname("firstname", Limit.of(5))).hasSize(5) + } + + @Test + fun findByFirstnameOrLastname() { + user = repository.save(user) + + assertThat(repository.findByFirstnameOrLastname("lastname")).contains(user) + } + + @Test + fun useOptionalAsReturnAndParameterType() { + assertNull(repository.findByUsername("foobar")) + + repository.save(user) + + assertNotNull(repository.findByUsername("foobar")) + } + + @Test + fun useSliceToLoadContent() { + val totalNumberUsers = 11 + val source: MutableList = ArrayList(totalNumberUsers) + + for (i in 1..totalNumberUsers) { + val user = User().apply { + lastname = user.lastname + username = "${user.lastname}-${String.format("%03d", i)}" + } + + source.add(user) + } + + repository.saveAll(source) + + val users = repository.findByLastnameOrderByUsernameAsc(user.lastname, PageRequest.of(1, 5)) + + assertThat(users).containsAll(source.subList(5, 10)) + } + + @Test + fun findFirst2ByOrderByLastnameAsc() { + val user0 = User().apply { lastname = "lastname-0" } + + val user1 = User().apply { lastname = "lastname-1" } + + val user2 = User().apply { lastname = "lastname-2" } + + // we deliberately save the items in reverse + repository.saveAll(listOf(user2, user1, user0)) + + val result = repository.findFirst2ByOrderByLastnameAsc() + + assertThat(result).containsExactly(user0, user1) + } + + @Test + fun findTop2ByWithSort() { + val user0 = User().apply { lastname = "lastname-0" } + + val user1 = User().apply { lastname = "lastname-1" } + + val user2 = User().apply { lastname = "lastname-2" } + + // we deliberately save the items in reverse + repository.saveAll(listOf(user2, user1, user0)) + + val resultAsc = repository.findTop2By(Sort.by(Sort.Direction.ASC, "lastname")) + + assertThat(resultAsc).containsExactly(user0, user1) + + val resultDesc = repository.findTop2By(Sort.by(Sort.Direction.DESC, "lastname")) + + assertThat(resultDesc).containsExactly(user2, user1) + } + + @Test + fun findByFirstnameOrLastnameUsingSpEL() { + val first = User().apply { lastname = "lastname" } + + val second = User().apply { firstname = "firstname" } + + val third = User() + + repository.saveAll(listOf(first, second, third)) + + val reference = User().apply { firstname = "firstname"; lastname = "lastname" } + + val users = repository.findByFirstnameOrLastname(reference) + + assertThat(users).contains(first) + assertThat(users).contains(second) + assertThat(users).hasSize(2) + } +} \ No newline at end of file diff --git a/jdbc/spring-data-jdbc/src/test/resources/application-test.properties b/jdbc/spring-data-jdbc/src/test/resources/application-test.properties new file mode 100644 index 0000000..e69de29 diff --git a/jdbc/spring-data-jdbc/src/test/resources/db/migration/V1__create_table.sql b/jdbc/spring-data-jdbc/src/test/resources/db/migration/V1__create_table.sql new file mode 100644 index 0000000..449db68 --- /dev/null +++ b/jdbc/spring-data-jdbc/src/test/resources/db/migration/V1__create_table.sql @@ -0,0 +1,9 @@ +CREATE TABLE Users +( + id Int64, + username Text, + firstname Text, + lastname Text, + PRIMARY KEY (id), + INDEX username_index GLOBAL ON (username) +) \ No newline at end of file