diff --git a/STATUS.adoc b/STATUS.adoc index 8f0b5d72..3814bd64 100644 --- a/STATUS.adoc +++ b/STATUS.adoc @@ -260,6 +260,12 @@ h|nativeTest |image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-test] |image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-native-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-native-test] +|data-jdbc-h2-kotlin +|image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-kotlin-app-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-kotlin-app-test] +|image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-kotlin-native-app-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-kotlin-native-app-test] +|image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-kotlin-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-kotlin-test] +|image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-kotlin-native-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-h2-kotlin-native-test] + |data-jdbc-postgresql |image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-postgresql-app-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-postgresql-app-test] |image:https://ci.spring.io/api/v1/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-postgresql-native-app-test/badge[link=https://ci.spring.io/teams/spring-aot-smoke-tests/pipelines/spring-aot-smoke-tests-1.0.x/jobs/data-jdbc-postgresql-native-app-test] diff --git a/ci/smoke-tests.yml b/ci/smoke-tests.yml index 35b355d9..05207f90 100644 --- a/ci/smoke-tests.yml +++ b/ci/smoke-tests.yml @@ -124,6 +124,9 @@ groups: - name: data-jdbc-h2 app_test: true test: true + - name: data-jdbc-h2-kotlin + app_test: true + test: true - name: data-jdbc-postgresql app_test: true test: false diff --git a/data/data-jdbc-h2-kotlin/README.adoc b/data/data-jdbc-h2-kotlin/README.adoc new file mode 100644 index 00000000..6b57ceb0 --- /dev/null +++ b/data/data-jdbc-h2-kotlin/README.adoc @@ -0,0 +1 @@ +Tests if Spring Data JDBC with the H2 database using Kotlin works diff --git a/data/data-jdbc-h2-kotlin/build.gradle b/data/data-jdbc-h2-kotlin/build.gradle new file mode 100644 index 00000000..eb7fa0f2 --- /dev/null +++ b/data/data-jdbc-h2-kotlin/build.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' + id 'org.springframework.boot' + id 'org.springframework.aot.smoke-test' + id 'org.graalvm.buildtools.native' + id 'org.jetbrains.kotlin.jvm' + id 'org.jetbrains.kotlin.plugin.spring' version "$kotlinVersion" +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-data-jdbc") + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + runtimeOnly("com.h2database:h2") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + + appTestImplementation(project(":aot-smoke-test-support")) + appTestImplementation("org.awaitility:awaitility:4.2.0") +} diff --git a/data/data-jdbc-h2-kotlin/src/appTest/java/com/example/data/jdbc/h2/DataJdbcH2ApplicationAotTests.java b/data/data-jdbc-h2-kotlin/src/appTest/java/com/example/data/jdbc/h2/DataJdbcH2ApplicationAotTests.java new file mode 100644 index 00000000..b2dc6536 --- /dev/null +++ b/data/data-jdbc-h2-kotlin/src/appTest/java/com/example/data/jdbc/h2/DataJdbcH2ApplicationAotTests.java @@ -0,0 +1,66 @@ +package com.example.data.jdbc.h2; + +import java.time.Duration; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import org.springframework.aot.smoketest.support.assertj.AssertableOutput; +import org.springframework.aot.smoketest.support.junit.ApplicationTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@ApplicationTest +class DataJdbcH2ApplicationAotTests { + + @Test + void insert(AssertableOutput output) { + Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> { + assertThat(output).hasSingleLineContaining("insertAuthors(): author1 = Author(name='Josh Long')") + .hasSingleLineContaining("insertAuthors(): author2 = Author(name='Martin Kleppmann')"); + }); + } + + @Test + void listAllAuthors(AssertableOutput output) { + Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> { + assertThat(output).hasSingleLineContaining("listAllAuthors(): author = Author(name='Josh Long')") + .hasSingleLineContaining("Book(title='Cloud Native Java')") + .hasSingleLineContaining("Book(title='Reactive Spring')") + .hasSingleLineContaining("listAllAuthors(): author = Author(name='Martin Kleppmann')") + .hasSingleLineContaining("Book(title='Designing Data Intensive Applications')"); + }); + } + + @Test + void findById(AssertableOutput output) { + Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> { + assertThat(output).hasSingleLineContaining("findById(): author1 = Author(name='Josh Long')") + .hasSingleLineContaining("findById(): author2 = Author(name='Martin Kleppmann')"); + }); + } + + @Test + void queryDerivedFromMethodName(AssertableOutput output) { + Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> { + assertThat(output).hasSingleLineContaining("findByPartialName(): author1 = Author(name='Josh Long')") + .hasSingleLineContaining("findByPartialName(): author2 = Author(name='Martin Kleppmann')"); + }); + } + + @Test + void queryAnnotatedMethod(AssertableOutput output) { + Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> { + assertThat(output).hasSingleLineContaining("queryFindByName(): author1 = Author(name='Josh Long')") + .hasSingleLineContaining("queryFindByName(): author2 = Author(name='Martin Kleppmann')"); + }); + } + + @Test + void deleteAll(AssertableOutput output) { + Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted(() -> { + assertThat(output).hasSingleLineContaining("deleteAll(): count = 0"); + }); + } + +} diff --git a/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/AuthorRepository.kt b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/AuthorRepository.kt new file mode 100644 index 00000000..45f308be --- /dev/null +++ b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/AuthorRepository.kt @@ -0,0 +1,13 @@ +package com.example.data.jdbc.h2.kotlin + +import com.example.data.jdbc.h2.kotlin.model.Author +import org.springframework.data.jdbc.repository.query.Query +import org.springframework.data.repository.ListCrudRepository +import java.util.* + +interface AuthorRepository : ListCrudRepository { + fun findByNameContainingIgnoreCase(partialName: String): Author? + + @Query("SELECT * FROM author a WHERE a.name = :name LIMIT 1") + fun queryFindByName(name: String): Author? +} diff --git a/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/CLR.kt b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/CLR.kt new file mode 100644 index 00000000..3a78400a --- /dev/null +++ b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/CLR.kt @@ -0,0 +1,75 @@ +package com.example.data.jdbc.h2.kotlin + +import com.example.data.jdbc.h2.kotlin.model.Author +import com.example.data.jdbc.h2.kotlin.model.Book +import org.springframework.boot.CommandLineRunner +import org.springframework.stereotype.Component + +@Component +class CLR(private val authorRepository: AuthorRepository) : CommandLineRunner { + override fun run(vararg args: String) { + insertAuthors() + listAllAuthors() + findById() + findByPartialName() + queryFindByName() + deleteAll() + } + + private fun deleteAll() { + authorRepository.deleteAll() + val count = authorRepository.count() + System.out.printf("deleteAll(): count = %d%n", count) + } + + private fun queryFindByName() { + val author1 = authorRepository.queryFindByName("Josh Long") + val author2 = authorRepository.queryFindByName("Martin Kleppmann") + System.out.printf("queryFindByName(): author1 = %s%n", author1) + System.out.printf("queryFindByName(): author2 = %s%n", author2) + } + + private fun findByPartialName() { + val author1 = + authorRepository.findByNameContainingIgnoreCase("sh lo") + val author2 = + authorRepository.findByNameContainingIgnoreCase("in kl") + System.out.printf("findByPartialName(): author1 = %s%n", author1) + System.out.printf("findByPartialName(): author2 = %s%n", author2) + } + + private fun findById() { + val author1 = authorRepository.findById(1L).orElse(null) + val author2 = authorRepository.findById(2L).orElse(null) + System.out.printf("findById(): author1 = %s%n", author1) + System.out.printf("findById(): author2 = %s%n", author2) + } + + private fun listAllAuthors() { + val authors = authorRepository.findAll() + for (author in authors) { + System.out.printf("listAllAuthors(): author = %s%n", author) + for (book in author.books) { + System.out.printf("\t%s%n", book) + } + } + } + + private fun insertAuthors() { + val author1 = authorRepository.save( + Author( + null, "Josh Long", + setOf(Book(null, "Reactive Spring"), Book(null, "Cloud Native Java")) + ) + ) + val author2 = authorRepository.save( + Author( + null, + "Martin Kleppmann", + setOf(Book(null, "Designing Data Intensive Applications")) + ) + ) + System.out.printf("insertAuthors(): author1 = %s%n", author1) + System.out.printf("insertAuthors(): author2 = %s%n", author2) + } +} diff --git a/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/DataJdbcConfiguration.kt b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/DataJdbcConfiguration.kt new file mode 100644 index 00000000..c461df82 --- /dev/null +++ b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/DataJdbcConfiguration.kt @@ -0,0 +1,8 @@ +package com.example.data.jdbc.h2.kotlin + +import org.springframework.context.annotation.Configuration +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories + +@Configuration +@EnableJdbcRepositories +class DataJdbcConfiguration diff --git a/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/DataJdbcH2Application.kt b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/DataJdbcH2Application.kt new file mode 100644 index 00000000..27b11a9e --- /dev/null +++ b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/DataJdbcH2Application.kt @@ -0,0 +1,11 @@ +package com.example.data.jdbc.h2.kotlin + +import org.springframework.boot.SpringApplication +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +class DataJdbcH2Application + +fun main(args: Array) { + SpringApplication.run(DataJdbcH2Application::class.java, *args) +} diff --git a/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/model/Author.kt b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/model/Author.kt new file mode 100644 index 00000000..0b1dbb9b --- /dev/null +++ b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/model/Author.kt @@ -0,0 +1,13 @@ +package com.example.data.jdbc.h2.kotlin.model + +import org.springframework.data.annotation.Id + +data class Author( + @Id val id: Long?, + val name: String, + val books: Set +) { + override fun toString(): String { + return "Author(name='$name')" + } +} diff --git a/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/model/Book.kt b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/model/Book.kt new file mode 100644 index 00000000..81f09b69 --- /dev/null +++ b/data/data-jdbc-h2-kotlin/src/main/kotlin/com/example/data/jdbc/h2/kotlin/model/Book.kt @@ -0,0 +1,12 @@ +package com.example.data.jdbc.h2.kotlin.model + +import org.springframework.data.annotation.Id + +data class Book( + @Id val id: Long?, + val title: String +) { + override fun toString(): String { + return "Book(title='$title')" + } +} diff --git a/data/data-jdbc-h2-kotlin/src/main/resources/application.properties b/data/data-jdbc-h2-kotlin/src/main/resources/application.properties new file mode 100644 index 00000000..e69de29b diff --git a/data/data-jdbc-h2-kotlin/src/main/resources/schema.sql b/data/data-jdbc-h2-kotlin/src/main/resources/schema.sql new file mode 100644 index 00000000..14e51639 --- /dev/null +++ b/data/data-jdbc-h2-kotlin/src/main/resources/schema.sql @@ -0,0 +1,12 @@ +create table author +( + id bigint auto_increment primary key, + name varchar not null +); + +create table book +( + id bigint auto_increment primary key, + author bigint not null references author (id), + title varchar not null +); diff --git a/data/data-jdbc-h2-kotlin/src/test/kotlin/com/example/data/jdbc/h2/kotlin/DataJdbcTests.kt b/data/data-jdbc-h2-kotlin/src/test/kotlin/com/example/data/jdbc/h2/kotlin/DataJdbcTests.kt new file mode 100644 index 00000000..925ec51f --- /dev/null +++ b/data/data-jdbc-h2-kotlin/src/test/kotlin/com/example/data/jdbc/h2/kotlin/DataJdbcTests.kt @@ -0,0 +1,39 @@ +package com.example.data.jdbc.h2.kotlin + +import com.example.data.jdbc.h2.kotlin.model.Author +import com.example.data.jdbc.h2.kotlin.model.Book +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest +import org.springframework.jdbc.core.JdbcTemplate +import java.util.Set + +@DataJdbcTest +class DataJdbcTests { + @Autowired + private lateinit var jdbcTemplate: JdbcTemplate + + @Autowired + private lateinit var authorRepository: AuthorRepository + + @Test + fun shouldHaveSchema() { + Assertions.assertThatCode { jdbcTemplate.execute("SELECT * FROM author") } + .doesNotThrowAnyException() + } + + @Test + fun shouldSaveAndLoad() { + val book1 = Book(null, "book-1") + val author1 = Author(null, "author-1", Set.of(book1)) + authorRepository.save(author1) + val found = authorRepository.queryFindByName("author-1") + assertThat(found).isNotNull() + checkNotNull(found) + assertThat(found.id).isNotNull() + assertThat(found.name).isEqualTo("author-1") + assertThat(found.books).hasSize(1) + } +}