GithubHelp home page GithubHelp logo

krush's Introduction

Krush

Maven Central CircleCI Sputnik

Krush is a lightweight persistence layer for Kotlin based on Exposed SQL DSL. It’s similar to Requery and Micronaut-data jdbc, but designed to work idiomatically with Kotlin and immutable data classes.

It’s based on a compile-time JPA annotation processor that generates Exposed DSL table and objects mappings for you. This lets you instantly start writing type-safe SQL queries without need to write boilerplate infrastructure code.

Rationale

  • (type-safe) SQL-first - use type-safe SQL-like DSL in your queries, no string or method name parsing
  • Minimal changes to your domain model - no need to extend external interfaces and used special types - just add annotations to your existing domain model
  • Explicit fetching - you specify explicitly in query what data you want to fetch, no additional fetching after data is loaded
  • No runtime magic - no proxies, lazy loading, just data classes containing data fetched from DB
  • Pragmatic - easy to start, but powerful even in not trivial cases (associations, grouping queries)

Example

Given a simple Book class:

data class Book(
   val id: Long? = null,
   val isbn: String,
   val title: String,
   val author: String,
   val publishDate: LocalDate
)

we can turn it into Krush entity by adding @Entity and @Id annotations:

@Entity
data class Book(
   @Id @GeneratedValue
   val id: Long? = null,
   val isbn: String,
   val title: String,
   val author: String,
   val publishDate: LocalDate
)

When we build the project we’ll have BookTable mapping generated for us. So we can persist the Book:

val book = Book(
   isbn = "1449373321", publishDate = LocalDate.of(2017, Month.APRIL, 11),
   title = "Designing Data-Intensive Applications", author = "Martin Kleppmann"
)

// insert method is generated by Krush
val persistedBook = BookTable.insert(book)
assertThat(persistedBook.id).isNotNull()

So we have now a Book persisted in DB with autogenerated Book.id field. And now we can use type-safe SQL DSL to query the BookTable:

val bookId = book.id ?: throw IllegalArgumentException()

// toBook method is generated by Krush
val fetchedBook = BookTable.select { BookTable.id eq bookId }.singleOrNull()?.toBook()
assertThat(fetchedBook).isEqualTo(book)

// toBookList method is generated by Krush
val selectedBooks = (BookTable)
   .select { BookTable.author like "Martin K%" }
   .toBookList()

assertThat(selectedBooks).containsOnly(persistedBook)

Installation

Gradle Groovy:

repositories {
    mavenCentral()
}

apply plugin: 'kotlin-kapt'

dependencies {
    api "pl.touk.krush:krush-annotation-processor:$krushVersion"
    kapt "pl.touk.krush:krush-annotation-processor:$krushVersion"
    api "pl.touk.krush:krush-runtime:$krushVersion" 
}

Gradle Kotlin:

repositories {
    mavenCentral()
}

plugins {
    kotlin("kapt") version "$kotlinVersion"
}

dependencies {
    api("pl.touk.krush:krush-annotation-processor:$krushVersion")
    kapt("pl.touk.krush:krush-annotation-processor:$krushVersion")
    api("pl.touk.krush:krush-runtime:$krushVersion")
}

Maven:

<dependencies>
    <dependency>
        <groupId>pl.touk.krush</groupId>
        <artifactId>krush-runtime</artifactId>
        <version>${krush.version}</version>
    </dependency>
</dependencies>

...

<plugin>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-maven-plugin</artifactId>
    <executions>
        <execution>
            <id>kapt</id>
            <goals>
                <goal>kapt</goal>
            </goals>
            <configuration>
                ...
                <annotationProcessorPaths>
                    <annotationProcessorPath>
                        <groupId>pl.touk.krush</groupId>
                        <artifactId>krush-annotation-processor</artifactId>
                        <version>${krush.version}</version>
                    </annotationProcessorPath>
                </annotationProcessorPaths>
            </configuration>
        </execution>
        ...
    </executions>
</plugin>

Dependencies

Features

  • generates table mappings and functions for mapping from/to data classes
  • type-safe SQL DSL without reading schema from existing database (code-first)
  • explicit association fetching (via leftJoin / innerJoin)
  • multiple data types support, including type aliases
  • custom data type support (with @Converter), also for wrapped auto-generated ids
  • you can still persist associations not directly reflected in domain model (eq. article favorites)

