Skip to content

Releases: kotlin-orm/ktorm

v4.1.1

03 Sep 03:39
a1c008d
Compare
Choose a tag to compare

Fix bug that KSP generates non-compile code when entity property names are too long.

v4.1.0

17 Jul 03:24
f258717
Compare
Choose a tag to compare

What's Changed

  • Upgrade Kotlin version to 1.9.23, support JDK 21.
  • Add EntityExtensionsApi.isAttached() function to check if an entity is attached to the database.
  • Track and provide access to entity changed properties by @HC-224 in #525
  • Make BaseTable.asExpression() function open by @qumn in #560
  • Reduce memory allocation on Column.label used for accessing in query by @AlexRiedler in #564
  • Bugfix: fix binary compatibility error when using a higher version of KotlinModule by @k163377 in #527
  • Bugfix: Entity.flushChanges() and Entity.delete() throw SQLException instead of UndeclaredThrowableException when SQL execution fails.
  • Update documentation for insertOrUpdate and bulkInsertOrUpdate by @ipalo in #501

New Contributors

Full Changelog: v4.0.0...v4.1.0

v4.0.0

13 May 13:15
Compare
Choose a tag to compare

What's Changed

Ktorm KSP Quick Start

Ktorm KSP can help us generate boilerplate code for table schemas. To use Ktorm KSP, you need to apply our plugin to your project first.

For Gradle users:

plugins {
    kotlin("jvm") version "1.9.0"
    id("com.google.devtools.ksp") version "1.9.0-1.0.13"
}

dependencies {
    implementation(kotlin("stdlib"))
    implementation(kotlin("reflect"))
    implementation("org.ktorm:ktorm-core:${ktorm.version}")
    implementation("org.ktorm:ktorm-support-mysql:${ktorm.version}")
    implementation("org.ktorm:ktorm-ksp-annotations:${ktorm.version}")
    ksp("org.ktorm:ktorm-ksp-compiler:${ktorm.version}")
}

ksp {
    arg("ktorm.dbNamingStrategy", "lower-snake-case")
}

For Maven users:

<dependencies>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-stdlib</artifactId>
        <version>${kotlin.version}</version>
    </dependency>
    <dependency>
        <groupId>org.jetbrains.kotlin</groupId>
        <artifactId>kotlin-reflect</artifactId>
        <version>${kotlin.version}</version>
    </dependency>
    <dependency>
        <groupId>org.ktorm</groupId>
        <artifactId>ktorm-core</artifactId>
        <version>${ktorm.version}</version>
    </dependency>
    <dependency>
        <groupId>org.ktorm</groupId>
        <artifactId>ktorm-support-mysql</artifactId>
        <version>${ktorm.version}</version>
    </dependency>
    <dependency>
        <groupId>org.ktorm</groupId>
        <artifactId>ktorm-ksp-annotations</artifactId>
        <version>${ktorm.version}</version>
    </dependency>
</dependencies>

<build>
    <sourceDirectory>src/main/kotlin</sourceDirectory>
    <testSourceDirectory>src/test/kotlin</testSourceDirectory>
    <plugins>
        <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <version>${kotlin.version}</version>
            <executions>
                <execution>
                    <id>compile</id>
                    <phase>compile</phase>
                    <goals>
                        <goal>compile</goal>
                    </goals>
                    <configuration>
                        <compilerPlugins>
                            <compilerPlugin>ksp</compilerPlugin>
                        </compilerPlugins>
                        <pluginOptions>
                            <option>ksp:apoption=ktorm.dbNamingStrategy=lower-snake-case</option>
                        </pluginOptions>
                    </configuration>
                </execution>
                <execution>
                    <id>test-compile</id>
                    <phase>test-compile</phase>
                    <goals>
                        <goal>test-compile</goal>
                    </goals>
                    <configuration>
                        <compilerPlugins>
                            <compilerPlugin>ksp</compilerPlugin>
                        </compilerPlugins>
                        <pluginOptions>
                            <option>ksp:apoption=ktorm.dbNamingStrategy=lower-snake-case</option>
                        </pluginOptions>
                    </configuration>
                </execution>
            </executions>
            <dependencies>
                <dependency>
                    <groupId>org.ktorm</groupId>
                    <artifactId>ktorm-ksp-compiler-maven-plugin</artifactId>
                    <version>${ktorm.version}</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

