+
+ /**
+ * 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