However, Krush is not a full-blown ORM library. This means following JPA features are not supported:

  • lazy association fetching
  • dirty checking
  • caching
  • versioning / optimistic locking

Updating

Given following entity:

@Entity
data class Reservation(
    @Id
    val uid: UUID = UUID.randomUUID(),

    @Enumerated(EnumType.STRING)
    val status: Status = Status.FREE,

    val reservedAt: LocalDateTime? = null,
    val freedAt: LocalDateTime? = null
) {
    fun reserve() = copy(status = Status.RESERVED, reservedAt = LocalDateTime.now())
    fun free() = copy(status = Status.FREE, freedAt = LocalDateTime.now())
}

enum class Status { FREE, RESERVED }

you can call Exposed update with generated from metod to overwrite it's data:

val reservation = Reservation().reserve().let(ReservationTable::insert)

val freedReservation = reservation.free()
ReservationTable.update({ ReservationTable.uid eq reservation.uid }) { it.from(freedReservation) }

val updatedReservation = ReservationTable.select({ ReservationTable.uid eq reservation.uid }).singleOrNull()?.toReservation()
assertThat(updatedReservation?.status).isEqualTo(Status.FREE)
assertThat(updatedReservation?.reservedAt).isEqualTo(reservation.reservedAt)
assertThat(updatedReservation?.freedAt).isEqualTo(freedReservation.freedAt)

For simple cases you can still use Exposed native update syntax:

val freedAt = LocalDateTime.now()
ReservationTable.update({ ReservationTable.uid eq reservation.uid }) {
  it[ReservationTable.status] = Status.FREE
  it[ReservationTable.freedAt] = freedAt
}

Other Exposed features are supported as well, like, replace:

val reservation = Reservation().reserve()

ReservationTable.replace { it.from(reservation) }
val freedReservation = reservation.free()
ReservationTable.replace { it.from(freedReservation) }

val allReservations = ReservationTable.selectAll().toReservationList()
assertThat(allReservations).containsExactly(freedReservation)

and batchInsert/batchReplace:

val reservation1 = Reservation().reserve()
val reservation2 = Reservation().reserve()

ReservationTable.batchInsert(
    listOf(reservation1, reservation2), body = { this.from(it) }
)
val allReservations = ReservationTable.selectAll().toReservationList()
assertThat(allReservations)
    .containsExactly(reservation1, reservation2)
}

Complete example

Associations

@Entity
@Table(name = "articles")
data class Article(
    @Id @GeneratedValue
    val id: Long? = null,

    @Column(name = "title")
    val title: String,

    @ManyToMany
    @JoinTable(name = "article_tags")
    val tags: List<Tag> = emptyList()
)

@Entity
@Table(name = "tags")
data class Tag(
    @Id @GeneratedValue
    val id: Long? = null,

    @Column(name = "name")
    val name: String
)

Persisting

val tag1 = Tag(name = "jvm")
val tag2 = Tag(name = "spring")

val tags = listOf(tag1, tag2).map(TagTable::insert)
val article = Article(title = "Spring for dummies", tags = tags)
val persistedArticle = ArticleTable.insert(article)

Querying and fetching

val (selectedArticle) = (ArticleTable leftJoin ArticleTagsTable leftJoin TagTable)
    .select { TagTable.name inList listOf("jvm", "spring") }
    .toArticleList()

assertThat(selectedArticle).isEqualTo(persistedArticle)

Update logic for associations not implemented (yet!) - you have to manually add/remove records from ArticleTagsTable.

Custom column wrappers

Krush exposes some helpful wrappers for user classes to easily convert them to specific columns in database, e.g.

@JvmInline
value class MyStringId(val raw: String)

@JvmInline
value class MyUUID(val raw: UUID)

@JvmInline
value class MyVersion(val raw: Int)

enum class MyState { ACTIVE, INACTIVE }

fun Table.myStringId(name: String) = stringWrapper(name, ::MyStringId) { it.raw }

fun Table.myUUID(name: String) = uuidWrapper(name, ::MyUUID) { it.raw }

fun Table.myVersion(name: String) = integerWrapper(name, ::MyVersion) { it.raw }

fun Table.myState(name: String) = booleanWrapper(name, { if (it) MyState.ACTIVE else MyState.INACTIVE }) {
    when (it) {
        MyState.ACTIVE -> true
        MyState.INACTIVE -> false
    }
}