Now mark your entity classes with annotation @Table:

@Table("t_department")
interface Department : Entity<Department> {
    @PrimaryKey
    var id: Int
    var name: String
    var location: String
}

@Table("t_employee")
interface Employee : Entity<Employee> {
    @PrimaryKey
    var id: Int
    var name: String
    var job: String
    var managerId: Int?
    var hireDate: LocalDate
    var salary: Long
    @References
    var department: Department
}

Compile the project with ./gradlew assemble or mvn compile to trigger code generation, then you can use entity API like below, without defining table schemas and column bindings manually:

// Create the entity just like there is a constructor.
// Actually Employee() is a function generated by Ktorm KSP.
val employee = Employee(name = "vince", job = "engineer")
database.employees.add(employee)

// Database.employees is an extension property generated by Ktorm KSP returning a default entity sequence of Employee.
// You can also use it.refs to access the referenced table in filter conditions, the generated SQL will be like:
//   select *
//   from t_employee
//   left join t_department _ref0 on t_employee.department_id = _ref0.id
//   where _ref0.name = ? and _ref0.location = ?
val employees = database.employees
    .filter { it.refs.department.name eq "tech" }
    .filter { it.refs.department.location eq "Guangzhou" }
    .toList()

You can also use data classes as entities. In this case, @Table annotation is required, too:

@Table
data class User(
    @PrimaryKey
    var id: Int,
    var username: String,
    var age: Int
)

// Use the generated add function to insert a new row into the table.
// Optional argument useGeneratedKey = true means the user ID will be generated by the database.
val user = User(id = 0, username = "vince", age = 28)
database.users.add(user, useGeneratedKey = true)

// Database.users is an extension property generated by Ktorm KSP.
// Use the entity sequence API to fetch all users under 28 from the table.
val users = database.users.filter { it.age lte 28 }.toList()

Generated Code

Ktorm KSP generates .kt files for each entity class. The generated files are located in the project's build directory. For the examples above, the generated files are listed as follows.

Departments.kt

// Auto-generated by ktorm-ksp-compiler, DO NOT EDIT!
@file:Suppress("RedundantVisibilityModifier")

package org.ktorm.example.model

import org.ktorm.database.Database
import org.ktorm.entity.Entity
import org.ktorm.entity.EntitySequence
import org.ktorm.entity.sequenceOf
import org.ktorm.ksp.annotation.Undefined
import org.ktorm.schema.Column
import org.ktorm.schema.Table
import org.ktorm.schema.int
import org.ktorm.schema.varchar
import kotlin.Int
import kotlin.String
import kotlin.Suppress

/**
 * Table t_department.
 */
public open class Departments(alias: String?) : Table<Department>("t_department", alias, schema = "test") {
    /**
     * Column id.
     */
    public val id: Column<Int> = int("id").primaryKey().bindTo { it.id }

    /**
     * Column name.
     */
    public val name: Column<String> = varchar("name").bindTo { it.name }

    /**
     * Column location.
     */
    public val location: Column<String> = varchar("location").bindTo { it.location }

    /**
     * Return a new-created table object with all properties (including the table name and columns and
     * so on) being copied from this table, but applying a new alias given by the parameter.
     */
    public override fun aliased(alias: String): Departments = Departments(alias)

    /**
     * The default table object of t_department.
     */
    public companion object : Departments(alias = null)
}

/**
 * Return the default entity sequence of [Departments].
 */
