{"id":18245508,"url":"https://github.com/touk/krush","last_synced_at":"2025-09-05T11:09:43.906Z","repository":{"id":37432166,"uuid":"230889734","full_name":"TouK/krush","owner":"TouK","description":"Idiomatic persistence layer for Kotlin","archived":false,"fork":false,"pushed_at":"2023-08-30T09:09:12.000Z","size":768,"stargazers_count":248,"open_issues_count":14,"forks_count":13,"subscribers_count":23,"default_branch":"master","last_synced_at":"2025-04-02T09:08:33.549Z","etag":null,"topics":["jpa","kotlin","lightweight","sql"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TouK.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-12-30T09:46:17.000Z","updated_at":"2025-03-19T12:49:42.000Z","dependencies_parsed_at":"2024-11-05T09:33:16.087Z","dependency_job_id":null,"html_url":"https://github.com/TouK/krush","commit_stats":null,"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TouK%2Fkrush","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TouK%2Fkrush/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TouK%2Fkrush/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TouK%2Fkrush/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TouK","download_url":"https://codeload.github.com/TouK/krush/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248027411,"owners_count":21035594,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["jpa","kotlin","lightweight","sql"],"created_at":"2024-11-05T09:21:08.407Z","updated_at":"2025-04-09T11:11:45.738Z","avatar_url":"https://github.com/TouK.png","language":"Kotlin","readme":"## Krush\n![Maven Central](https://img.shields.io/maven-central/v/pl.touk.krush/krush-annotation-processor)\n![CircleCI](https://img.shields.io/circleci/build/github/TouK/krush)\n[![Sputnik](https://sputnik.ci/conf/badge)](https://sputnik.ci/app#/builds/TouK/krush)\n\n**Krush** is a lightweight persistence layer for Kotlin based on [Exposed SQL DSL](https://github.com/JetBrains/Exposed/wiki/DSL). It’s similar to [Requery](http://requery.io) and [Micronaut-data jdbc](https://micronaut-projects.github.io/micronaut-data/latest/guide/#jdbc), but designed to work idiomatically with Kotlin and immutable data classes.\n\nIt’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.\n\n### Rationale \n* **(type-safe) SQL-first** - use type-safe SQL-like DSL in your queries, no string or method name parsing \n* **Minimal changes to your domain model** - no need to extend external interfaces and used special types - just add annotations to your existing domain model\n* **Explicit fetching** - you specify explicitly in query what data you want to fetch, no additional fetching after data is loaded\n* **No runtime magic** - no proxies, lazy loading, just data classes containing data fetched from DB\n* **Pragmatic** - easy to start, but powerful even in not trivial cases (associations, grouping queries)\n\n### Example\nGiven a simple `Book` class:\n\n```kotlin\ndata class Book(\n   val id: Long? = null,\n   val isbn: String,\n   val title: String,\n   val author: String,\n   val publishDate: LocalDate\n)\n```\n\nwe can turn it into **Krush** entity by adding `@Entity` and `@Id` annotations:\n\n```kotlin\n@Entity\ndata class Book(\n   @Id @GeneratedValue\n   val id: Long? = null,\n   val isbn: String,\n   val title: String,\n   val author: String,\n   val publishDate: LocalDate\n)\n```\n\nWhen we build the project we’ll have `BookTable` mapping generated for us. So we can persist the `Book`:\n\n```kotlin\nval book = Book(\n   isbn = \"1449373321\", publishDate = LocalDate.of(2017, Month.APRIL, 11),\n   title = \"Designing Data-Intensive Applications\", author = \"Martin Kleppmann\"\n)\n\n// insert method is generated by Krush\nval persistedBook = BookTable.insert(book)\nassertThat(persistedBook.id).isNotNull()\n```\n\nSo we have now a `Book` persisted in DB with autogenerated `Book.id` field.\nAnd now we can use type-safe SQL DSL to query the `BookTable`:\n\n```kotlin\nval bookId = book.id ?: throw IllegalArgumentException()\n\n// toBook method is generated by Krush\nval fetchedBook = BookTable.select { BookTable.id eq bookId }.singleOrNull()?.toBook()\nassertThat(fetchedBook).isEqualTo(book)\n\n// toBookList method is generated by Krush\nval selectedBooks = (BookTable)\n   .select { BookTable.author like \"Martin K%\" }\n   .toBookList()\n\nassertThat(selectedBooks).containsOnly(persistedBook)\n```\n\n### Installation\nGradle Groovy:\n```groovy\nrepositories {\n    mavenCentral()\n}\n\napply plugin: 'kotlin-kapt'\n\ndependencies {\n    api \"pl.touk.krush:krush-annotation-processor:$krushVersion\"\n    kapt \"pl.touk.krush:krush-annotation-processor:$krushVersion\"\n    api \"pl.touk.krush:krush-runtime:$krushVersion\" \n}\n```\n\nGradle Kotlin:\n```kotlin\nrepositories {\n    mavenCentral()\n}\n\nplugins {\n    kotlin(\"kapt\") version \"$kotlinVersion\"\n}\n\ndependencies {\n    api(\"pl.touk.krush:krush-annotation-processor:$krushVersion\")\n    kapt(\"pl.touk.krush:krush-annotation-processor:$krushVersion\")\n    api(\"pl.touk.krush:krush-runtime:$krushVersion\")\n}\n```\n\nMaven:\n```xml\n\u003cdependencies\u003e\n    \u003cdependency\u003e\n        \u003cgroupId\u003epl.touk.krush\u003c/groupId\u003e\n        \u003cartifactId\u003ekrush-runtime\u003c/artifactId\u003e\n        \u003cversion\u003e${krush.version}\u003c/version\u003e\n    \u003c/dependency\u003e\n\u003c/dependencies\u003e\n\n...\n\n\u003cplugin\u003e\n    \u003cgroupId\u003eorg.jetbrains.kotlin\u003c/groupId\u003e\n    \u003cartifactId\u003ekotlin-maven-plugin\u003c/artifactId\u003e\n    \u003cexecutions\u003e\n        \u003cexecution\u003e\n            \u003cid\u003ekapt\u003c/id\u003e\n            \u003cgoals\u003e\n                \u003cgoal\u003ekapt\u003c/goal\u003e\n            \u003c/goals\u003e\n            \u003cconfiguration\u003e\n                ...\n                \u003cannotationProcessorPaths\u003e\n                    \u003cannotationProcessorPath\u003e\n                        \u003cgroupId\u003epl.touk.krush\u003c/groupId\u003e\n                        \u003cartifactId\u003ekrush-annotation-processor\u003c/artifactId\u003e\n                        \u003cversion\u003e${krush.version}\u003c/version\u003e\n                    \u003c/annotationProcessorPath\u003e\n                \u003c/annotationProcessorPaths\u003e\n            \u003c/configuration\u003e\n        \u003c/execution\u003e\n        ...\n    \u003c/executions\u003e\n\u003c/plugin\u003e\n```\n\n### Dependencies\n* [JetBrains Exposed](https://github.com/JetBrains/Exposed)\n* JPA annotations 2.1\n\n### Features\n* generates table mappings and functions for mapping from/to data classes\n* type-safe SQL DSL without reading schema from existing database (code-first)\n* explicit association fetching (via `leftJoin` / `innerJoin`)\n* multiple data types support, including type aliases\n* custom data type support (with `@Converter`), also for wrapped auto-generated ids\n* you can still persist associations not directly reflected in domain model (eq. article favorites) \n\nHowever, **Krush** is not a full-blown ORM library. This means following JPA features are not supported:\n* lazy association fetching\n* dirty checking\n* caching\n* versioning / optimistic locking\n\n### Updating\nGiven following entity:\n```kotlin\n@Entity\ndata class Reservation(\n    @Id\n    val uid: UUID = UUID.randomUUID(),\n\n    @Enumerated(EnumType.STRING)\n    val status: Status = Status.FREE,\n\n    val reservedAt: LocalDateTime? = null,\n    val freedAt: LocalDateTime? = null\n) {\n    fun reserve() = copy(status = Status.RESERVED, reservedAt = LocalDateTime.now())\n    fun free() = copy(status = Status.FREE, freedAt = LocalDateTime.now())\n}\n\nenum class Status { FREE, RESERVED }\n```\nyou can call Exposed `update` with generated `from` metod to overwrite it's data:\n\n```kotlin\nval reservation = Reservation().reserve().let(ReservationTable::insert)\n\nval freedReservation = reservation.free()\nReservationTable.update({ ReservationTable.uid eq reservation.uid }) { it.from(freedReservation) }\n\nval updatedReservation = ReservationTable.select({ ReservationTable.uid eq reservation.uid }).singleOrNull()?.toReservation()\nassertThat(updatedReservation?.status).isEqualTo(Status.FREE)\nassertThat(updatedReservation?.reservedAt).isEqualTo(reservation.reservedAt)\nassertThat(updatedReservation?.freedAt).isEqualTo(freedReservation.freedAt)\n```\n\nFor simple cases you can still use Exposed native update syntax:\n```kotlin\nval freedAt = LocalDateTime.now()\nReservationTable.update({ ReservationTable.uid eq reservation.uid }) {\n  it[ReservationTable.status] = Status.FREE\n  it[ReservationTable.freedAt] = freedAt\n}\n```\n\nOther Exposed features are supported as well, like, `replace`:\n```kotlin\nval reservation = Reservation().reserve()\n\nReservationTable.replace { it.from(reservation) }\nval freedReservation = reservation.free()\nReservationTable.replace { it.from(freedReservation) }\n\nval allReservations = ReservationTable.selectAll().toReservationList()\nassertThat(allReservations).containsExactly(freedReservation)\n```               \nand `batchInsert`/`batchReplace`:\n```kotlin\nval reservation1 = Reservation().reserve()\nval reservation2 = Reservation().reserve()\n\nReservationTable.batchInsert(\n    listOf(reservation1, reservation2), body = { this.from(it) }\n)\nval allReservations = ReservationTable.selectAll().toReservationList()\nassertThat(allReservations)\n    .containsExactly(reservation1, reservation2)\n}\n```\n[Complete example](https://github.com/TouK/krush-example/blob/master/src/test/kotlin/pl/touk/krush/ReservationTest.kt)\n\n### Associations\n\n```kotlin\n@Entity\n@Table(name = \"articles\")\ndata class Article(\n    @Id @GeneratedValue\n    val id: Long? = null,\n\n    @Column(name = \"title\")\n    val title: String,\n\n    @ManyToMany\n    @JoinTable(name = \"article_tags\")\n    val tags: List\u003cTag\u003e = emptyList()\n)\n\n@Entity\n@Table(name = \"tags\")\ndata class Tag(\n    @Id @GeneratedValue\n    val id: Long? = null,\n\n    @Column(name = \"name\")\n    val name: String\n)\n```\n\nPersisting\n\n```kotlin\nval tag1 = Tag(name = \"jvm\")\nval tag2 = Tag(name = \"spring\")\n\nval tags = listOf(tag1, tag2).map(TagTable::insert)\nval article = Article(title = \"Spring for dummies\", tags = tags)\nval persistedArticle = ArticleTable.insert(article)\n```\n\nQuerying and fetching\n```kotlin\nval (selectedArticle) = (ArticleTable leftJoin ArticleTagsTable leftJoin TagTable)\n    .select { TagTable.name inList listOf(\"jvm\", \"spring\") }\n    .toArticleList()\n\nassertThat(selectedArticle).isEqualTo(persistedArticle)\n```\n\nUpdate logic for associations not implemented (yet!) - you have to manually add/remove records from `ArticleTagsTable`.\n\n### Custom column wrappers\n\nKrush exposes some helpful wrappers for user classes to easily convert them to specific columns in database, e.g.\n\n```kotlin\n@JvmInline\nvalue class MyStringId(val raw: String)\n\n@JvmInline\nvalue class MyUUID(val raw: UUID)\n\n@JvmInline\nvalue class MyVersion(val raw: Int)\n\nenum class MyState { ACTIVE, INACTIVE }\n\nfun Table.myStringId(name: String) = stringWrapper(name, ::MyStringId) { it.raw }\n\nfun Table.myUUID(name: String) = uuidWrapper(name, ::MyUUID) { it.raw }\n\nfun Table.myVersion(name: String) = integerWrapper(name, ::MyVersion) { it.raw }\n\nfun Table.myState(name: String) = booleanWrapper(name, { if (it) MyState.ACTIVE else MyState.INACTIVE }) {\n    when (it) {\n        MyState.ACTIVE -\u003e true\n        MyState.INACTIVE -\u003e false\n    }\n}\n\nobject MyTable : Table(\"test\") {\n    val id = myStringId(\"my_id\").nullable()\n    val uuid = myUUID(\"my_uuid\").nullable()\n    val version = myVersion(\"my_version\").nullable()\n    val state = myState(\"my_state\").nullable()\n}\n```\n\n### Support for Postgresql `distinct on (...)`\n\nPostgresql allows usage of nonstandard clause [`DISTINCT ON` in queries](https://www.postgresql.org/docs/current/sql-select.html).\n\nKrush provides custom `distinctOn` extension method which can be used as first parameter in custom `slice` extension method.\n\n**Postgresql specific extensions needs `krush-runtime-postgresql` dependency in maven or gradle**\n\nExample code:\n\n```kotlin\n@JvmInline\nvalue class MyStringId(val raw: String)\n\n@JvmInline\nvalue class MyVersion(val raw: Int)\n\nfun Table.myStringId(name: String) = stringWrapper(name, ::MyStringId) { it.raw }\n\nfun Table.myVersion(name: String) = integerWrapper(name, ::MyVersion) { it.raw }\n\n\nobject MyTable : Table(\"test\") {\n    val id = myStringId(\"my_id\").nullable()\n    val version = myVersion(\"my_version\").nullable()\n    val content = jsonb(\"content\").nullable()\n}\n\nfun findNewestContentVersion(id: MyStringId): String? =\n    MyTable\n        .slice(MyTable.id.distinctOn(), MyTable.content)\n        .select { MyTable.id eq id }\n        .orderBy(MyTable.id to SortOrder.ASC, MyTable.version to SortOrder.DESC)\n        .map { it[MyTable.content] }\n        .firstOrNull()\n```\n\nwhen `findNewestContentVersion(MyStringId(\"123\"))` is called  will generate SQL:\n\n```postgresql\nSELECT DISTINCT ON (test.my_id) TRUE, test.my_id, test.\"content\"\nFROM test\nWHERE test.my_id = '123'\nORDER BY test.my_id ASC, test.my_version DESC\n```\n\n### Example projects\n\n* [https://github.com/TouK/kotlin-exposed-realworld](https://github.com/TouK/kotlin-exposed-realworld)\n* [https://github.com/TouK/krush-example](https://github.com/TouK/krush-example)\n\n### Contributors\n* [Mateusz Śledź](https://github.com/mateuszsledz)\n* [Piotr Jagielski](https://github.com/pjagielski)\n* [Namnodorel](https://github.com/Namnodorel)\n* [Dominik Przybysz](https://github.com/alien11689)\n\nSpecial thanks to [Łukasz Jędrzejewski](https://github.com/jedrz) for original idea of using Exposed in our projects.\n\n### Licence\n**Krush** is published under Apache License 2.0.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftouk%2Fkrush","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftouk%2Fkrush","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftouk%2Fkrush/lists"}