object MyTable : Table("test") {
    val id = myStringId("my_id").nullable()
    val uuid = myUUID("my_uuid").nullable()
    val version = myVersion("my_version").nullable()
    val state = myState("my_state").nullable()
}

Support for Postgresql distinct on (...)

Postgresql allows usage of nonstandard clause DISTINCT ON in queries.

Krush provides custom distinctOn extension method which can be used as first parameter in custom slice extension method.

Postgresql specific extensions needs krush-runtime-postgresql dependency in maven or gradle

Example code:

@JvmInline
value class MyStringId(val raw: String)

@JvmInline
value class MyVersion(val raw: Int)

fun Table.myStringId(name: String) = stringWrapper(name, ::MyStringId) { it.raw }

fun Table.myVersion(name: String) = integerWrapper(name, ::MyVersion) { it.raw }


object MyTable : Table("test") {
    val id = myStringId("my_id").nullable()
    val version = myVersion("my_version").nullable()
    val content = jsonb("content").nullable()
}

fun findNewestContentVersion(id: MyStringId): String? =
    MyTable
        .slice(MyTable.id.distinctOn(), MyTable.content)
        .select { MyTable.id eq id }
        .orderBy(MyTable.id to SortOrder.ASC, MyTable.version to SortOrder.DESC)
        .map { it[MyTable.content] }
        .firstOrNull()

when findNewestContentVersion(MyStringId("123")) is called will generate SQL:

SELECT DISTINCT ON (test.my_id) TRUE, test.my_id, test."content"
FROM test
WHERE test.my_id = '123'
ORDER BY test.my_id ASC, test.my_version DESC

Example projects

Contributors

Special thanks to Łukasz Jędrzejewski for original idea of using Exposed in our projects.

Licence

Krush is published under Apache License 2.0.

krush's People

Contributors

christiansch avatar dependabot[bot] avatar emh333 avatar hlipinski avatar nickgieschen avatar pjagielski avatar sledzmateusz avatar tblakers avatar volltage avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

krush's Issues

Support java.util.Instant

Exposed added support late last year, and it's available in 24.1.

In addition to supporting Instant as a column type, it should also be possible to use Instant as the target type in an attribute converter, so that kotlinx.time.instant can be used directly on the Entity.

build task fails when kotlin version bumped to 1.9.10

Following tests are failing

DFSTest. shouldVisitAllNodesUsingDFS()
EntityGraphBuilderTest. shouldHandleColumnNaming()
EntityGraphBuilderTest. shouldHandleDatePropertyTypes()
EntityGraphBuilderTest. shouldHandleEmbeddedPropertyTypes()
EntityGraphBuilderTest. shouldHandleEnumPropertyTypes()
EntityGraphBuilderTest. shouldHandleNullableFields()
EntityGraphBuilderTest. shouldHandleNumericPropertyTypes()
EntityGraphBuilderTest. shouldHandleOneToOneMapping()
EntityGraphBuilderTest. shouldHandleTypeAliases()
EntityGraphBuilderTest. shouldPutEntityToGraph()
ModelValidatorTest. shouldNotPassValidationWhenEntityIsNotDataClass()

This is due to the kotlinpoet v 1.12.0 incompatibility with kotlin 1.9.10.
The fix: bump kotlin poet to v 1.14.2
The common root cause:

pl.touk.krush.validation.EntityGraphValidationFailedException: Entity Graph validation failed. Errors:
ValidationErrorMessage(text=Validation exception: java.lang.IllegalStateException: Could not parse metadata! Try bumping kotlinpoet and/or kotlinx-metadata version., element: pl.touk.example.OneToOneTargetEntity, validator: pl.touk.krush.model.DataClassValidator@5f0fd5a0)
ValidationErrorMessage(text=Validation exception: java.lang.IllegalStateException: Could not parse metadata! Try bumping kotlinpoet and/or kotlinx-metadata version., element: pl.touk.example.OneToOneSourceEntity, validator: pl.touk.krush.model.DataClassValidator@5f0fd5a0)
at app//pl.touk.krush.model.EntityGraphBuilder.build(EntityGraphBuilder.kt:27)
at app//pl.touk.krush.model.DFSTest.shouldVisitAllNodesUsingDFS(DFSTest.kt:19)
at [email protected]/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at [email protected]/java.lang.reflect.Method.invoke(Method.java:578)
at app//org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:727)
at app//org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at app//org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)
at app//org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)
at app//org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)
at app//org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)
at app//org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)
at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at app//org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at app//org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)
at app//org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)
at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:217)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:213)
at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:138)
at app//org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:68)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at [email protected]/java.util.ArrayList.forEach(ArrayList.java:1511)
at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at [email protected]/java.util.ArrayList.forEach(ArrayList.java:1511)
at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
at app//org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
at app//org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
at app//org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
at app//org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
at app//org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at app//org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.processAllTestClasses(JUnitPlatformTestClassProcessor.java:99)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor$CollectAllTestClassesExecutor.access$000(JUnitPlatformTestClassProcessor.java:79)
at org.gradle.api.internal.tasks.testing.junitplatform.JUnitPlatformTestClassProcessor.stop(JUnitPlatformTestClassProcessor.java:75)
at org.gradle.api.internal.tasks.testing.SuiteTestClassProcessor.stop(SuiteTestClassProcessor.java:62)
at [email protected]/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
at [email protected]/java.lang.reflect.Method.invoke(Method.java:578)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:36)
at org.gradle.internal.dispatch.ReflectionDispatch.dispatch(ReflectionDispatch.java:24)
at org.gradle.internal.dispatch.ContextClassLoaderDispatch.dispatch(ContextClassLoaderDispatch.java:33)
at org.gradle.internal.dispatch.ProxyDispatchAdapter$DispatchingInvocationHandler.invoke(ProxyDispatchAdapter.java:94)
at jdk.proxy1/jdk.proxy1.$Proxy2.stop(Unknown Source)
at org.gradle.api.internal.tasks.testing.worker.TestWorker$3.run(TestWorker.java:193)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.executeAndMaintainThreadName(TestWorker.java:129)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:100)
at org.gradle.api.internal.tasks.testing.worker.TestWorker.execute(TestWorker.java:60)
at org.gradle.process.internal.worker.child.ActionExecutionWorker.execute(ActionExecutionWorker.java:56)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:113)
at org.gradle.process.internal.worker.child.SystemApplicationClassLoaderWorker.call(SystemApplicationClassLoaderWorker.java:65)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.run(GradleWorkerMain.java:69)
at app//worker.org.gradle.process.internal.worker.GradleWorkerMain.main(GradleWorkerMain.java:74)

Support @EmbeddedId

Example:

@Embeddable
data class RecordId(
    @Column(name = "ID")
    val id: String,

    @Column(name = "TYPE")
    val type: String
)

@Entity
data class Record(

    @EmbeddedId
    val id: RecordId,

    val timestamp: LocalDateTime

)

Update to Exposed 0.32.1

Hi guys,

Currently our test suite is failing due to JetBrains/Exposed#1028, which was solved in v0.32.1 (which introduced kotlin 1.5.10). Updating to the most current Exposed version would be highly appreciated.

@Column not processed as expected regarding updatable

Hi guys,

Using the aforementioned annotation (as you do in the examples) with @Column(updatable=false) doesn't do anything.

@Entity
data class Customer(
        @Id
        val uuid: UUID = UUID.randomUUID(),

        val email: String,

        @Column(updatable = false)
        val name: String,

        @Embedded
        var address: CustomerAddress = CustomerAddress(),

        var groupId: String?
)

generates the following mapping (mapping.kt):

fun UpdateBuilder<Any>.from(customer: Customer) {
  	this[CustomerTable.uuid] = customer.uuid
  	this[CustomerTable.email] = customer.email
  	this[CustomerTable.name] = customer.name
  	this[CustomerTable.groupId] = customer.groupId
  	this[CustomerTable.addressCity] = customer.address.city
  	this[CustomerTable.addressCountry] = customer.address.country
  	this[CustomerTable.addressLine1] = customer.address.line1
  	this[CustomerTable.addressLine2] = customer.address.line2
  	this[CustomerTable.addressPostalCode] = customer.address.postalCode
  	this[CustomerTable.addressState] = customer.address.state
}

I expect the uuid and the name to not be included in this generated update mapping.

So it would be nice to either have documentation on what you actually process (found it here indirectly though). I feel like it's weird behaviour btw that I can just update an id after creation. My question is: can I somehow prevent updating with krush? If not, would it be something you'd consider implementing?

Column Convert cause compile error

My entity column

@Convert(converter = StringListConverter::class)
    var permissions: MutableList<String> = mutableListOf()