public val Database.departments: EntitySequence<Department, Departments> get() = this.sequenceOf(Departments)

/**
 * Create an entity of [Department] and specify the initial values for each property, properties
 * that doesn't have an initial value will leave unassigned.
 */
public fun Department(
    id: Int? = Undefined.of(),
    name: String? = Undefined.of(),
    location: String? = Undefined.of()
): Department {
    val entity = Entity.create<Department>()
    if (id !== Undefined.of<Int>()) {
        entity["id"] = id ?: error("`id` should not be null.")
    }
    if (name !== Undefined.of<String>()) {
        entity.name = name ?: error("`name` should not be null.")
    }
    if (location !== Undefined.of<String>()) {
        entity.location = location ?: error("`location` should not be null.")
    }
    return entity
}

/**
 * Return a deep copy of this entity (which has the same property values and tracked statuses), and
 * alter the specified property values.
 */
public fun Department.copy(
    id: Int? = Undefined.of(),
    name: String? = Undefined.of(),
    location: String? = Undefined.of()
): Department {
    val entity = this.copy()
    if (id !== Undefined.of<Int>()) {
        entity["id"] = id ?: error("`id` should not be null.")
    }
    if (name !== Undefined.of<String>()) {
        entity.name = name ?: error("`name` should not be null.")
    }
    if (location !== Undefined.of<String>()) {
        entity.location = location ?: error("`location` should not be null.")
    }
    return entity
}

/**
 * Return the value of [Department.id].
 */
public operator fun Department.component1(): Int = this.id

/**
 * Return the value of [Department.name].
 */
public operator fun Department.component2(): String = this.name

/**
 * Return the value of [Department.location].
 */
public operator fun Department.component3(): String = this.location

Employees.kt

// Auto-generated by ...
Read more

v3.6.0

29 Jan 13:50
Compare
Choose a tag to compare

Infrastructure

  • Upgrade Kotlin version to 1.7.22, support latest JDK versions (min 8, max 19)
  • Support Java 9 modular #421

Support SQL Window Functions, by @michaelfyc in #460

Ktorm now supports SQL window functions, here is an example:

database
    .from(Employees)
    .select(
        Employees.name,
        Employees.salary,
        Employees.departmentId,
        rank().over { partitionBy(Employees.departmentId).orderBy(Employees.salary.desc()) }
    )

Generated SQL:

SELECT 
    "t_employee"."name" AS "t_employee_name", 
    "t_employee"."salary" AS "t_employee_salary", 
    "t_employee"."department_id" AS "t_employee_department_id", 
    RANK() OVER (PARTITION BY "t_employee"."department_id" ORDER BY "t_employee"."salary" DESC) 
FROM "t_employee" 

Support Case-When DSL, by @zuisong in #413

Ktorm now supports case-when DSL, for example:

database
    .from(Employees)
    .select(
        Employees.id,
        Employees.name,
        CASE(Employees.sex).WHEN(1).THEN("male").WHEN(2).THEN("female").ELSE("unknown").END()
    )

Generated SQL:

SELECT 
    "t_employee"."id" AS "t_employee_id", 
    "t_employee"."name" AS "t_employee_name", 
    CASE "t_employee"."sex" WHEN 1 THEN 'male' WHEN 2 THEN 'female' ELSE 'unknown' END 
FROM "t_employee" 

Other Optimizations & Bug Fixes

  • Support insert ... returning ... for SQLite, by @2938137849 in #427
  • Support SQL full join, by @KocproZ in #419
  • Support MySQL & PostgreSQL default keyword for insert statements to use column default values, by @lookup-cat in #431
  • Support SQL type casting syntax, by @svenallers in #415
  • Rename Query#totalRecords to Query#totalRecordsInAllPages for better understandability, the origin name is now deprecated
  • Disable entity sequence for tables that doesn’t bound to entities, legacy usages will get a warning after 3.6.0
  • DefaultMethodHandler to use the same class loader as the method's declaring class, by @brohacz in #471
  • Refactor combineConditions to avoid stack overflow errors #328
  • Fix SQLite syntax error for UNIONs with subqueries #329
  • Fix entity equality bug #466
  • Fix bugs #386, #400, #405, #432, #433, #457

