🇺🇸 English | 🇨🇳 简体中文
What's ktorm-ksp?
Ktorm KSP extension to help generate boilerplate code. It can automatically generate Table objects through entity classes, while making entities defined by data classes easier to use, and supports custom extension code generation logic.
Feature
-
Just write the entity class and automatically generate the corresponding Table object. Support classes defined based on the Entity interface, as well as entities defined by ordinary class or data class
-
Better support for class entity classes, the default implementation of the doCreateEntity method, and the add and update method of the entity sequence
-
Extensible code generation logic. Through the SPI mechanism, you only need to implement the specified interface, and you can write your own automatically generated logic.
custom entity ▼
@Table
public data class Student(
@PrimaryKey
public var id: Int?,
public var name: String,
public var age: Int
)
Auto generate code ▼
public open class Students(
alias: String? = null,
) : BaseTable<Student>(tableName = "Student", alias = alias, entityClass = Student::class) {
public val id: Column<Int> = int("id").primaryKey()
public val name: Column<String> = varchar("name")
public val age: Column<Int> = int("age")
public override fun aliased(alias: String): Students = Students(alias)
public override fun doCreateEntity(row: QueryRowSet, withReferences: Boolean): Student {
return Student(
id = row[this.id],
name = row[this.name]!!,
age = row[this.age]!!,
)
}
public companion object : Students()
}
public fun EntitySequence<Student, Students>.add(entity: Student): Int { /*Ignore code*/
}
public fun EntitySequence<Student, Students>.update(entity: Student): Int { /*Ignore code*/
}
public val Database.students: EntitySequence<Student, Students> get() = this.sequenceOf(Students)
Why use class as a table type instead of an object singleton? Please refer to the documentation: Self-Joining-amp-Table-Aliases
- Quick Start
- Define Entities
- Global Configuration
- Naming Style
- Type Converter
- Function And Property Generator
Quick Start
Add a dependency to build.gradle
or pom.xml
file:
// groovy dsl gradle
plugins {
id 'com.google.devtools.ksp' version '1.6.21-1.0.5'
}
dependencies {
implementation 'org.ktorm:ktorm-ksp-api:${ktorm-ksp.version}'
ksp 'org.ktorm:ktorm-ksp-compiler:${ktorm-ksp.version}'
}
// kotlin dsl gradle
plugins {
id("com.google.devtools.ksp").version("1.6.21-1.0.5")
}
dependencies {
implementation("org.ktorm:ktorm-ksp-api:${ktorm-ksp.version}")
ksp("org.ktorm:ktorm-ksp-compiler:${ktorm-ksp.version}")
}
<!-- maven -->
<project>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<compilerPlugin>ksp</compilerPlugin>
</compilerPlugins>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
<sourceDir>target/generated-sources/ksp</sourceDir>
</sourceDirs>
</configuration>
<dependencies>
<dependency>
<groupId>com.dyescape</groupId>
<artifactId>kotlin-maven-symbol-processing</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.ktorm</groupId>
<artifactId>ktorm-ksp-compiler</artifactId>
<version>${ktorm-ksp.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.ktorm</groupId>
<artifactId>ktorm-ksp-api</artifactId>
<version>${ktorm-ksp.version}</version>
</dependency>
</dependencies>
</project>
In order for idea to aware the generated code, you also need to add the following configuration to build.gradle
(otherwise you will see some red line warnings). If you use maven, please ignore this step. Because the relevant
configuration has been added in the previous step.
// Groovy DSL
kotlin {
sourceSets {
main.kotlin.srcDirs += 'build/generated/ksp/main/kotlin'
test.kotlin.srcDirs += 'build/generated/ksp/test/kotlin'
}
}
// Kotlin DSL
kotlin {
sourceSets.main {
kotlin.srcDir("build/generated/ksp/main/kotlin")
}
sourceSets.test {
kotlin.srcDir("build/generated/ksp/test/kotlin")
}
}
How to get ksp to generate code?
- Gradle: build project, running application, execute
gradle build
command. will generate code inbuild/generated/ksp/main/kotlin
directory - Maven: execute
mvn kotlin:compile
will generate code intarget/generated-sources/ksp
directory
Define Entities
Define Entities Of Any Kind Of Class
@Table
public data class Student(
@PrimaryKey
public var id: Int?,
public var name: String,
public var age: Int
)
Generate code:
public open class Students(
alias: String? = null,
) : BaseTable<Student>(tableName = "Student", alias = alias, entityClass = Student::class) {
// Ignore code
}
public fun EntitySequence<Student, Students>.add(entity: Student): Int { /*Ignore code*/
}
public fun EntitySequence<Student, Students>.update(entity: Student): Int { /*Ignore code*/
}
public val Database.students: EntitySequence<Student, Students> get() = this.sequenceOf(Students)
The extended properties of Entity Sequence are generated by default, eliminating the need to manually write boilerplate code for creating entity sequences.
val users = database.users.toList()
Define Entities Based On The Entity Interface
@Table
public interface Student : Entity<Student> {
@PrimaryKey
public var id: Int?
public var name: String
public var age: Int
}
Generate code:
public open class Students(
alias: String? = null,
) : Table<Student>(tableName = "Student", alias = alias, entityClass = Student::class) {
// Ignore code
}
public val Database.students: EntitySequence<Student, Students> get() = this.sequenceOf(Students)
The difference from the table generated by the entity of any kind class
is that the table inherits
the Table class
instead of the
BaseTable class
, so there is no need to implement the doCreateEntity
method. Therefore, there is no need to
generate the
add
update
extension method of EntitySequence
(because it already exists)
val users = database.users.toList()
Define Table Schema
Adding the @Table annotation to the entity class will automatically generate the corresponding Table class.
The parameters of @Table are as follows:
Parameter | Description |
---|---|
tableName | Specify the parameter value of BaseTable.tableName |
tableClassName | Specifies the type name of the generated table type, which defaults to the plural form of the noun of the entity class |
alias | Specify the parameter value of BaseTable.alias |
catalog | Specify the parameter value of BaseTable.catalog |
schema | Specify the parameter value of BaseTable.schema |
ignoreColumns | Specifies a list of property names to ignore. The ignored properties will not generate corresponding column definitions in the generated Table class |
sequenceName | The sequence name,By default, the first character lowercase of the tableClassName |
Define Primary Key
Add the @Primary Key annotation to the entity class property to specify the property as the primary key.
Define Table Column
Add the @Column annotation to the entity class property to configure the generation options of the column definition.
The parameters of @Column are as follows:
Parameter | Description |
---|---|
columnName | Specify column names in SQL |
converter | Specify the column converter. For the converter, please refer to the type converter description at the bottom of the document |
propertyName | Specifies the property name of the corresponding column definition in the generated table class. |
isReferences | Specifies whether this property is a reference column. Only entity class based on the Entity interface can be assigned a value of true. When this value is true, the generated column definition will automatically call the references method |
Ignore The Specified Properties
Add the @Ignore annotation to the entity class property, and the generated table class will not contain the column definition of this property. Properties to ignore can also be specified in the ignore Columns parameter in @Table.
Global Configuration
Add the @KtormKspConfig annotation to any class for global configuration (this annotation can only be added once), and the annotation parameters are as follows:
Parameter | Description |
---|---|
allowReflectionCreateClassEntity | Whether to allow the creation of instance objects of entity of any kind class through reflection in the doCreateEntity method. If true, the instance will be created using reflection when the entity class constructor parameter has a default value parameter (reflection means a slight performance penalty, although in most cases this penalty is negligible). If it is false, the method will be directly constructed to create an instance, and the default value of the default value parameter in the construction will not take effect. |
enumConverter | Global enum converter, which is automatically used by enum type properties in entity classes. For converters, please refer to the description of type converters below |
singleTypeConverters | Global single-type converter, which is automatically used by properties of the corresponding type in the entity class. For converters, please refer to the description of type converters below |
namingStrategy | Global naming style configuration. For the naming style, please refer to the description of the naming style below |
extension | Generation options for extension methods/properties. For specific extension descriptions, please refer to the related descriptions of method/property generators below |
Extension Parameter
-
enableSequenceOf Whether to generate
EntitySequence
property extension. Generate code:val Database.employees: EntitySequence<Employee,Employees>
-
enableClassEntitySequenceAddFun Whether to generate
EntitySequence.add
method extension. This method is used to insert entity into the database. Generate code:fun EntitySequence<Employee,Employees>.add(employee: Employee)
-
enableClassEntitySequenceUpdateFun Whether to generate
EntitySequence.update
method extension. This method is used to update the entity by primary key. Generate code:fun EntitySequence<Employee,Employees>.update(employee: Employee)
-
enableInterfaceEntitySimulationDataClass Whether to generate
constuctor
components
copy
method. Generated only forentity class based on the Entity interface
. The purpose is to make the entity class likedata class
, Generate code:public fun Employee( id: Int? = undefined(), name: String = undefined(), job: String = undefined(), ): Employee public fun Employee.copy( id: Int? = undefined(), name: String = undefined(), job: String = undefined(), ): Employee public operator fun Employee.component1(): Int = this.id public operator fun Employee.component2(): String = this.name public operator fun Employee.component3(): String = this.job
Understand default argument:
undefined()
After creating an entity instance in ktorm, assigning null and unassigned values to the instance properties are two substantially different behaviors. Example:
val employee1 = Entity.create<Employee>() employee1.id = null database.employees.add(employee1) // SQL: insert into employee (id) values (null) val employee2 = Entity.create<Employee>() employee2.id = null employee2.name = null database.employees.add(employee2) // SQL: insert into employee (id, name) values (null, null)
The generated SQL does not contain unassigned property. The constructor function and copy function generated by ksp have similar effects
val employee = Employee(id = null) // The actual effect is equivalent to val employee = Entity.create<Employee>() employee.id = null // will not assign name property: employee.name = null
When calling the function, the created entity instance will not assign the corresponding properties to the parameters that are not passed. In order to achieve this, the default argument value in the constructor and copy function may generate JDK dynamic proxy object, proxy object generated by byte-buddy, object created by Unsafe(It depends on what the specific type is)This generated instance is unique and will not conflict with the parameters passed when calling (Unless you also call the undefined function to get the instance)Therefore, it can help us determine which parameters have passed values and which parameters have not passed values when calling the method. A limitation of this implementation is that the parameter type cannot be a non-null primitive type. This is because the non-null primitive type in kotlin will be automatically unboxed, which will cause our above implementation to fail, and there is no way to tell which parameter values were passed when calling. So in the generated Constructor Function, Copy Function,If the property is a non-null primitive type, it is automatically converted to a nullable type. And in the process of actually creating the instance, Will judge whether the parameter value is null, if it is null, an exception will be thrown
Naming Style
By default, the table name in the table class is generated, taking the entity class name. The column name takes the property name in the corresponding entity class.
The generated name can be modified through Global Naming Configuration
and Single Naming Configuration
.
Single Naming Configuration
Table name: assign the tableName
parameter to the @Table annotation on the entity class
Column name: add the @Column annotation to the property and assign the columnName
parameter
@Table(tableName = "t_student")
public interface Student : Entity<Student> {
@PrimaryKey
public var id: Int?
@Column(columnName = "student_name")
public var name: String
public var age: Int
}
Generate code:
public open class Students(
alias: String? = null,
) : Table<Student>(tableName = "t_student", alias = alias, entityClass = Student::class) {
public val id: Column<Int> = int("id").bindTo { it.id }.primaryKey()
public val name: Column<String> = varchar("student_name").bindTo { it.name }
public val age: Column<Int> = int("age").bindTo { it.age }
public override fun aliased(alias: String): Students = Students(alias)
public companion object : Students()
}
Table and column names configured in this way have the highest priority and are not affected by
the Global Naming Configuration
.
Global Naming Configuration
Add @KtormKspConfig annotation configuration on any class (this annotation can only be added once) and assign
the namingStrategy
parameter, this property requires a singleton object that implements the NamingStrategy
interface, In ktorm-ksp comes a camel case to snake case naming style strategy: CamelCaseToSnakeCaseNamingStrategy
@KtormKspConfig(
namingStrategy = CamelCaseToSnakeCaseNamingStrategy::class
)
public class KtormConfig
@Table
public interface Student : Entity<Student> {
@PrimaryKey
public var id: Int?
public var firstName: String
public var age: Int
}
Generate code:
public open class Students(
alias: String? = null,
) : Table<Student>(tableName = "student", alias = alias, entityClass = Student::class) {
public val id: Column<Int> = int("id").bindTo { it.id }.primaryKey()
public val firstName: Column<String> = varchar("first_name").bindTo { it.firstName }
public val age: Column<Int> = int("age").bindTo { it.age }
public override fun aliased(alias: String): Students = Students(alias)
public companion object : Students()
}
Type Converter
The data types supported by default in ktorm-ksp are as follows:
kotlin Type | Function Name | Underlying SQL Type | JDBC Type Code (java.sql.Types) |
---|---|---|---|
kotlin.Boolean | boolean | boolean | Types.BOOLEAN |
kotlin.Int | int | int | Types.INTEGER |
kotlin.Short | short | smallint | Types.SMALLINT |
kotlin.Long | long | bigint | Types.BIGINT |
kotlin.Float | float | float | Types.FLOAT |
kotlin.Double | double | double | Types.DOUBLE |
kotlin.BigDecimal | decimal | decimal | Types.DECIMAL |
kotlin.String | varchar | varchar | Types.VARCHAR |
java.sql.Date | jdbcDate | date | Types.DATE |
java.sql.Time | jdbcTime | time | Types.TIME |
java.sql.Timestamp | jdbcTimestamp | timestamp | Types.TIMESTAMP |
java.time.LocalDateTime | datetime | datetime | Types.TIMESTAMP |
java.time.LocalDate | date | date | Types.DATE |
java.time.LocalTime | time | time | Types.TIME |
java.time.MonthDay | monthDay | varchar | Types.VARCHAR |
java.time.YearMonth | yearMonth | varchar | Types.VARCHAR |
java.time.Year | year | int | Types.INTEGER |
java.time.Instant | timestamp | timestamp | Types.TIMESTAMP |
java.util.UUID | uuid | uuid | Types.OTHER |
kotlin.ByteArray | bytes | bytes | Types.BINARY |
kotlin.Enum | enum | enum | Types.VARCHAR |
If you need to use a type that is not listed above, or if you want to override the default type behavior, you need to use a type converter
There are three types of type converters (corresponding to three interfaces)
-
SingleTypeConverter
Only supports a certain type of converter, which can be used for global configuration or specified column configuration
-
MultiTypeConverter
Supports any type of converter, suitable for use in scenarios where objects are converted into json and stored in the database, and can only be used to specify column configuration
-
EnumConverter
A converter that supports any enumeration type, which can be used for global configuration or specific column configuration
How To Use Type Converter
You need to define a singleton and implement any of the above converter type interfaces. Then type converters can be
used via global configuration
or column configuration
, and the priority of the converters is as follows:
Column Configuration > Global Configuration > Default Type Conversion Behavior
Use Type Converter in Column
Any type of converter can be used via the converter property in @Column.
//Define Entities
@Table
data class User(
@PrimaryKey
var id: Int,
@Column(converter = UsernameConverter::class)
var username: Username,
var age: Int,
@Column(converter = IntEnumConverter::class)
var gender: Gender
)
enum class Gender {
MALE,
FEMALE
}
data class Username(
val firstName: String,
val lastName: String
)
//Type Converter
object UsernameConverter : SingleTypeConverter<Username> {
public override fun convert(
table: BaseTable<*>,
columnName: String,
propertyType: KClass<Username>
): Column<Username> {
return with(table) {
varchar(columnName).transform({
val spilt = it.split("#")
Username(spilt[0], spilt[1])
}, {
it.firstName + "#" + it.lastName
})
}
}
}
object IntEnumConverter : EnumConverter {
override fun <E : Enum<E>> convert(table: BaseTable<*>, columnName: String, propertyType: KClass<E>): Column<E> {
val values = propertyType.java.enumConstants
return with(table) {
int(columnName).transform({ values[it] }, { it.ordinal })
}
}
}
Generate Code:
public open class Users(
alias: String? = null,
) : BaseTable<User>(tableName = "User", alias = alias, entityClass = User::class) {
public val id: Column<Int> = int("id").primaryKey()
public val username: Column<Username> =
UsernameConverter.convert(this,"username",Username::class)
public val age: Column<Int> = int("age")
public val gender: Column<Gender> = IntEnumConverter.convert(this,"gender",Gender::class)
// ...
}
Use Type Converter in Global Configuration
Type converters can be added to the singleTypeConverters and enumConverter parameters in the global configuration @KtormKspConfig
-
singleTypeConverters: Receive the type array of SingleTypeConverter, when there is a property of the type supported by SingleTypeConverter, the corresponding converter will be used automatically
-
enumConverter: Receives a type of EnumConverter, all enumeration types will automatically use the converter.
enum class Gender {
MALE,
FEMALE
}
@Table
data class User(
@PrimaryKey
var id: Int,
var username: Username,
var age: Int,
var gender: Gender
)
data class Username(
val firstName: String,
val lastName: String
)
@KtormKspConfig(
singleTypeConverters = [UsernameConverter::class],
enumConverter = IntEnumConverter::class
)
class KtormConfig
object UsernameConverter : SingleTypeConverter<Username> {
public override fun convert(
table: BaseTable<*>,
columnName: String,
propertyType: KClass<Username>
): Column<Username> {
return with(table) {
varchar(columnName).transform({
val spilt = it.split("#")
Username(spilt[0], spilt[1])
}, {
it.firstName + "#" + it.lastName
})
}
}
}
object IntEnumConverter : EnumConverter {
override fun <E : Enum<E>> convert(table: BaseTable<*>, columnName: String, propertyType: KClass<E>): Column<E> {
val values = propertyType.java.enumConstants
return with(table) {
int(columnName).transform({ values[it] }, { it.ordinal })
}
}
}
Generate code:
public open class Users(
alias: String? = null,
) : BaseTable<User>(tableName = "User", alias = alias, entityClass = User::class) {
public val id: Column<Int> = int("id").primaryKey()
public val username: Column<Username> =
UsernameConverter.convert(this,"username",Username::class)
public val age: Column<Int> = int("age")
public val gender: Column<Gender> = IntEnumConverter.convert(this,"gender",Gender::class)
// ...
}
Function And Property Generator
The table class code generated by ktorm-ksp is generated by multiple code generators, and these generators are all customizable and extensible.
-
TableTypeGenerator
The table type declaration generator, only one is allowed, and the custom implementation will override the default implementation.
-
TablePropertyGenerator
The table class property declaration generator, only one is allowed, and the custom implementation will override the default implementation.
-
TableFunctionGenerator
The table class function generator, only one is allowed, and the custom implementation will override the default implementation.
-
TopLevelPropertyGenerator
Top-level property generator, generally used to generate extended property, multiple are allowed.
-
TopLevelFunctionGenerator
Top-level function generator, generally used to generate extension function, multiple are allowed.
Principles of custom generators
ktorm-ksp implements the custom extension of the generator through the SPI mechanism. The module dependencies are as follows (simplified):
The ktorm-ksp-compiler
module automatically loads the generator defined in your-ext-module
through SPI, and
uses it to participate in code generator
, to achieve the purpose of custom generator.
Steps To Customize The Generator
Please refer to the code implementation of this module
Create a new module that implements the generator (corresponding to your-ext-module
in the above figure), and add
dependencies in build.gradle
or pom.xml
// groovy dsl gradle
dependencies {
implementation 'org.ktorm:ktorm-ksp-codegen:${ktorm-ksp.version}'
}
// kotlin dsl gradle
dependencies {
implementation("org.ktorm:ktorm-ksp-codegen:${ktorm-ksp.version}")
}
<!-- maven -->
<dependencies>
<dependency>
<groupId>org.ktorm</groupId>
<artifactId>ktorm-ksp-codegen</artifactId>
<version>${ktorm-ksp.version}</version>
</dependency>
</dependencies>
Create a new generator class that implements any generator interface.
public class SequenceAddAllFunctionGenerator : TopLevelFunctionGenerator {
// Ignore code
}
public class SequenceUpdateAllFunctionGenerator : TopLevelFunctionGenerator {
// Ignore code
}
Create a new file in the resources/META-INF/services
directory, the file name is the fully qualified class name of
the generator interface (org.ktorm.ksp.codegen.TopLevelFunctionGenerator), and add the fully qualified class name of the
custom generator in the file. name, and multiple classes are separated by newlines.
org.ktorm.ksp.ext.SequenceAddAllFunctionGenerator
org.ktorm.ksp.ext.SequenceUpdateAllFunctionGenerator
Add the your-ext-module
to the modules that need to generate code with it (corresponding to your-app-module
in the above figure)
// groovy dsl gradle
dependencies {
implementation 'org.ktorm:ktorm-ksp-api:${ktorm-ksp.version}'
ksp 'org.ktorm:ktorm-ksp-compile:${ktorm-ksp.version}'
ksp project(':your-ext-module')
}
// kotlin dsl gradle
dependencies {
implementation("org.ktorm:ktorm-ksp-api:${ktorm-ksp.version}")
ksp("org.ktorm:ktorm-ksp-compile:${ktorm-ksp.version}")
ksp(project(":your-ext-module"))
}
<!-- maven -->
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<compilerPlugin>ksp</compilerPlugin>
</compilerPlugins>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
<sourceDir>target/generated-sources/ksp</sourceDir>
</sourceDirs>
</configuration>
<dependencies>
<dependency>
<groupId>com.dyescape</groupId>
<artifactId>kotlin-maven-symbol-processing</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.ktorm</groupId>
<artifactId>ktorm-ksp-compiler</artifactId>
<version>${ktorm-ksp.version}</version>
</dependency>
<dependency>
<groupId><!-- your-ext-module groupId --></groupId>
<artifactId><!-- your-ext-module artifactId --></artifactId>
<version><!-- your-ext-module version --></version>
</dependency>
</dependencies>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
Let ksp generate the code again. You will see the code generated by the custom generator.
Available Generator Extensions
A function to generate batch addition and batch update for any kind of class
entity sequence. Dependencies:
ksp 'org.ktorm:ktorm-ksp-sequence-batch:${ktorm-ksp.version}'
Generate the following extension function:
/**
* Batch insert entities into the database, this method will not get the auto-incrementing primary key
* @param entities List of entities to insert
* @return the effected row counts for each sub-operation.
*/
public fun EntitySequence<Customer, Customers>.addAll(entities: Iterable<Customer>): IntArray =
this.database.batchInsert(Customers) {
for (entity in entities) {
item {
set(Customers.id, entity.id)
set(Customers.name, entity.name)
set(Customers.email, entity.email)
set(Customers.phoneNumber, entity.phoneNumber)
}
}
}
/**
* Batch update based on entity primary key
* @param entities List of entities to update
* @return the effected row counts for each sub-operation.
*/
public fun EntitySequence<Customer, Customers>.updateAll(entities: Iterable<Customer>): IntArray =
this.database.batchUpdate(Customers) {
for (entity in entities) {
item {
set(Customers.name, entity.name)
set(Customers.email, entity.email)
set(Customers.phoneNumber, entity.phoneNumber)
where {
it.id eq entity.id!!
}
}
}
}