generated code is

fun Table.role_permissions(columnName: String): Column<MutableList> =
    stringWrapper<MutableList>(columnName, {
    pl.touk.krush.StringListConverter().convertToEntityAttribute(it) }, {
    pl.touk.krush.StringListConverter().convertToDatabaseColumn(it) })

tables.kt: (74, 56): One type argument expected for interface MutableList

Multiplatform support

Any chance of making this project support multiplatform apps, where model classes are declared inside a common module?

@JoinColumn name ignored when using @ManyToOne

Taking this test case as an example with these entities, the generated sql looks something like this:

CREATE TABLE IF NOT EXISTS tree (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(255) NOT NULL);

CREATE TABLE IF NOT EXISTS branch (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(255) NOT NULL,
    treeId_id BIGINT NULL,
    CONSTRAINT fk_branch_treeId_id_id FOREIGN KEY (treeId_id) REFERENCES tree(id) ON DELETE RESTRICT ON UPDATE RESTRICT);

CREATE TABLE IF NOT EXISTS leaf (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(255) NOT NULL,
    branchId_id BIGINT NULL,
    CONSTRAINT fk_leaf_branchId_id_id FOREIGN KEY (branchId_id) REFERENCES branch(id) ON DELETE RESTRICT ON UPDATE RESTRICT);

It appears that the @JoinColumn is ignored. Compare tree_id with treeId_id, where I would've expected tree_id to be used for the column name. I feel like that is a recent change because it's caused by updating to 0.4.1 and I can't get back because the previous repository seems to be gone. Is this a bug? If not, is there a way to control the name of the join column in this case?

I would much rather not change the migrations

Jasync support

I think one of the core thing that any Kotlin ORM/persistence layer should have is the coroutine support. It's a sad landscape today where we have basic layers like https://github.com/jasync-sql/jasync-sql but nobody is building the nice idiomatic layer and almost no projects (except R2DBC) support the async DB calls. I believe Krush can actually be distinct here and support async DB out of the box.

Self-referenced entities generate invalid code

When attempting to create self referencing entities, the app fails to build because the generated code creates an invalid getOrNull method.

example:

@Entity
@Table(name = "category")
data class Category(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column
    val name: String,

    @OneToOne
    @JoinColumn(name = "parent_id")
    val parent: Category?,

    @OneToMany(mappedBy = "parent")
    val children: List<Category> = emptyList()
)

generates:

  resultRow.getOrNull(CategoryTable.id)?.let {
  	val children_category = children_map.filter { category -> category.key == it }
  		.mapValues { (_, category) -> category.copy(category = category) }
  		.values.toMutableSet()
  	category_children[categoryId]?.addAll(children_category) ?: category_children.put(categoryId, children_category)
  }

The issue is here mapValues { (_, category) -> category.copy(category = category) }, where the copy method is using a category parameter that doesn't exist. I believe the proper code should be mapValues { (_, category) -> category.copy(parent = category) }.

Any help would be appreciated!

EDIT:
Actually, it seems like any self-referenced parent-child entity causes problems. I tried this code to use a join table instead and got a error: Exception while building entity graph: java.lang.NullPointerException

@Entity
@Table(name = "category")
data class Category(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,

    @Column
    val name: String,

    @ManyToMany
    @JoinTable(name="category_category",
        joinColumns=[JoinColumn(name="category_id_1")],
        inverseJoinColumns=[JoinColumn(name="category_id_2")])
    val parentCategories: List<Category> = emptyList(),

    @ManyToMany(mappedBy="parentCategories")
    val childrenCategories: List<Category> = emptyList()
)

Nullable @Embedded causing compile error

@Entity
data class User(
    @Id @GeneratedValue
    val id: Long? = null,

    @Embedded
    @AttributeOverride(name = "houseNumber", column = Column(name = "house_no"))
    val contactAddress: Address,

    @Embedded
    @AttributeOverrides(
            AttributeOverride(name = "city", column = Column(name = "inv_city")),
            AttributeOverride(name = "street", column = Column(name = "inv_street"))
    )
    val invoiceAddress: Address? = null
)

@Embeddable
data class Address(

    @Column(name = "city")
    val city: String,

    @Column(name = "street")
    val street: String,

    @Column(name = "house_number")
    val houseNumber: Int
)