Break Changes

  • Refactor SqlExpressionVisitor and its subtypes from classes to interfaces for better coding flexibility.
  • Change SQL Server datetimeoffset's return type from Column<microsoft.sql.DateTimeOffset> to Column<java.time.OffsetDateTime>

v3.5.0

05 Jun 05:53
6c2bf7c
Compare
Choose a tag to compare
  • Upgrade Kotlin version to 1.5.32, upgrade min JDK version to 1.8.
  • New operator functions gt, gte, lt, lte, neq, synonyms for greater, greaterEq, less, lessEq, notEq.
  • Compatible with JVM default methods for entity non-abstract members.
  • Support using inline classes as column values and entity properties. #253
  • Support PostgreSQL cube & earthdistance data type and their extension functions, by @KocproZ in #365
  • Support SQLite bulk insert statement and some utility functions, by @2938137849 in #370
  • Fix stack overflow bug when using large where clauses, by @ecopoesis in #328
  • Deprecate the ktorm-global module which will be completely removed in the future.

v3.4.1

10 May 17:46
Compare
Choose a tag to compare

Fix PostgreSQL json type mismatch error when using connection pools #268

v3.4.0

09 May 15:03
Compare
Choose a tag to compare

Support Locking Clause for MySQL & PostgreSQL #247

Now Ktorm supports locking clause like for update, for share both for MySQL & PostgreSQL, for example:

val employee = database.employees
    .filter { it.name eq "vince" }
    .locking(LockingMode.FOR_UPDATE, wait = LockingWait.SKIP_LOCKED)
    .firstOrNull()

Generated SQL:

SELECT *
FROM t_employee
WHERE t_employee.name = ? 
LIMIT ?, ? 
FOR UPDATE SKIP LOCKED

Refer to these two functions for detailed usage:

Support insert ... returning ... for PostgreSQL #233

With an insert ... returning ... statement, we can insert records to the database, and at the same time, retrieve some generated columns. For example:

val id = database.insertReturning(Employees, Employees.id) {
    set(it.name, "pedro")
    set(it.job, "engineer")
    set(it.salary, 1500)
    set(it.hireDate, LocalDate.now())
    set(it.departmentId, 1)
}

Returning multiple columns is also supported:

val (id, job) = database.insertReturning(Employees, Pair(Employees.id, Employees.job)) {
    set(it.name, "vince")
    set(it.job, "engineer")
    set(it.salary, 1000)
    set(it.hireDate, LocalDate.now())
    set(it.departmentId, 1)
}

Generated SQL:

insert into t_employee (name, job, salary, hire_date, department_id) 
values (?, ?, ?, ?, ?) returning id, job

There are also some other versions of xxxReturning functions, check the API docs for details:

Other Optimizations & Bug Fixes

  • PostgreSQL: support onConflict { doNothing() } for insertOrUpdate & bulkInsertOrUpdate #255
  • PostgreSQL: Fix type mismatch error for JsonSqlType #268
  • Value semantics for Entity: add default equals & hashCode function #242
  • Auto transformation between JSR-310 classes and JDBC date & time #252
  • Support using unsigned integers as column types #253
  • Fix null-value-ignoring bug for add function #273

v3.3.0

09 Jan 17:50
Compare
Choose a tag to compare
  • sortedBy function supports lambda varargs to allow using multiple sorted columns, eg. sortedBy({ it.salary.desc() }, { it.hireDate.asc() }).
  • Added bulkInsert & bulkInsertOrUpdate for PostgreSQL. #226
  • Added more overloaded functions to allow specifying offset & limit separately. #198
  • Fixed bug of missing catalog & schema in generated insert SQLs. #207, #209
  • Changed the default transaction isolation to null (stands for the default isolation level of the underlying datastore), not REPEATABLE_READ anymore. #231
  • Upgraded Kotlin version to 1.4.21.

