KRowMapper
Spring JDBC RowMapper for Kotlin.
Install / Use
/learn @ProjectMapK/KRowMapperREADME
KRowMapper
KRowMapper is a RowMapper for Kotlin, which provides the following features.
- Object relationship mapping with minimal effort, equivalent to
BeanPropertyRowMapper. - Faster mapping than
BeanPropertyRowMapper. - Flexible and safe mapping based on function calls with
reflection.
Notes
With the update to SpringFramework 5.3/SpringBoot 2.4, mapping by constructor call is now supported by the DataClassRowMapper.
If you do not need to use an external library, please consider using this one.
Demo code
Here is a comparison between writing the mapping code manually and using KRowMapper.
The more arguments you write manually, the more you need to write, but if you use KRowMapper This allows you to do the mapping without writing any code.
Also, you don't need external configuration file at all.
However, if the naming conventions of arguments and DB columns are different, you will need to pass a naming conversion function.
Please note that there are (see below).
// mapping destination
data class Dst(
foo: String,
bar: String,
baz: Int?,
...
)
// If you write RowMapper manually.
val dst: Dst = jdbcTemplate.query(query) { rs, _ ->
Dst(
rs.getString("foo"),
rs.getString("bar"),
rs.getInt("baz"),
...
)
}
// If you use KRowMapper
val dst: Dst = jdbcTemplate.query(query, KRowMapper(::Dst, /* Naming transformation functions as needed. */))
Installation
KRowMapper is published on JitPack.
You can use this library on maven, gradle and any other build tools.
Please see here for the formal introduction method.
Example on maven
1. add repository reference for JitPack
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
2. add dependency
<dependency>
<groupId>com.github.ProjectMapK</groupId>
<artifactId>KRowMapper</artifactId>
<version>Tag</version>
</dependency>
Principle of operation
The behavior of KRowMapper is as follows.
- Get the
KFunctionto be called. - Analyze the
KFunctionand determine what arguments are needed and how to deserialize them. - Get the value for each argument from the
ResultSetand deserialize it. and call theKFunction.
KRowMapper performs the mapping by calling a function, so the result is a Subject to the constraints on the argument and nullability.
That is, there is no runtime error due to breaking the null safety of Kotlin(The null safety on type arguments may be broken due to problems on the Kotlin side).
Also, it supports the default arguments which are peculiar to Kotlin.
Initialization
KRowMapper can be initialized from method reference(KFunction) to be called or the KClass to be mapped.
Also, by default, KRowMapper compares argument names and column names to see if they correspond.
Therefore, in the case of "argument name is camelCase and column name is snake_case", it is necessary to pass a function that appropriately converts the naming convention of the argument name.
You can also pass a ConversionService for value conversion if needed.
If you don't pass it, DefaultConversionService.sharedInstance will be used as default.
Initialization from method reference(KFunction)
You can initialize KRowMapper from method reference(KFunction) as follows It is.
data class Dst(
foo: String,
bar: String,
baz: Int?,
...
)
// get constructor method reference
val dstConstructor: KFunction<Dst> = ::Dst
// initialize KRowMapper from KFunction
val mapper: KRowMapper<Dst> = KRowMapper(dstConstructor)
The following three methods are the main ways to get the method reference.
- from
constructor:::Dst - from
factory methodincompanion object:(Dst)::factoryMethod - from instance method in
thisscope:this::factoryMethod
Initialization from KClass
The KRowMapper can be initialized by KClass.
By default, the primary constructor is the target of the call.
data class Dst(...)
val mapper: KRowMapper<Dst> = KRowMapper(Dst::class)
You can also write the following using a dummy constructor.
val mapper: KRowMapper<Dst> = KRowMapper<Dst>()
Specifying the target of a call by KConstructor annotation
When you initialize from the KClass, you can use the KConstructor annotation and to specify the function to be called.
In the following example, the secondary constructor is called.
data class Dst(...) {
@KConstructor
constructor(...) : this(...)
}
val mapper: KRowMapper<Dst> = KRowMapper(Dst::class)
Similarly, the following example calls the factory method.
data class Dst(...) {
companion object {
@KConstructor
fun factory(...): Dst {
...
}
}
}
val mapper: KRowMapper<Dst> = KRowMapper(Dst::class)
Conversion of argument names
By default, KRowMapper looks for the column corresponding to the argument name.
data class Dst(
fooFoo: String,
barBar: String,
bazBaz: Int?
)
// required arguments: fooFoo, barBar, bazBaz
val mapper: KRowMapper<Dst> = KRowMapper(::Dst)
// the behavior is equivalent to the following
val rowMapper: RowMapper<Dst> = { rs, _ ->
Dst(
rs.getString("fooFoo"),
rs.getString("barBar"),
rs.getInt("bazBaz"),
)
}
On the other hand, if the argument naming convention is a camelCase and the DB column naming convention is a snake_case You will not be able to see the match in this case.
In this situation, you need to pass the naming transformation function at the initialization of KRowMapper.
val mapper: KRowMapper<Dst> = KRowMapper(::Dst) { fieldName: String ->
/* some naming transformation process */
}
The actual conversion process
Since KRowMapper does not provide the naming transformation process, the naming transformation process requires an external library.
As an example, sample code that passes the conversion process from camelCase to snake_case is shown for two libraries, Jackson and Guava.
These libraries are often used by Spring framework and other libraries.
Jackson
import com.fasterxml.jackson.databind.PropertyNamingStrategy
val parameterNameConverter: (String) -> String = PropertyNamingStrategy.SnakeCaseStrategy()::translate
val mapper: KRowMapper<Dst> = KRowMapper(::Dst, parameterNameConverter)
Guava
import com.google.common.base.CaseFormat
val parameterNameConverter: (String) -> String = { fieldName: String ->
CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, fieldName)
}
val mapper: KRowMapper<Dst> = KRowMapper(::Dst, parameterNameConverter)
Detailed usage
By using the contents described so far, you can perform more flexible and safe mapping compared to BeanPropertyRowMapper.
In addition, by making full use of the abundant functions provided by KRowMapper, further labor saving is possible.
Deserialization
KRowMapper supports deserialization using the ConversionService (DefaultConversionService.sharedInstance by default).
In addition to this, as a more explicit and flexible deserialization method, KRowMapper provides the following three deserialization methods.
- Deserialization by using the
KColumnDeserializerannotation. - Deserialization by creating your own custom deserialization annotations.
- Deserialization from multiple arguments.
These deserialization methods take precedence over deserialization by ConversionService.
Deserialization by using the KColumnDeserializer annotation
If it is a self-made class and can be initialized from a single argument, deserialization using the KColumnDeserializer annotation can be used.
KColumnDeserializer annotation can be used to constructor or factory method defined in companion object.
// for primary constructor
data class FooId @KColumnDeserializer constructor(val id: Int)
// for secondary constructor
data class FooId(val id: Int) {
@KColumnDeserializer
constructor(id: String) : this(id.toInt())
}
// for factory method
data class FooId(val id: Int) {
companion object {
@KColumnDeserializer
fun of(id: String): FooId = FooId(id.toInt())
}
}
Class with KColumnDeserializer annotation can be mapped as an argument without any special description.
data class Dst(
fooId: FooId,
bar: String,
baz: Int?,
...
)
Deserialization by creating your own custom deserialization annotations
If you cannot use KColumnDeserializer, you can deserialize it by creating a custom deserialization annotations and adding it to the parameter.
Custom deserialization annotation is made by defining a pair of deserialization annotation and deserializer.
As an example, we will show how to create a LocalDateTimeDeserializer that deseria