Support replace

    // given
    val book = Book(
        isbn = "1449373321",
        publishDate = LocalDate.of(2017, APRIL, 11),
        title = "Designing Data-Intensive Applications",
        author = "Martin Kleppmann"
    ).let(BookTable::insert)

    // when
    val updated = book.copy(publishDate = LocalDate.of(2021, APRIL, 11))
    BookTable.replace(updated)

    val selectedBooks = BookTable.selectAll().toBookList()

    // then
    assertThat(selectedBooks).containsOnly(updated)

Allow switching between deep and shallow references

This is a small follow-up after having used krush with self-references supported for a while. Although I'm happy with having code generated that can generate objects that reference others of the same kind, this has also started to become a bit of a problem:

When using the deep/real reference mode of krush, it requires the entire table of self-referential entities to be loaded into memory (because AFAIK there is no way to construct a query in a way that will select a row and also all of its (potentially n layers deep) referenced rows in the same layer). That by itself is a compromise I'm willing to make when I need these references to be correct - but that is not always the case. In my application, there are plenty of cases where only surface-level information is required to do what is needed, and in those cases, loading everything in the table feels a bit wasteful.

That's why I'd like to revisit this discussion - currently, there is only one global option for switching between shallow/deep references (which happens during code generation).
I would, however, find it useful if there was an (optional) parameter that could be used to turn this on or off per query that is being executed (at runtime). Existing code would automatically use the default value which enables the old, default behavior while new code could make use of being more specific about which mode to use.

Basically, something like this:

fun Iterable<ResultRow>.toEntityMap(deepReferences: Boolean = false): MutableMap<Int, Entity> {
  if(deepReferences) {
     // Generate deep references
  } else {
      // Business as usual
  }
}

Support shared reference on composite key

@Embeddable
data class RecordId(
    @Column(name = "RUN_ID")
    val runId: String,

    @Column(name = "RECORD_ID")
    val id: String,

    @Column(name = "RECORD_TYPE")
    @Enumerated(EnumType.STRING)
    val type: RecordType
)

@Entity
@Table(name = "RESULTS_RECORDS")
data class ResultRecord(
    @EmbeddedId
    val id: RecordId,

    @ManyToOne
    @JoinColumn(name = "RUN_ID")
    val summary: RunSummary

)

Support converter as companion object

It would be nice to be able to have a converter as an object. Example:

data class JobId(val id: Long = 0) {

    override fun toString(): String = id.toString()

    companion object : AttributeConverter<JobId, Long>() {
        override fun convertToDatabaseColumn(attribute: JobId): Long = attribute.toLong()
        override fun convertToEntityAttribute(dbData: Long): JobId = JobId(attribute)
    }
}

And using that converter:

@Entity
data class Resource(
    @Id val id: Long = 0,

    @Convert(converter = JobId.Companion::class)
    @Json(name = "job_id")
    val jobId: JobId = JobId()
)

Right now, the following throws errors in the generated tables.kt:

fun Table.job_id(columnName: String): Column<JobId> = longWrapper<JobId>(columnName, {
    com.mypackage.ids.JobId.Companion().convertToEntityAttribute(it) }, {
    com.mypackage.ids.JobId.Companion().convertToDatabaseColumn(it) })
}

This is because of the () after JobId.Companion. If the () are removed, then I believe it would work fine for objects (but obviously cause issues for classes).

I think the code where this occurs is located here: https://github.com/TouK/krush/blob/master/annotation-processor/src/main/kotlin/pl/touk/krush/source/TablesGenerator.kt#L221

A sneaky workaround is to add this function to the object:

fun Companion(): Companion = Companion

Support self referential relations

Currently, krush does not support relations that reference the same table.

Use case: I have a table with tasks. A task can depend on another task, thus there is a collection of dependencies in its data class:

@Entity
data class Task (
    @Id @GeneratedValue
    val id: Int? = null,
    val name: String = "unnamed task",
    val description: String = "",
    val timeEstimate: Int = 10,
    @JoinTable(name = "dependence_table")
    @ManyToMany
    val dependentOn: Collection<Task> = emptySet()
)

This leads to all sorts of build errors, unfortunately.

Nullable field in @Embeddable causing compile error

@Embeddable
data class ExtendedAddress(

    @Column(name = "city")
    val city: String,

    @Column(name = "street")
    val street: String,

    @Column(name = "house_number")
    val houseNumber: Int,

    @Column(name = "apartment_number")
    val apartmentNumber: Int? = null
)