v3.2.0

08 Oct 13:30
Compare
Choose a tag to compare

Package Name Changed to org.ktorm

Finally, we have our own domain and the official website becomes https://www.ktorm.org

Accordingly, starting from Ktorm 3.2, we have changed our group ID to org.ktorm, so you need to modify your Maven dependency:

<dependency>
    <groupId>org.ktorm</groupId>
    <artifactId>ktorm-core</artifactId>
    <version>3.2.0</version>
</dependency>

Or Gradle:

compile "org.ktorm:ktorm-core:3.2.0"

Package names are also changed to org.ktorm.*, so you also need to modify your import statements:

import org.ktorm.database.*
import org.ktorm.dsl.*
import org.ktorm.entity.*
import org.ktorm.schema.*

With the only two steps, you have completed all the migration work to version 3.2. Everything should work well as there are no other changes apart from this.

v3.1.0

19 Sep 06:20
Compare
Choose a tag to compare

Upgrade to Kotlin 1.4

To use the new features of Kotlin, we've upgraded its version to 1.4.10, which brings some changes to us:

  • Explicit API mode to ensure we don't expose some internal things accidentally.

  • Contract support for database.useConnection { .. } & database.useTransaction { .. } to let the compiler know our callback functions are called in place exactly once.

  • Deprecation of mapColumnsN & aggregateColumnsN functions. As Kotlin 1.4 supports overload resolution by lambda return type, we don't need these functions anymore, mapColumns & aggregateColumns is enough.

    database.employees
        .filter { it.departmentId eq 1 }
        .mapColumns { tupleOf(it.id, it.name) }
        .forEach { (id, name) ->
            println("$id:$name")
        }

Entity Based Update Function

In Ktorm 3.1, we provide an update function that can update all the non-null properties of an entity object to the database. Using this function, the entity object is not required to be associated with a table first. That means, comparing to flushChanges, we don’t have to obtain an entity object from the database first before performing the update. The usage is as follows:

val employee = Employee {
    id = 5
    job = "engineer"
    salary = 100
}

database.employees.update(employee)

Generated SQL:

update t_employee set job = ?, salary = ? where id = ?

Syntax Refactoring of Insert & Update DSL

Previously, we inserted a record into the table like this:

database.insert(Employees) {
    it.name to "jerry"
    it.job to "trainee"
    it.managerId to 1
    it.hireDate to LocalDate.now()
    it.salary to 50
    it.departmentId to 1
}

Here, we used it.name to "jerry" to set the name to jerry in the closure. And the to function is a member of AssignmentsBuilder, but not the to function used to create Pair instances of Kotlin standard lib.

It is very easy for users to get confused with these two functions, so in Ktorm 3.1, we provide another set function as an alternative. The to function is marked deprecated and will be removed in the future. You can learn the new syntax here https://ktorm.liuwj.me/en/dml.html

database.insert(Employees) {
    set(it.name, "jerry")
    set(it.job, "trainee")
    set(it.managerId, 1)
    set(it.hireDate, LocalDate.now())
    set(it.salary, 50)
    set(it.departmentId, 1)
}

Other Optimizations and Bug Fixes

  • Add ShortSqlType. #160
  • Add MySQL IF function. #163
  • Support mixed-case column names & auto case transform. #175
  • Allow specify PostgreSQL insertOrUpdate conflict columns. #181
  • Support select .. for update. #69
  • Support catalog & schema in table definition. #89, #154, #183
  • Check max column name length. #122
  • Fix timestamp fraction bug. #130
  • Update the syntax of defining JSON columns from json("foo", typeRef<List<Int>>) to json<List<Int>>("foo").