• Stars
    star
    250
  • Rank 156,404 (Top 4 %)
  • Language
    Kotlin
  • License
    Apache License 2.0
  • Created over 4 years ago
  • Updated 8 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Idiomatic persistence layer for Kotlin

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.

More Repositories

1

nussknacker

Low-code tool for automating actions on real time data | Stream processing for the users.
Scala
585
star
2

sputnik

Static code review for your Gerrit patchsets. Runs Checkstyle, PMD, FindBugs, Scalastyle, CodeNarc, JSLint for you!
Java
199
star
3

bubble

Screen orientation detector for android
Kotlin
99
star
4

excel-export

excel-export grails plugin
Groovy
58
star
5

kotlin-exposed-realworld

Medium clone backend using Kotlin, Spring, Krush and Exposed. API as specified on https://realworld.io/
Kotlin
48
star
6

plumber

plumber helps you tame NiFi flow
Scala
44
star
7

sputnik-ci

Sputnik.ci - Continuous code reviews
Python
16
star
8

dockds

Docker contained database autoconfiguration for Spring Boot
Java
11
star
9

janusz

Slack bot for simplifying developer life
Java
10
star
10

http-mock-server

Groovy
9
star
11

nussknacker-kubernetes

Example deployment setup for running Nussknacker with Flink on Kubernetes cluster.
Shell
8
star
12

ctrl-pkw

Informacje na temat akcji "policzymy głosy w wyborach prezydenckich" i aplikacji dostępne na stronie
Java
8
star
13

nussknacker-helm

Helm chart installing Nussknacker
Shell
7
star
14

re-cms

A simple embeddable CMS
Clojure
6
star
15

nussknacker-quickstart

Docker Demo for Nussknacker - A visual tool to define and run real-time decision algorithms. Brings agility to business teams, liberates developers to focus on technology.
Shell
6
star
16

akka-http-swagger

An attempt to automatically generate and serve swagger documentation for REST APIs built with akka-http
JavaScript
5
star
17

hades

High Availability Data Source
Java
5
star
18

touk-bash

Bash snippets for your development
Shell
5
star
19

influxdb-reporter

Reporter to Influxdb 0.9 implementing (extended) Dropwizard metrics API
Scala
5
star
20

excel-export-samples

Examples on how to use excel-export plugin
Groovy
5
star
21

camel-spock

Small library allowing you to test your Camel routes with Spock
Groovy
5
star
22

airboat

A no-ceremony code review app, firstly developed during summer internships (2012) at TouK
JavaScript
4
star
23

RapidOSS3TouK

TouK Open Source fork to RapidOSS v3. RapidOSS is delivered by iFountain (http://www.ifountain.com) and you should go for stabe/official builds there. This here is to allow us to share any work we can, so it can be merged back to officiall build if it's worth it.
Groovy
4
star
24

QuaK

2D liero/soldat-inspired game made in 2 MD during internal TouK hackaton
Kotlin
3
star
25

jedzieTramwaj

Scala
3
star
26

nussknacker-flink-compatibility

Additional code needed for using Nussknacker with different Flink versions
Scala
3
star
27

petasos

A better user interface for https://github.com/allegro/hermes
TypeScript
3
star
28

confitura-man

A simple JS game created during 1-day hackathon.
JavaScript
3
star
29

cxf-utils

Java
2
star
30

jedzie-tramwaj-web

CoffeeScript
2
star
31

jpub-maven-plugin

Maven3 plugin that integrates Oracle JPublisher into Maven project lifecycle
Groovy
2
star
32

duck404

JavaScript
2
star
33

krush-example

Example project using krush
Kotlin
2
star
34

ormtest

Framework for unit testing Spring based DAOs
Java
2
star
35

angular-typewriter

Typewriter angular directive
JavaScript
2
star
36

angular-workshop

Source code from workshop about AngularJS @ TouK
JavaScript
2
star
37

gxt-tools

Java
2
star
38

widerest

RESTful API for Broadleaf Commerce
Java
2
star
39

touk-framework

Java
2
star
40

sonar-file-alerts-plugin

This plugin raises alerts on file level in Sonar. It extends default behaviour, which raises alerts only at root project level.
Java
2
star
41

devoxx-tv

Chromecast Hello World
JavaScript
1
star
42

gwtaculous

Java
1
star
43

touk-url

Haskell
1
star
44

ignite-issues

Java
1
star
45

nk-windows

Window manager used in Nussknacker (nussknacker.io)
TypeScript
1
star
46

nussknacker-sample-components

Sample components for Nussknacker
Scala
1
star
47

ksp-example

Kotlin Symbol Processing example
Kotlin
1
star
48

metatype-exporter-maven-plugin

Generate markdown file from OSGI metatype xml
Groovy
1
star
49

touk-angular-lib

CoffeeScript
1
star
50

nussknacker-benchmarks

Benchmarks for Nussknacker - A visual tool to define and run real-time decision algorithms. Brings agility to business teams, liberates developers to focus on technology.
Python
1
star