diff --git a/data-jpa-kotlin/README.adoc b/data-jpa-kotlin/README.adoc new file mode 100644 index 00000000..eb652490 --- /dev/null +++ b/data-jpa-kotlin/README.adoc @@ -0,0 +1 @@ +Tests if Spring Data JPA with works with Kotlin diff --git a/data-jpa-kotlin/build.gradle b/data-jpa-kotlin/build.gradle new file mode 100644 index 00000000..a8fd6e9e --- /dev/null +++ b/data-jpa-kotlin/build.gradle @@ -0,0 +1,25 @@ +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 "1.7.10" +} + +repositories { + mavenLocal() +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + 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") + + aotSmokeTestImplementation(project(":aot-smoke-test-support")) + aotSmokeTestImplementation("org.awaitility:awaitility:4.2.0") +} diff --git a/data-jpa-kotlin/src/aotSmokeTest/java/com/example/data/jpa/DataJpaApplicationAotTests.java b/data-jpa-kotlin/src/aotSmokeTest/java/com/example/data/jpa/DataJpaApplicationAotTests.java new file mode 100644 index 00000000..e2988add --- /dev/null +++ b/data-jpa-kotlin/src/aotSmokeTest/java/com/example/data/jpa/DataJpaApplicationAotTests.java @@ -0,0 +1,66 @@ +package com.example.data.jpa; + +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.AotSmokeTest; + +import static org.assertj.core.api.Assertions.assertThat; + +@AotSmokeTest +class DataJpaApplicationAotTests { + + @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-jpa-kotlin/src/main/java/com/example/data/jpa/kotlin/CLR.java b/data-jpa-kotlin/src/main/java/com/example/data/jpa/kotlin/CLR.java new file mode 100644 index 00000000..58ab5008 --- /dev/null +++ b/data-jpa-kotlin/src/main/java/com/example/data/jpa/kotlin/CLR.java @@ -0,0 +1,85 @@ +package com.example.data.jpa.kotlin; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import com.example.data.jpa.kotlin.model.Author; +import com.example.data.jpa.kotlin.model.Book; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +class CLR implements CommandLineRunner { + + private final AuthorRepository authorRepository; + + CLR(AuthorRepository authorRepository) { + this.authorRepository = authorRepository; + } + + @Override + @Transactional + public void run(String... args) { + var authors = insertAuthors(); + listAllAuthors(); + findById(authors); + findByPartialName(); + queryFindByName(); + deleteAll(); + } + + private void deleteAll() { + this.authorRepository.deleteAll(); + long count = this.authorRepository.count(); + System.out.printf("deleteAll(): count = %d%n", count); + } + + private void queryFindByName() { + Author author1 = this.authorRepository.queryFindByName("Josh Long"); + Author author2 = this.authorRepository.queryFindByName("Martin Kleppmann"); + + System.out.printf("queryFindByName(): author1 = %s%n", author1); + System.out.printf("queryFindByName(): author2 = %s%n", author2); + } + + private void findByPartialName() { + Author author1 = this.authorRepository.findByNameContainingIgnoreCase("sh lo"); + Author author2 = this.authorRepository.findByNameContainingIgnoreCase("in kl"); + + System.out.printf("findByPartialName(): author1 = %s%n", author1); + System.out.printf("findByPartialName(): author2 = %s%n", author2); + } + + private void findById(List authors) { + Author author1 = this.authorRepository.findById(authors.get(0).getId()).orElse(null); + Author author2 = this.authorRepository.findById(authors.get(1).getId()).orElse(null); + + System.out.printf("findById(): author1 = %s%n", author1); + System.out.printf("findById(): author2 = %s%n", author2); + } + + private void listAllAuthors() { + List authors = this.authorRepository.findAll(); + for (Author author : authors) { + System.out.printf("listAllAuthors(): author = %s%n", author); + for (Book book : author.getBooks()) { + System.out.printf("\t%s%n", book); + } + } + } + + private List insertAuthors() { + Author author1 = this.authorRepository.save(new Author(null, "Josh Long", + Set.of(new Book(null, "Reactive Spring"), new Book(null, "Cloud Native Java")))); + Author author2 = this.authorRepository.save( + new Author(null, "Martin Kleppmann", Set.of(new Book(null, "Designing Data Intensive Applications")))); + + System.out.printf("insertAuthors(): author1 = %s%n", author1); + System.out.printf("insertAuthors(): author2 = %s%n", author2); + + return Arrays.asList(author1, author2); + } + +} diff --git a/data-jpa-kotlin/src/main/java/com/example/data/jpa/kotlin/DataJpaConfiguration.java b/data-jpa-kotlin/src/main/java/com/example/data/jpa/kotlin/DataJpaConfiguration.java new file mode 100644 index 00000000..a3cf404e --- /dev/null +++ b/data-jpa-kotlin/src/main/java/com/example/data/jpa/kotlin/DataJpaConfiguration.java @@ -0,0 +1,10 @@ +package com.example.data.jpa.kotlin; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@Configuration +@EnableJpaRepositories +class DataJpaConfiguration { + +} diff --git a/data-jpa-kotlin/src/main/java/com/example/data/jpa/kotlin/DataJpaKotlinApplication.java b/data-jpa-kotlin/src/main/java/com/example/data/jpa/kotlin/DataJpaKotlinApplication.java new file mode 100644 index 00000000..492ed5af --- /dev/null +++ b/data-jpa-kotlin/src/main/java/com/example/data/jpa/kotlin/DataJpaKotlinApplication.java @@ -0,0 +1,14 @@ +package com.example.data.jpa.kotlin; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DataJpaKotlinApplication { + + public static void main(String[] args) throws InterruptedException { + SpringApplication.run(DataJpaKotlinApplication.class, args); + Thread.currentThread().join(); // To be able to measure memory consumption + } + +} diff --git a/data-jpa-kotlin/src/main/kotlin/com/example/data/jpa/kotlin/AutorRepository.kt b/data-jpa-kotlin/src/main/kotlin/com/example/data/jpa/kotlin/AutorRepository.kt new file mode 100644 index 00000000..4d3f4175 --- /dev/null +++ b/data-jpa-kotlin/src/main/kotlin/com/example/data/jpa/kotlin/AutorRepository.kt @@ -0,0 +1,14 @@ +package com.example.data.jpa.kotlin + +import com.example.data.jpa.kotlin.model.Author +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import java.util.* + +interface AuthorRepository : JpaRepository { + + fun findByNameContainingIgnoreCase(partialName: String): Author? + + @Query("SELECT a FROM Author a WHERE a.name = :name") + fun queryFindByName(name: String): Author? +} diff --git a/data-jpa-kotlin/src/main/kotlin/com/example/data/jpa/kotlin/model/Autor.kt b/data-jpa-kotlin/src/main/kotlin/com/example/data/jpa/kotlin/model/Autor.kt new file mode 100644 index 00000000..4c7d7ed8 --- /dev/null +++ b/data-jpa-kotlin/src/main/kotlin/com/example/data/jpa/kotlin/model/Autor.kt @@ -0,0 +1,39 @@ +package com.example.data.jpa.kotlin.model + +import jakarta.persistence.* +import java.util.* + +@Entity +class Author(@Id @GeneratedValue var id: Long?, var name: String?) { + + @OneToMany(cascade = [CascadeType.ALL]) + var books: Set? = null + + constructor(id: Long?, name: String?, books: Set?) : this(id, name) { + this.books = books + } + + @PostPersist + fun postPersist() { + println("Persisted Author $id") + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null) { + return false + } + val author = other as Author + return id == author.id && name == author.name && books == author.books + } + + override fun hashCode(): Int { + return Objects.hash(id, name, books) + } + + override fun toString(): String { + return "Author{name='$name'}" + } +} diff --git a/data-jpa-kotlin/src/main/kotlin/com/example/data/jpa/kotlin/model/Book.kt b/data-jpa-kotlin/src/main/kotlin/com/example/data/jpa/kotlin/model/Book.kt new file mode 100644 index 00000000..11d81381 --- /dev/null +++ b/data-jpa-kotlin/src/main/kotlin/com/example/data/jpa/kotlin/model/Book.kt @@ -0,0 +1,29 @@ +package com.example.data.jpa.kotlin.model + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.Id +import java.util.* + +@Entity +class Book(@Id @GeneratedValue var id: Long?, var title: String?) { + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null) { + return false + } + val book = other as Book + return id == book.id && title == book.title + } + + override fun hashCode(): Int { + return Objects.hash(id, title) + } + + override fun toString(): String { + return "Book{title='$title'}" + } +} diff --git a/data-jpa-kotlin/src/main/resources/application.properties b/data-jpa-kotlin/src/main/resources/application.properties new file mode 100644 index 00000000..e69de29b diff --git a/data-jpa-kotlin/src/main/resources/schema.sql b/data-jpa-kotlin/src/main/resources/schema.sql new file mode 100644 index 00000000..14e51639 --- /dev/null +++ b/data-jpa-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-jpa-kotlin/src/test/java/com/example/data/jpa/kotlin/DataJpaKotlinApplicationTests.java b/data-jpa-kotlin/src/test/java/com/example/data/jpa/kotlin/DataJpaKotlinApplicationTests.java new file mode 100644 index 00000000..a577e3a9 --- /dev/null +++ b/data-jpa-kotlin/src/test/java/com/example/data/jpa/kotlin/DataJpaKotlinApplicationTests.java @@ -0,0 +1,14 @@ +package com.example.data.jpa.kotlin; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DataJpaKotlinApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/settings.gradle b/settings.gradle index e7538909..b9fe58a3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -59,6 +59,7 @@ include "data-cassandra-reactive" include "data-jdbc-h2" include "data-jdbc-postgresql" include "data-jpa" +include "data-jpa-kotlin" include "data-mongodb" include "data-mongodb-reactive" include "data-r2dbc"