AttributeConverter only supports wrapped String/Long

My domain model contains inline classes wrapping UUIDs. I'd like to be able to store these using the native Postgres UUID type, and this is allowed by JPA AttributeConverter class. Unfortunately the processor requires the target type to be either a Long or a String.

    private fun converterFunc(name: String, type: TypeName, it: ConverterDefinition, fileSpec: FileSpec.Builder) {
        val wrapperName = when (it.targetType.asClassName()) {
            STRING -> "stringWrapper"
            LONG -> "longWrapper"
            else -> throw TypeConverterNotSupportedException(it.targetType)
        }

Is there any particular reason for this, and if not would you accept a PR that adds UUID as an option?

Bump to Exposed 0.23.x

  • primaryKey depracated, migrate to override val primaryKey
  • new package org.jetbrains.exposed.sql.java-time

OneToOne not work

here I'm using SQLite and I'm trying to make a one-to-one association

Box class:

@Entity
data class Box(
    @Id @GeneratedValue
    val id: Long? = null,

    @OneToOne
    @JoinColumn(name = "item_id")
    val item: Item
)

Item class:

@Entity
data class Item(
    @Id @GeneratedValue
    val id: Long? = null,
    val size: Int,

    @OneToOne(mappedBy = "item")
    val box: Box? = null,
)

but when got null for item in below code :

transaction {
       val item = ItemTable.insert(Item(size = 5))
       val box = BoxTable.insert(Box(item = item))
       val result = BoxTable.selectAll().first().toBox()
       println(result)  // problem ==> result.item = null 
   }

I tried different ways for the one-to-one association but nothing worked and I realized in generated code when it try to get Item for box it checks the existence of "ItemTable.id" instead of "BoxTable.item_id"

anyway, is this my implementation problem or library problem ?

breaking tests due to old exposed version

HI!

the root cause of our failing tests (#54) apparently hasn't been fixed in exposed v0.32.1 but needs 0.33.1, which is also quite old and newer versions need major changes apparently. that's why I made a PR (#59) a while ago to have dependabot make PRs for available updates to keep up. it's quite important to be up to date in the database layer because of vulnerabilities of dependencies.

right now we help ourselves with the following hack in our gradle config, which is quite ugly and also deprecated:

    // forces exposed to be > 0.33.1, fixing failing tests. this is a transitive dependency of krush
    // we can remove this extra config after krush updates exposed
    api("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") {
        isForce = true
    }
    api("org.jetbrains.exposed:exposed-core:$exposedVersion") {
        isForce = true
    }
    api("org.jetbrains.exposed:exposed-java-time:$exposedVersion") {
        isForce = true
    }

Optional OneToOne is unsupported

There's no strict documentation, so I'm not sure if I'm right, but it looks like Krush can not support nullable (optional) values in OneToOne relations...
The following has also the same behavior when the relation is bidirectional instead of unidirectional, so there is no difference.

Imagine the following usecase: I have an entity Box and an entity Item. I want a Box to contain either an Item or nothing, so I write

@Entity
data class Item(
  @Id @GeneratedValue val id: Long? = null,
  val nonNullable: NotNull // any non-nullable type
)

@Entity
data class Box(
  @Id @GeneratedValue val id: Long? = null,
  @OneToOne(optional = true) @JoinColumn(name = "box_id") val item: Item? = null,
  // some other data maybe
)

What I expect from this mapping is, obviously, to have

  1. a BoxTable with val item: Column<Long?> = long("whatever").references(Item.id).nullable()
  2. a ResultRow.toBox(), which, as I can assume, will return a Box with item = null when there is no corresponding Item.

There are no issues with the first point, but ResultRow.toBox() looks like

fun ResultRow.toBox(): Box = Box(
  id = this[BoxTable.id],
  item = this.toItem(),
  ...
)

where this.toItem() returns an Item instead of Item? resulting in NullPointerException when trying to construct an Item from null row fields.

There is a workaround for this, making all Item's fields nullable and just checking if Item.id is null after calling .toBox(), but it seems a bit ugly, so I just wanted to ask if this usecase is going to be supported or it's better to use some other relation/entity structure.

Support for batch insert

Exposed has EntityTable.batchInsert() for inserting multiple items at once. Having something similar generated by krush would be more efficient than calling EntityTable.insert(entity) for every item individually.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.