{"id":15383566,"url":"https://github.com/mvysny/vok-orm","last_synced_at":"2025-04-14T10:33:24.050Z","repository":{"id":29747479,"uuid":"122636495","full_name":"mvysny/vok-orm","owner":"mvysny","description":"Mapping rows from a SQL database to POJOs in its simplest form","archived":false,"fork":false,"pushed_at":"2025-03-21T07:03:52.000Z","size":608,"stargazers_count":24,"open_issues_count":4,"forks_count":4,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-14T03:49:03.440Z","etag":null,"topics":["database","jdbc","jdbc-orm-framework","kotlin","pojos","sql-databases","sql2o","vok-orm"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mvysny.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2018-02-23T15:11:04.000Z","updated_at":"2025-03-21T07:03:55.000Z","dependencies_parsed_at":"2024-08-23T17:52:32.621Z","dependency_job_id":"51dcd73e-ba94-485a-a163-cb4a1c4245df","html_url":"https://github.com/mvysny/vok-orm","commit_stats":{"total_commits":468,"total_committers":6,"mean_commits":78.0,"dds":0.4508547008547008,"last_synced_commit":"22729ae8707f8906fb3098cc704f6374f28a0733"},"previous_names":[],"tags_count":28,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mvysny%2Fvok-orm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mvysny%2Fvok-orm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mvysny%2Fvok-orm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mvysny%2Fvok-orm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mvysny","download_url":"https://codeload.github.com/mvysny/vok-orm/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248862952,"owners_count":21173904,"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":["database","jdbc","jdbc-orm-framework","kotlin","pojos","sql-databases","sql2o","vok-orm"],"created_at":"2024-10-01T14:38:48.123Z","updated_at":"2025-04-14T10:33:24.032Z","avatar_url":"https://github.com/mvysny.png","language":"Kotlin","readme":"[![Join the chat at https://gitter.im/vaadin/vaadin-on-kotlin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/vaadin/vaadin-on-kotlin?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge)\n[![GitHub tag](https://img.shields.io/github/tag/mvysny/vok-orm.svg)](https://github.com/mvysny/vok-orm/tags)\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.mvysny.vokorm/vok-orm/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.mvysny.vokorm/vok-orm)\n\n# Vaadin-On-Kotlin database mapping library\n\n`vok-orm` allows you to load the data from database rows into objects (POJOs)\nand write the data back into the database. No JPA dirty tricks are used: no runtime\nenhancements, no lazy loading, no `DetachedExceptions`, no change tracking\nbehind the scenes - everything happens explicitly. No compiler\nplugin is needed - `vok-orm` uses Kotlin language features to add a standard\nset of finders to your entities. You can add any number of business logic methods as\nyou like to your entities; the database transaction is easy to launch simply by calling the\nglobal `db {}` function.\n\nNo dependency injection framework is required - the library works in all\nsorts of environments.\n\n\u003e vok-orm uses the [jdbi-orm](https://gitlab.com/mvysny/jdbi-orm) and [JDBI](http://jdbi.org/) under the belt,\nand introduces first-class Kotlin support on top of those frameworks.\n\n## Supported Databases\n\nvok-orm currently supports MySQL, MariaDB, PostgreSQL, H2, Microsoft SQL and CockroachDB.\nOther databases are untested - they might or might not work.\n\n## Usage\n\nJust add the following lines to your Gradle script, to include this library in your project:\n```groovy\nrepositories {\n    mavenCentral()\n}\ndependencies {\n    compile(\"com.github.mvysny.vokorm:vok-orm:x.y\")\n}\n```\n\n\u003e Note: obtain the newest version from the tag name at the top of the page\n\nMaven: (it's very simple since vok-orm is in Maven Central):\n\n```xml\n\u003cproject\u003e\n\t\u003cdependencies\u003e\n\t\t\u003cdependency\u003e\n\t\t\t\u003cgroupId\u003ecom.github.mvysny.vokorm\u003c/groupId\u003e\n\t\t\t\u003cartifactId\u003evok-orm\u003c/artifactId\u003e\n\t\t\t\u003cversion\u003ex.y\u003c/version\u003e\n\t\t\u003c/dependency\u003e\n    \u003c/dependencies\u003e\n\u003c/project\u003e\n```\n\nSee the [vok-orm-playground](https://gitlab.com/mvysny/vok-orm-playground) for a very simple example project\nusing `vok-orm`.\n\nCompatibility Chart:\n\n| vok-orm version | validation API     | Min. Java version |\n|-----------------|--------------------|-------------------|\n| 1.x             | javax.validation   | 8+                |\n| 2.x             | jakarta.validation | 11+               |\n\n## Usage Examples\n\nSay that we have a table containing a list of beverage categories, such as Cider or Beer. The H2 DDL for such table is simple:\n\n```sql92\ncreate TABLE CATEGORY (\n  id bigint auto_increment PRIMARY KEY,\n  name varchar(200) NOT NULL\n);\ncreate UNIQUE INDEX idx_category_name ON CATEGORY(name);\n```\n\n\u003e **Note:** We expect that the programmer wants to write the DDL scripts herself, to make full\nuse of the DDL capabilities of the underlying database.\nWe will therefore not hide the DDL behind some type-safe generator API.\n\nSuch entity can be mapped to a data class as follows:\n```kotlin\ndata class Category(override var id: Long? = null, var name: String = \"\") : KEntity\u003cLong\u003e\n```\n(the `id` is nullable since its value is initially `null` until the category is\nactually created and the id is assigned by the database).\n\nThe `Category` class is just a simple data class: there are no hidden private fields added by\nruntime enhancements, no hidden lazy loading - everything is pre-fetched upfront. Because of that,\nthe class can be passed around the application freely as a DTO (data transfer object),\nwithout the fear of failing with\n`DetachedException` when accessing properties. Since `Entity` is `Serializable`, you can\nalso store the entity into a session. \n\n\u003e The Category class (or any entity class for that matter) must have all fields\npre-initialized, so that Kotlin creates a zero-arg constructor.\nZero-arg constructor is mandated by JDBI, in order for JDBI to be able to construct\ninstances of entity class for every row returned.\n\nBy implementing the `KEntity\u003cLong\u003e` interface, we are telling vok-orm that the primary key is of type `Long`;\nthis will be important later on when using Dao.\nThe [KEntity](src/main/kotlin/com/github/vokorm/KEntity.kt) interface brings in three useful methods:\n\n* `save()` which either creates a new row by generating the INSERT statement\n  (if the ID is null), or updates the row by generating the UPDATE statement (if the ID is not null)\n* `create()` for special cases when the ID is pre-known (social security number)\n  and `save()` wouldn't work. More info in the 'Pre-known IDs' chapter.\n* `delete()` which deletes the row identified by the `id` primary key from the database.\n* `validate()` validates the bean. By default all `javax.validation` annotations\n  are validated; you can override this method to provide further bean-level validations.\n  Please read the 'Validation' chapter below, for further details.\n\n\u003e There are two interfaces you can use: Entity and KEntity. Both work the same way, however\nEntity is tailored towards Java developers and is not as pleasant to use with Kotlin as KEntity is.\n\nThe INSERT/UPDATE statement is automatically constructed by the `save()` method,\nsimply by enumerating all non-transient and non-ignored properties of\nthe bean using reflection and fetching their values. See the [KEntity](src/main/kotlin/com/github/vokorm/KEntity.kt)\nsources for more details.\nYou can annotate the `Category` class with the `@Table(dbname = \"Categories\")` annotation, to specify a different table name.\n\nThe category can now be created easily:\n\n```kotlin\nCategory(name = \"Beer\").save()\n```\n\nBut how do we specify the target database where to store the category in? \n\n### Connecting to a database\n\nAs a bare minimum, you need to specify the JDBC URL and a couple of config parameters as follows:\n```kotlin\nJdbiOrm.setDataSource(HikariDataSource(HikariConfig()))\n```\n\nto the `JdbiOrm.setDataSource()` first. It's a [Hikari-CP](https://brettwooldridge.github.io/HikariCP/)\nconfiguration file which contains lots of other options as well.\nIt comes pre-initialized with sensible default settings.\n\n\u003e Hikari-CP is a JDBC connection pool which manages a pool of JDBC connections\nsince they are \"expensive\" to create - it takes some time to establish the TCP-IP connection for example.\nTypically all projects use some sort of JDBC connection pooling; we'll use Hikari-CP in this\ntutorial however you can use whichever pool you wish, or no pool at all. You can also use DataSource\noffered by Spring or JavaEE.\n\nFor example, to use an in-memory H2 database, just add H2 onto the classpath as\na Gradle dependency: `compile 'com.h2database:h2:1.4.196'`. Then,\nconfigure vok-orm as follows:\n\n```kotlin\nval cfg = HikariConfig().apply {\n    jdbcUrl = \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1\"\n    username = \"sa\"\n    password = \"\"\n}\nJdbiOrm.setDataSource(HikariDataSource(cfg))\n```\n\nAfter you have configured the JDBC URL, just call `JdbiOrm.setDataSource(HikariDataSource(cfg))` which will initialize\nHikari-CP's connection pool. After the connection pool is initialized, you can simply call\nthe `db{}` function to run the\nblock in a database transaction. The `db{}` function will acquire new connection from the\nconnection pool; then it will start a transaction and it will provide you with means to execute SQL commands:\n\n```kotlin\ndb {\n    handle.createUpdate(\"delete from Category where id = :id\")\n        .bind(\"id\", id)\n        .execute()\n}\n```\n\nYou can call this function from anywhere; you don't need to use dependency injection or anything like that.\nThat is precisely how the `save()` function saves the bean - it simply calls the `db {}` function and executes\nan appropriate INSERT/UPDATE statement.\n\nThe function will automatically roll back the transaction on any exception thrown out from the block (both checked and unchecked).\n\nAfter you're done, call `JdbiOrm.destroy()` to close the pool.\n\n\u003e You can call methods of this library from anywhere. You don't need to be running inside of the JavaEE or Spring container or\nany container at all - you can actually use this library from a plain JavaSE main method.\n\nFull example of a `main()` method that does all of the above:\n\n```kotlin\nfun main(args: Array\u003cString\u003e) {\n    val cfg = HikariConfig().apply {\n        jdbcUrl = \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1\"\n        username = \"sa\"\n        password = \"\"\n    }\n    JdbiOrm.setDataSource(HikariDataSource(cfg))\n    db {\n        con.createQuery(\"create TABLE CATEGORY (id bigint auto_increment PRIMARY KEY, name varchar(200) NOT NULL );\").executeUpdate()\n    }\n    db {\n        (0..100).forEach { Category(name = \"cat $it\").save() }\n    }\n    JdbiOrm.destroy()\n}\n```\n\nSee the [vok-orm-playground](https://gitlab.com/mvysny/vok-orm-playground)\nproject which contains such `main` method, all JDBC drivers pre-loaded and\nsimple instructions on how to query different database kinds.\n\n\u003e *Note*: for the sake of simplicity we're running the `CREATE TABLE` as a query. For a persistent database\nit's definitely better to use [Flyway](https://flywaydb.org/) as described below.\n\n### Finding Categories\n\nThe so-called finder (or Dao) methods actually resemble factory methods since they also produce instances of Categories. The best place for such\nmethods is on the `Category` class itself. We can write all of the necessary finders ourselves, by using the `db{}`\nmethod as stated above; however vok-orm already provides a set of handy methods for you. All you need\nto do is for the companion object to extend the `Dao` class:\n\n```kotlin\ndata class Category(override var id: Long? = null, var name: String = \"\") : KEntity\u003cLong\u003e {\n    companion object : Dao\u003cCategory, Long\u003e(Category::class.java)\n}\n```\n\nSince Category's companion object extends the `Dao` class, Category will now be outfitted\nwith several useful finder methods (static extension methods\nthat are attached to the [Dao](src/main/kotlin/com/github/vokorm/Dao.kt) interface itself):\n\n* `Category.findAll()` will return a list of all categories\n* `Category.getById(25L)` will fetch a category with the ID of 25, failing if there is no such category\n* `Category.findById(25L)` will fetch a category with ID of 25, returning `null` if there is no such category\n* `Category.deleteAll()` will delete all categories\n* `Category.deleteById(42L)` will delete a category with ID of 42\n* `Category.count()` will return the number of rows in the Category table.\n* `Category.findBy { \"name = :name1 or name = :name2\"(\"name1\" to \"Beer\", \"name2\" to \"Cider\") }` will find all categories with the name of \"Beer\" or \"Cider\".\n  This is an example of a parametrized select, from which you only need to provide the WHERE clause.\n* `Category.deleteBy { (Category::name eq \"Beer\") or (Category::name eq \"Cider\") }` will delete all categories\n  matching given criteria. This is an example of a statically-typed matching criteria which\n  is converted into the WHERE clause.\n* `Category.singleBy { \"name = :name\"(\"name\" to \"Beer\") }` will fetch exactly one matching category, failing if there is no such category or there are more than one.\n* `Category.findSingleBy { \"name = :name\"(\"name\" to \"Beer\") }` will fetch one matching category, failing if there are more than one. Returns `null` if there is none.\n* `Category.count { \"name = :name\"(\"name\" to \"Beer\") }` will return the number of rows in the Category table matching given query.\n\nIn the spirit of type safety, the finder methods will only accept `Long` (or whatever is the type of\nthe primary key in the `KEntity\u003cx\u003e` implementation clause). \n\nYou can of course add your own custom finder methods into the Category companion object. For example:\n\n```kotlin\ndata class Category(override var id: Long? = null, var name: String = \"\") : KEntity\u003cLong\u003e {\n    companion object : Dao\u003cCategory\u003e {\n        fun findByName(name: String): Category? = findSingleBy { Category::name eq name }\n        fun getByName(name: String): Category = singleBy { Category::name eq name }\n        fun existsWithName(name: String): Boolean = count { Category::name eq name } \u003e 0\n    }\n}\n```  \n\n\u003e **Note**: If you don't want to use the KEntity interface for some reason\n(for example when the table has no primary key), you can still include\nuseful finder methods by making the companion object to implement the `DaoOfAny`\ninterface. The finder methods such as `findById()` will accept\n`Any` as a primary key.\n\n### Adding Reviews\n\nLet's add the second table, the \"Review\" table. The Review table is a list of reviews for\nvarious drinks; it back-references the drink category as a foreign key into the `Category` table:\n\n```sql92\ncreate TABLE REVIEW (\n  id bigint auto_increment PRIMARY KEY,\n  beverageName VARCHAR(200) not null,\n  score TINYINT NOT NULL,\n  date DATE not NULL,\n  category BIGINT,\n  count TINYINT not null\n);\nalter table Review add CONSTRAINT r_score_range CHECK (score \u003e= 1 and score \u003c= 5);\nalter table Review add FOREIGN KEY (category) REFERENCES Category(ID);\nalter table Review add CONSTRAINT r_count_range CHECK (count \u003e= 1 and count \u003c= 99);\ncreate INDEX idx_beverage_name ON Review(beverageName);\n```\n\nThe mapping class is as follows:\n```kotlin\n/**\n * Represents a beverage review.\n * @property score the score, 1..5, 1 being worst, 5 being best\n * @property date when the review was done\n * @property category the beverage category [Category.id]\n * @property count times tasted, 1..99\n */\ndata class Review(override var id: Long? = null,\n                  var score: Int = 1,\n                  var beverageName: String = \"\",\n                  var date: LocalDate = LocalDate.now(),\n                  var category: Long? = null,\n                  var count: Int = 1) : KEntity\u003cLong\u003e {\n\n    companion object : Dao\u003cReview, Long\u003e(Review::class.java)\n}\n```\n\nNow if we want to delete a category, we need to first set the `Review.category` value to `null` for all reviews that\nare linked to that very category, otherwise\nwe will get a foreign constraint violation. It's quite easy: just override the `delete()` method in the\n`Category` class as follows:\n\n```kotlin\ndata class Category(/*...*/) : KEntity\u003cLong\u003e {\n    // ...\n    override fun delete() {\n        db {\n            if (id != null) {\n                handle.createQuery(\"update Review set category = NULL where category=:catId\")\n                        .bind(\"catId\", id!!)\n                        .executeUpdate()\n            }\n            super.delete()\n        }\n    }\n}\n```\n\n\u003e **Note:** for all slightly more complex queries it's a good practice to simply use the JDBI API - we will simply pass in the SQL command as a String to JDBI.\n\nAs you can see, you can use the JDBI connection yourself, to execute any kind of SELECT/UPDATE/INSERT/DELETE statements as you like.\nFor example you can define static finder or computation method into the `Review` companion object:\n\n```kotlin\n    companion object : Dao\u003cReview, Long\u003e(Review::class.java) {\n        /**\n         * Computes the total sum of [count] for all reviews belonging to given [categoryId].\n         * @return the total sum, 0 or greater.\n         */\n        fun getTotalCountForReviewsInCategory(categoryId: Long): Long = db {\n            handle.createQuery(\"select sum(r.count) from Review r where r.category = :catId\")\n                    .bind(\"catId\", categoryId)\n                    .mapTo(Long::class.java).one() ?: 0L\n        }\n    }\n```\n\nThen we can outfit the Category itself with this functionality, by adding an extension method to compute this value:\n```kotlin\nfun Category.getTotalCountForReviews(): Long = Review.getTotalCountForReviewsInCategory(id!!)\n```\n\nNote how freely and simply we can add useful business logic methods to entities. It's because:\n\n* the entities are just plain old classes with no hidden fields and no runtime enhancements, and\n* because we can invoke `db{}` freely from anywhere. You don't need transaction annotations and injected entity managers,\n  and you don't need huge container such as Spring or JavaEE which must instantiate your classes\n  in order to activate those annotations and injections.\n  Those are things of the past.\n\n### Auto-generated IDs vs pre-provided IDs\n\nThere are generally three cases for entity ID generation:\n\n* IDs generated by the database when the `INSERT` statement is executed\n* Natural IDs, such as a NaturalPerson with ID pre-provided by the government (social security number etc).\n* IDs created by the application, for example via `UUID.randomUUID()`\n\nThe `save()` method is designed to work out-of-the-box only for the first case (IDs auto-generated by the database). In this\ncase, `save()` emits `INSERT` when the ID is null, and `UPDATE` when the ID is not null.\n\nWhen the ID is pre-provided, you can only use `save()` method to update a row in the database; using `save()` to create a\nrow in the database will throw an exception. In order to create an\nentity with a pre-provided ID, you need to use the `create()` method:\n```kotlin\nNaturalPerson(id = \"12345678\", name = \"Albedo\").create()\n```\n\nFor entities with IDs created by the application you can make `save()` work properly, by overriding the `create()` method\nin your entity as follows:\n```kotlin\noverride fun create(validate: Boolean) {\n  id = UUID.randomUUID()\n  super.create(validate)\n}\n```\n\nEven better, you can inherit from the `Entity` interface as follows:\n\n```kotlin\ninterface UuidEntity : KEntity\u003cUUID\u003e {\n    override fun create(validate: Boolean) {\n        id = UUID.randomUUID()\n        super.create(validate)\n    }\n}\n```\n\nAnd simply make all of your entities implement the `UuidEntity` interface.\n\n### Joins\n\nWhen we display a list of reviews (say, in a Vaadin Grid), we want to display an actual category name instead of the numeric category ID.\nWe can take advantage of JDBI simply matching all SELECT column names into bean fields; all we have to do is to:\n\n* create a new class which contains both the `Review` field and add the `categoryName` field which will carry the category name information;\n* write a SELECT that will return all of the `Review` fields, and, additionally, the `categoryName` field\n\nLet's thus create a `ReviewWithCategory` class:\n\n```kotlin\nclass ReviewWithCategory : Serializable {\n    @field:Nested\n    var review: Review = Review()\n    @field:ColumnName(\"categoryName\")\n    var categoryName: String? = null\n}\n```\n\n\u003e Note the `@ColumnName` annotation which tells vok-orm that the field is named differently in the database. Often the database naming schema\nis different from Kotlin's naming schema, for example `NUMBER_OF_PETS` would be represented by the `numberOfPets` in the Kotlin class.\nYou can use database aliases, for example `SELECT NUMBER_OF_PETS AS numberOfPets`. However note that you can't then add a `WHERE` clause on\nthe `numberOfPets` alias - that's not supported by SQL databases. See [Issue #5](https://github.com/mvysny/vok-orm/issues/5) for more details.\nCurrently we don't use WHERE in our examples so you're free to use aliases, but aliases do not work with Data Loaders and therefore it's a good\npractice to use `@ColumnName` instead of SQL aliases.\n\nNow we can add a new finder function into `ReviewWithCategory`'s companion object:\n\n```kotlin\ncompanion object : DaoOfAny\u003cReviewWithCategory\u003e(ReviewWithCategory::class.java) {\n    //...\n    fun findReviews(): List\u003cReviewWithCategory\u003e = db {\n        handle.createQuery(\"\"\"select r.*, c.name\n            FROM Review r left join Category c on r.category = c.id\n            ORDER BY r.name\"\"\")\n                .map(getRowMapper())\n                .list()\n    }\n}\n```\n\nIt also makes sense to add this function to `Review`'s companion object:\n```kotlin\ncompanion object : Dao\u003cReview\u003e(Review::class.java) {\n    //...\n    fun findReviews() = ReviewWithCategory.findReviews()\n}\n```\n\nWe can take JDBI's mapping capabilities to full use: we can craft any SELECT we want,\nand then we can create a holder class that will not be an entity itself, but will merely hold the result of that SELECT.\nThe only thing that matters is that the class will have properties named exactly as the columns in the SELECT statement (or properly aliased\nusing the `@ColumnName` annotation):\n\n```kotlin\ndata class Beverage(@field:ColumnName(\"beverageName\") var name: String = \"\", @field:ColumnName(\"name\") var category: String? = null) : Serializable {\n    companion object {\n        fun findAll(): List\u003cBeverage\u003e = db {\n            handle.createQuery(\"select r.beverageName, c.name from Review r left join Category c on r.category = c.id\")\n                .map(FieldMapper.of(Beverage::class.java))\n                .list()\n        }\n    }\n}\n```\n\nWe just have to make sure that all of the `Beverage`'s fields are pre-initialized, so that the `Beverage` class has a zero-arg constructor.\nIf not, JDBI will throw an exception in runtime, stating that the `Beverage` class has no zero-arg constructor.\n\n## Validations\n\nOften the database entities are connected to UI forms which need to provide sensible\nvalidation errors to the users as they enter invalid values. The validation\ncould be done on the database level, but databases tend to provide unlocalized\ncryptic error messages. Also, some validations are either impossible to do, or very hard\nto do on the database level. That's why `vok-orm` provides additional validation\nlayer for your entities.\n\n`vok-orm` uses [JSR303 Java Standard for Validation](https://en.wikipedia.org/wiki/Bean_Validation); you can\nquickly skim over [JSR303 tutorial](https://dzone.com/articles/bean-validation-made-simple) to see how to start\nusing the validation.\nIn a nutshell, you annotate your KEntity's fields with validation annotations; the fields are\nthen checked for valid values with the JSR303 Validator (invoked when\n`entity.validate()`/`entity.save()`/`entity.create()` is called). The validation is\nalso mentioned in [Vaadin-on-Kotlin Forms](http://www.vaadinonkotlin.eu/forms.html) documentation.\n\nFor example:\n```kotlin\ndata class Person(\n        override var id: Long? = null,\n\n        @field:NotNull\n        @field:Size(min = 1, max = 200)\n        var name: String? = null,\n\n        @field:NotNull\n        @field:Min(15)\n        @field:Max(100)\n        var age: Int? = null) : KEntity\u003cLong\u003e\nval p = Person(name = \"John\", age = 10)\np.validate() // throws an exception since age must be at least 15\n```\n\n*Important note:* the validation is an optional feature in `vok-orm`, and by default\nthe validation is disabled. This fact is advertised in the `vok-orm` logs as the following message:\n\n\u003e JSR 303 Validator Provider was not found on your classpath, disabling entity validation\n\nIn order to activate the entity validations, you need to add a JSR303 Validation Provider jar\nto your classpath. Just use Hibernate-Validator (don't worry it will not pull in Hibernate nor\nJPA) and add this to your `build.gradle`:\n\n```groovy\ndependencies {\n  compile(\"org.hibernate.validator:hibernate-validator:6.0.17.Final\")\n  // EL is required: http://hibernate.org/validator/documentation/getting-started/\n  compile(\"org.glassfish:javax.el:3.0.1-b08\")\n}\n```\n\nYou can check out the [vok-orm-playground](https://gitlab.com/mvysny/vok-orm-playground) which\nhas validations enabled and all necessary jars included.\n\n## Data Loaders\n\nThe support for [Data Loader](https://gitlab.com/mvysny/vok-dataloader) is deprecated and removed.\n\n## Vaadin\n\nFor Vaadin integration please see [jdbi-orm-vaadin](https://gitlab.com/mvysny/jdbi-orm-vaadin)\nwhich supports vok-orm too, and it provides support for all sorts of data providers including\nentity, POJO and joins/custom SQL statements.\n\nFor Vaadin example apps, please take a look at:\n\n* [beverage-buddy-vok](https://github.com/mvysny/beverage-buddy-vok): A full-blown Vaadin Kotlin app which demoes vok-orm-based CRUD\n* [vok-security-demo](https://github.com/mvysny/vok-security-demo): uses vok-orm to load users\n* [vaadin-kotlin-pwa](https://github.com/mvysny/vaadin-kotlin-pwa): demoes Vaadin+Kotlin+CRUD as well\n\n## Full-Text Filters\n\nIn order for the `FullTextFilter` filter to work, you must create a proper full-text index\nin your database for the column being matched. Please see the documentation for\nindividual databases below.\n\nTo customize the SQL scripts being generated, you can create a delegate\n`FilterToSqlConverter` which is able to handle `FullTextFilter`s and passes through\nall other filters to the default `VokOrm.filterToSqlConverter`.\n\nThe `FullTextFilter` class cleans up the user input, removes any non-alphabetic and non-digit\ncharacters and turns user input into a set of words. The words will never contain\ncharacters such as `+` `/` `*` which are often used by the full text engine.\nTherefore, it's easy to join the words and produce a full-text query.\n\nIn order for the Filter converter to know which syntax to produce, you must set\nyour database variant in `VokOrm.databaseVariant`. The default one is 'Unknown'\nand since there is no full-text matching in SQL92, by default all `FullTextFilter`\nconversion will fail.\n\n### H2\n\nFull-Text searches are supported. vok-orm uses FullTextLucene implementation since\nthe native H2 implementation can't do partial matches (e.g. filter `car` won't match `carousel`).\n\nThe following WHERE clauses are produced by default:\n\n```sql\n$idColumn IN (SELECT CAST(FT.KEYS[1] AS BIGINT) AS ID FROM FTL_SEARCH_DATA(:$parameterName, 0, 0) FT WHERE FT.`TABLE`='${meta.databaseTableName.toUpperCase()}')\n```\n\nYou need to call the following to init the engine and create the index:\n\n```sql\nCREATE ALIAS IF NOT EXISTS FTL_INIT FOR \"org.h2.fulltext.FullTextLucene.init\";\nCALL FTL_INIT();\nCALL FTL_CREATE_INDEX('PUBLIC', 'TEST', 'NAME');  -- Adds index on the 'NAME' column of the 'TEST' table.\n```\n\nMake sure to use upper-case table+column names otherwise H2 will complain that the column/table doesn't exist.\n\nYou will need to add Lucene on the classpath:\n\n```gradle\n    compile(\"org.apache.lucene:lucene-analyzers-common:5.5.5\")\n    compile(\"org.apache.lucene:lucene-queryparser:5.5.5\")\n```\n\nLimitations:\n\n* Only tables with non-composite `BIGINT` primary keys are supported. You can\n  lift this limitation by implementing your own `FilterToSqlConverter` and constructing\n  your own query.\n\nSee [H2 Full-Text search](https://www.h2database.com/html/tutorial.html#fulltext) for\nmore info.\n\n### PostgreSQL\n\nThe following WHERE clauses are produced by default:\n`to_tsvector('english', $databaseColumnName) @@ to_tsquery('english', 'fat:* cat:*')`.\n\nA full-text index is not necessary in order for PostgreSQL to properly match records,\nhowever the performance will be horrible. I recommend to create the index, e.g.\n`CREATE INDEX pgweb_idx ON Test USING GIN (to_tsvector('english', name))`.\n\nSee [PostgreSQL Full-Text search](https://www.postgresql.org/docs/9.5/textsearch-tables.html#TEXTSEARCH-TABLES-INDEX)\nfor more info.\n\n### MySQL/MariaDB\n\nYou'll need to create a [FULLTEXT index](https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html)\nfor the column, otherwise MySQL will match nothing.\n\nBy default the following WHERE clauses are produced: e.g. when searching for \"fat cat\",\nthis is emitted: `MATCH($databaseColumnName) AGAINST (\"+fat* +cat*\" IN BOOLEAN MODE)`.\n\nMySQL has a number of quirks to look after:\n\n* Sometimes MySQL will use another index instead of a full-text index: [MySQL sporadic MATCH AGAINST behaviour with unique index](https://stackoverflow.com/questions/45281641/mysql-sporadic-match-against-behaviour-with-unique-index)\n  Either delete the offending index, or use [NativeFilter] and `IGNORE INDEX ()`\n\nWhy the filter is using BOOLEAN mode instead of NATURAL LANGUAGE mode:\n\n* Random treating of words as stopwords because they're present in more than 50% of the rows: [MySQL Natural Language](https://dev.mysql.com/doc/refman/5.5/en/fulltext-natural-language.html).\n* No way to match word beginnings.\n\n## Aliases\n\nOften database columns follow different naming convention than bean fields, e.g. database `CUSTOMER_NAME` should be mapped to the\n`CustomerAddress::customerName` field. The first thing to try is to use aliases in the SQL itself, for example\n```sql\nselect c.CUSTOMER_NAME as customerName from Customer c ...;\n```\n\nThe problem with this approach is twofold:\n\n* Databases can't sort nor filter based on aliased column;\n  please see [Issue 5](https://github.com/mvysny/vok-orm/issues/5) for more details.\n  Using such queries with `SqlDataLoader` and trying to pass in filter such as `buildFilter\u003cCustomerAddress\u003e { \"customerName ILIKE cn\"(\"cn\" to \"Foo%\") }` will cause\n  the select command to fail with `SqlException`.\n* INSERTs/UPDATEs issued by your entity `Dao` will fail since they will use the bean field names instead of actual column name\n  and will emit `INSERT INTO Customer (customerName) values ($1)` instead of `INSERT INTO Customer (CUSTOMER_NAME) values ($1)` \n\nTherefore, instead of database-based aliases it's better to use the `@ColumnName` annotation on your beans, both natural entities\nsuch as `Customer` and projection-only entities such as `CustomerAddress`:\n\n```kotlin\ndata class Customer(@field:ColumnName(\"CUSTOMER_NAME\") var name: String? = null) : KEntity\u003cLong\u003e\ndata class CustomerAddress(@field:ColumnName(\"CUSTOMER_NAME\") var customerName: String? = null)\n```\n\nThe `@ColumnName` annotation is honored both by `Dao`s and by all data loaders.\n\n## A main() method Example\n\nUsing the vok-orm library from a JavaSE main method;\nsee the [vok-orm-playground](https://gitlab.com/mvysny/vok-orm-playground) for a very simple example project\nusing `vok-orm`.\n\n```kotlin\ndata class Person(\n    override var id: Long? = null,\n    var name: String = \"\",\n    var age: Int = 0,\n    var dateOfBirth: LocalDate? = null,\n    var recordCreatedAt: Instant? = null\n) : KEntity\u003cLong\u003e {\n    override fun save(validate: Boolean) {\n        if (id == null) {\n            recordCreatedAt = Instant.now()\n        }\n        super.save(validate)\n    }\n\n    companion object : Dao\u003cPerson, Long\u003e(Person::class.java)\n}\n\nfun main(args: Array\u003cString\u003e) {\n    val cfg = HikariConfig().apply {\n        jdbcUrl = \"jdbc:h2:mem:test;DB_CLOSE_DELAY=-1\"\n    }\n    JdbiOrm.setDataSource(HikariDataSource(cfg))\n    db {\n        con.createQuery(\n            \"\"\"create table if not exists Test (\n            id bigint primary key auto_increment,\n            name varchar not null,\n            age integer not null,\n            dateOfBirth date,\n            recordCreatedAt timestamp\n             )\"\"\"\n        ).executeUpdate()\n    }\n    \n    // runs SELECT * FROM Person\n    // prints []\n    println(Person.findAll())\n    \n    // runs INSERT INTO Person (name, age, recordCreatedAt) values (:p1, :p2, :p3)\n    Person(name = \"John\", age = 42).save()\n    \n    // runs SELECT * FROM Person\n    // prints [Person(id=1, name=John, age=42, dateOfBirth=null, recordCreatedAt=2011-12-03T10:15:30Z)]\n    println(Person.findAll())\n    \n    // runs SELECT * FROM Person where id=:id\n    // prints John\n    println(Person.getById(1L).name)\n    \n    // mass-saves 11 persons in a single transaction.\n    db { (0..10).forEach { Person(name = \"person $it\", age = it).save() } }\n    \n    JdbiOrm.destroy()\n}\n```\n\n# Using Flyway to migrate the database\n\n[Flyway](https://flywaydb.org/) is able to run DDL scripts on given database and track which scripts already ran.\nThis way, you can simply add more scripts and Flyway will apply them, to migrate the database to the newest version.\nThis even works in a cluster since Flyway will obtain a database lock, locking out other members of the cluster attempting\nto upgrade.\n\nLet's use the Category example from above. We need Flyway to run two scripts, to initialize the database:\none creates the table, while other creates the indices.\n\nYou don't need to use Flyway plugin. Just add the following Gradle dependency to your project:\n\n```gradle\ncompile \"org.flywaydb:flyway-core:6.0.7\"\n```\n\nFlyway expects the migration scripts named in a certain format, to know the order in which to execute them.\nCreate the `db.migration` package in your `src/main/resources` and put two files there: the `V01__CreateCategory.sql`\nfile:\n```sql92\ncreate TABLE CATEGORY (\n  id bigint auto_increment PRIMARY KEY,\n  name varchar(200) NOT NULL\n);\n```\nThe next one will be `V02__CreateIndexCategoryName.sql`:\n```sql92\ncreate UNIQUE INDEX idx_category_name ON CATEGORY(name);\n```\n\nIn order to run the migrations, just run the following after `JdbiOrm.setDataSource()`:\n```kotlin\nval flyway = Flyway()\nflyway.dataSource = JdbiOrm.getDataSource()\nflyway.migrate()\n```\n\n# Using with Spring or JavaEE\n\nBy default VoK-ORM connects to the JDBC database directly and uses its own instance of\nHikari-CP to pool JDBC connections. That of course doesn't work with containers such as Spring or\nJavaEE which manage JDBC resources themselves.\n\nIt is very easy to use VoK-ORM with Spring or JavaEE. All you need is to obtain\nan instance of `DataSource` when your server boots up, then simply set it to\nJdbiOrm via `JdbiOrm.setDataSource()`. VoK-ORM will then simply poll Spring or JavaEE\nDataSource for connections; Spring/JavaEE will then make sure the connections are pooled properly.\n\nYou don't even need to call `JdbiOrm.destroy()` on Spring/JavaEE app shutdown:\nall `JdbiOrm.destroy()` does is that it closes the `DataSource`, however Spring/JavaEE\nwill do that for us.\n\n# `vok-orm` design principles\n\n`vok-orm` is a very simple object-relational mapping library, built around the following ideas:\n\n* Simplicity is the most valued property; working with plain SQL commands is preferred over having a type-safe\n  query language. If you want a type-safe database mapping library, try [Exposed](https://github.com/JetBrains/Exposed).\n* The database is the source of truth. JVM objects are nothing more than DTOs,\n  merely capture snapshots of the JDBC `ResultSet` rows. The entities are populated by the\n  means of reflection: for every column in\n  the JDBC `ResultSet` an appropriate setter is invoked, to populate the data.\n* The entities are real POJOs: they do not track modifications, they do not automatically store modified\n  values back into the database. They are not runtime-enhanced and can be final.\n* A switch from one type of database to another never happens. We understand that the programmer\n  wants to exploit the full potential of the database, by writing SQLs tailored for that particular database.\n  `vok-orm` should not attempt to generate SELECTs on behalf of the programmer (except for the very basic ones related to CRUD);\n  instead it should simply allow SELECTs to be passed as Strings, and then map the result\n  to an object of programmer's choosing.\n\nAs such, `vok-orm` has much in common with the [ActiveJDBC](https://github.com/javalite/activejdbc) project, in terms\nof design principles. The advantage of `vok-orm` is that it doesn't require any instrumentation to work\n(instead it uses Kotlin language features), and it's even simpler than ActiveJDBC.\n\nPlease read [Back to Base - make SQL great again](http://mavi.logdown.com/posts/5771422)\nfor the complete explanation of ideas behind this framework.\n\nThis framework uses [JDBI](http://jdbi.org/) to map data from the JDBC `ResultSet` to POJOs; in addition it provides a very simple\nmechanism to store/update the data back to the database.\n\n## Comparison with other database-related libraries\n\n* [ActiveJDBC](https://javalite.io/activejdbc) has much in common with jdbi-orm; the advantage of jdbi-orm\n  is that we do not require any instrumentation to work (we use only Java language features).\n* [JOOQ](https://www.jooq.org/) is great but requires initial generation of java code from your database scheme\n  (you write your entities by hand with jdbi-orm), and promotes type-safe query building instead of plain SQLs.\n  There's the usual set of problems coming with generated classes: you can't add your custom utility functions to those,\n  you can't add validation annotations, etc.\n  If you don't mind that, go for JOOQ - it's definitely more popular than jdbi-orm.\n* JPA: just no. We want real POJOs, not a dynamically-enhanced thing managed by the Entity Manager. Also see below.\n* Spring JdbcTemplate: not bad but it depends on Spring; jdbi-orm must be able to work on pure JVM, without Spring.\n\n## Why not JPA\n\nJPA promises simplicity of usage by providing an object-oriented API. However, this is achieved by\ncreating a *virtual object database* layer over a relational database; that creates much complexity\nunder the hood which leaks in various ways. In short, JPA is a double failure: it chose the wrong abstraction,\nand implemented it poorly.\n\nThere are major issues in JPA which cannot be overlooked:\n\n* [Vaadin-on-Kotlin Issue #3 Remove JPA](https://github.com/mvysny/vaadin-on-kotlin/issues/3)\n* [Back to Base - make SQL great again](http://mavi.logdown.com/posts/5771422)\n* [Do-It-Yourself ORM as an Alternative to Hibernate](https://blog.philipphauer.de/do-it-yourself-orm-alternative-hibernate-drawbacks/)\n\nWe strive to erase the virtual object database layer. We acknowledge the existence of\nthe relational database; we only provide tools to ease the use of the database from a\nstatically-typed OOP language.\n\n## Running tests\n\nRunning `./gradlew` or `./gradlew test` will run all tests on all databases (given that\nDocker is available on the host system). To run the tests on H2 only\n(the test suite will run much faster), run with `./gradlew -Dh2only=true`\n\n# License\n\nLicensed under the [MIT License](https://opensource.org/licenses/MIT).\n\nCopyright (c) 2017-2018 Martin Vysny\n\nAll rights reserved.\n\nPermission is hereby granted, free  of charge, to any person obtaining\na  copy  of this  software  and  associated  documentation files  (the\n\"Software\"), to  deal in  the Software without  restriction, including\nwithout limitation  the rights to  use, copy, modify,  merge, publish,\ndistribute,  sublicense, and/or sell  copies of  the Software,  and to\npermit persons to whom the Software  is furnished to do so, subject to\nthe following conditions:\n\nThe  above  copyright  notice  and  this permission  notice  shall  be\nincluded in all copies or substantial portions of the Software.\nTHE  SOFTWARE IS  PROVIDED  \"AS  IS\", WITHOUT  WARRANTY  OF ANY  KIND,\nEXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF\nMERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmvysny%2Fvok-orm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmvysny%2Fvok-orm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmvysny%2Fvok-orm/lists"}