github kotlin-orm/ktorm v4.0.0

one month ago

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.schema", "test")
    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.schema=test</option>
                            <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.schema=test</option>
                            <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 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.date
import org.ktorm.schema.int
import org.ktorm.schema.long
import org.ktorm.schema.varchar
import java.time.LocalDate
import kotlin.Int
import kotlin.Long
import kotlin.String
import kotlin.Suppress
import kotlin.collections.List

/**
 * Table t_employee.
 */
public open class Employees(alias: String?) : Table<Employee>("t_employee", 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 job.
     */
    public val job: Column<String> = varchar("job").bindTo { it.job }

    /**
     * Column manager_id.
     */
    public val managerId: Column<Int> = int("manager_id").bindTo { it.managerId }

    /**
     * Column hire_date.
     */
    public val hireDate: Column<LocalDate> = date("hire_date").bindTo { it.hireDate }

    /**
     * Column salary.
     */
    public val salary: Column<Long> = long("salary").bindTo { it.salary }

    /**
     * Column department_id.
     */
    public val departmentId: Column<Int> = int("department_id").references(Departments) { it.department }

    /**
     * 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): Employees = Employees(alias)

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

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

/**
 * Return the refs object that provides a convenient way to access referenced tables.
 */
public val Employees.refs: EmployeesRefs get() = EmployeesRefs(this)

/**
 * Wrapper class that provides a convenient way to access referenced tables.
 */
public class EmployeesRefs(t: Employees) {
    /**
     * Return the referenced table of [Employees.departmentId].
     */
    public val department: Departments = t.departmentId.referenceTable as Departments

    /**
     * Return all referenced tables as a list.
     */
    public fun toList(): List<Table<*>> = listOf(department)
}

/**
 * Create an entity of [Employee] and specify the initial values for each property, properties that
 * doesn't have an initial value will leave unassigned.
 */
public fun Employee(
    id: Int? = Undefined.of(),
    name: String? = Undefined.of(),
    job: String? = Undefined.of(),
    managerId: Int? = Undefined.of(),
    hireDate: LocalDate? = Undefined.of(),
    salary: Long? = Undefined.of(),
    department: Department? = Undefined.of()
): Employee {
    val entity = Entity.create<Employee>()
    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 (job !== Undefined.of<String>()) {
        entity.job = job ?: error("`job` should not be null.")
    }
    if (managerId !== Undefined.of<Int>()) {
        entity.managerId = managerId
    }
    if (hireDate !== Undefined.of<LocalDate>()) {
        entity.hireDate = hireDate ?: error("`hireDate` should not be null.")
    }
    if (salary !== Undefined.of<Long>()) {
        entity.salary = salary ?: error("`salary` should not be null.")
    }
    if (department !== Undefined.of<Department>()) {
        entity.department = department ?: error("`department` 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 Employee.copy(
    id: Int? = Undefined.of(),
    name: String? = Undefined.of(),
    job: String? = Undefined.of(),
    managerId: Int? = Undefined.of(),
    hireDate: LocalDate? = Undefined.of(),
    salary: Long? = Undefined.of(),
    department: Department? = Undefined.of()
): Employee {
    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 (job !== Undefined.of<String>()) {
        entity.job = job ?: error("`job` should not be null.")
    }
    if (managerId !== Undefined.of<Int>()) {
        entity.managerId = managerId
    }
    if (hireDate !== Undefined.of<LocalDate>()) {
        entity.hireDate = hireDate ?: error("`hireDate` should not be null.")
    }
    if (salary !== Undefined.of<Long>()) {
        entity.salary = salary ?: error("`salary` should not be null.")
    }
    if (department !== Undefined.of<Department>()) {
        entity.department = department ?: error("`department` should not be null.")
    }
    return entity
}

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

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

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

/**
 * Return the value of [Employee.managerId].
 */
public operator fun Employee.component4(): Int? = this.managerId

/**
 * Return the value of [Employee.hireDate].
 */
public operator fun Employee.component5(): LocalDate = this.hireDate

/**
 * Return the value of [Employee.salary].
 */
public operator fun Employee.component6(): Long = this.salary

/**
 * Return the value of [Employee.department].
 */
public operator fun Employee.component7(): Department = this.department

Users.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.dsl.AliasRemover
import org.ktorm.dsl.QueryRowSet
import org.ktorm.dsl.eq
import org.ktorm.dsl.getGeneratedKey
import org.ktorm.entity.EntitySequence
import org.ktorm.entity.sequenceOf
import org.ktorm.expression.ColumnAssignmentExpression
import org.ktorm.expression.InsertExpression
import org.ktorm.expression.UpdateExpression
import org.ktorm.schema.BaseTable
import org.ktorm.schema.Column
import org.ktorm.schema.int
import org.ktorm.schema.varchar
import kotlin.Boolean
import kotlin.Int
import kotlin.String
import kotlin.Suppress

/**
 * Table user.
 */
public open class Users(alias: String?) : BaseTable<User>("user", alias, schema = "test") {
    /**
     * Column id.
     */
    public val id: Column<Int> = int("id").primaryKey()

    /**
     * Column username.
     */
    public val username: Column<String> = varchar("username")

    /**
     * Column age.
     */
    public val age: Column<Int> = int("age")

    /**
     * Create an entity object from the specific row of query results.
     */
    public override fun doCreateEntity(
        row: QueryRowSet,
        withReferences: Boolean
    ): User = User(id = row[this.id]!!, username = row[this.username]!!, age = row[this.age]!!)

    /**
     * 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): Users = Users(alias)

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

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

/**
 * Insert the given entity into the table that the sequence object represents.
 *
 * @param entity the entity to be inserted.
 * @param isDynamic whether only non-null columns should be inserted.
 * @param useGeneratedKey whether to obtain the generated primary key value and fill it into the
 * property [User.id] after insertion.
 * @return the affected record number.
 */
public fun EntitySequence<User, Users>.add(
    entity: User,
    isDynamic: Boolean = false,
    useGeneratedKey: Boolean = false
): Int {
    val isModified = expression.where != null ||
        expression.groupBy.isNotEmpty() ||
        expression.having != null ||
        expression.isDistinct ||
        expression.orderBy.isNotEmpty() ||
        expression.offset != null ||
        expression.limit != null
    if (isModified) {
        val msg = "" +
            "Entity manipulation functions are not supported by this sequence object. " +
            "Please call on the origin sequence returned from database.sequenceOf(table)"
        throw UnsupportedOperationException(msg)
    }

    fun <T : Any> MutableList<ColumnAssignmentExpression<*>>.addVal(column: Column<T>, value: T?) {
        if (useGeneratedKey && column === sourceTable.id) {
            return
        }

        if (isDynamic && value == null) {
            return
        }

        this += ColumnAssignmentExpression(column.asExpression(), column.wrapArgument(value))
    }

    val assignments = ArrayList<ColumnAssignmentExpression<*>>()
    assignments.addVal(sourceTable.id, entity.id)
    assignments.addVal(sourceTable.username, entity.username)
    assignments.addVal(sourceTable.age, entity.age)
    if (assignments.isEmpty()) {
        return 0
    }

    val visitor = database.dialect.createExpressionVisitor(AliasRemover)
    val expression = visitor.visit(InsertExpression(sourceTable.asExpression(), assignments))
    if (!useGeneratedKey) {
        return database.executeUpdate(expression)
    } else {
        val (effects, rowSet) = database.executeUpdateAndRetrieveKeys(expression)
        if (rowSet.next()) {
            val generatedKey = rowSet.getGeneratedKey(sourceTable.id)
            if (generatedKey != null) {
                if (database.logger.isDebugEnabled()) {
                    database.logger.debug("Generated Key: $generatedKey")
                }

                entity.id = generatedKey
            }
        }

        return effects
    }
}

/**
 * Update the given entity to the database.
 *
 * @param entity the entity to be updated.
 * @param isDynamic whether only non-null columns should be updated.
 * @return the affected record number.
 */
public fun EntitySequence<User, Users>.update(entity: User, isDynamic: Boolean = false): Int {
    val isModified = expression.where != null ||
        expression.groupBy.isNotEmpty() ||
        expression.having != null ||
        expression.isDistinct ||
        expression.orderBy.isNotEmpty() ||
        expression.offset != null ||
        expression.limit != null
    if (isModified) {
        val msg = "" +
            "Entity manipulation functions are not supported by this sequence object. " +
            "Please call on the origin sequence returned from database.sequenceOf(table)"
        throw UnsupportedOperationException(msg)
    }

    fun <T : Any> MutableList<ColumnAssignmentExpression<*>>.addVal(column: Column<T>, value: T?) {
        if (isDynamic && value == null) {
            return
        }

        this += ColumnAssignmentExpression(column.asExpression(), column.wrapArgument(value))
    }

    val assignments = ArrayList<ColumnAssignmentExpression<*>>()
    assignments.addVal(sourceTable.username, entity.username)
    assignments.addVal(sourceTable.age, entity.age)
    if (assignments.isEmpty()) {
        return 0
    }

    val visitor = database.dialect.createExpressionVisitor(AliasRemover)
    val conditions = (sourceTable.id eq entity.id)
    val expression = visitor.visit(UpdateExpression(sourceTable.asExpression(), assignments, conditions))
    return database.executeUpdate(expression)
}

Don't miss a new ktorm release

NewReleases is sending notifications on new releases.