@LukasEder, this question is more Kotlin related, you can probably skip it in favour of the more jOOQ-related continuation in https://github.com/jOOQ/jOOQ/issues/14972 :)
So part of a legacy database has a pretty over-engineered design, but we cannot and do not want to change that as part of a migration away from JPA to jOOQ and want to leave that for later.
For (one of sadly multiple) examples: we have Characteristic
s. It's not really important what these do, it's just that they are typed.
Because they look very identical and because at some point, we may want to simplify the database, we don't really want to write multiple queries for them.
The solution we've settled on is to define a mapping that will deliver us the correct table given a certain type.
I.e. something like
@Bean(CHARACTERISTIC_TABLE)
fun characteristicTable(): Map<CharacteristicType, Table<*>> = mapOf(
CharacteristicType.INTEGER to INTEGER_CHARACTERISTIC,
CharacteristicType.FLOAT to FLOAT_CHARACTERISTIC,
CharacteristicType.TEXT to TEXT_CHARACTERISTIC,
CharacteristicType.NOMINAL to NOMINAL_CHARACTERISTIC,
CharacteristicType.ORDINAL to ORDINAL_CHARACTERISTIC,
CharacteristicType.BINARY_PDF to BINARY_CHARACTERISTIC,
CharacteristicType.BINARY_IMAGE to BINARY_CHARACTERISTIC,
CharacteristicType.TABULAR to GROUP_CHARACTERISTIC
)
this we can inject into our repositories and retrieve a Table<*>
from it based on a CharacteristicType
.
Obviously, Table<*>
doesn't know anything about its fields, so we have to define the columns that they share ourselves. E.g.
private val Table<*>.ID get() = this.checkField<Int>("id")
private val Table<*>.NAME get() = this.checkField<String>("name")
where
inline fun <reified T : Any> Table<*>.checkField(fieldName: String): Field<T> =
checkNotNull(this.field(fieldName, T::class.javaObjectType)) {
"Table [$this] lacks the '$fieldName' column."
}
Works, but the problem with that, of course, is that now ALL tables have an e.g. .NAME
column suggestion, even when they do not, in fact, have a name
column, because they all fit the type Table<*>
.
Enter Kotlin value classes: by defining a wrapper, we can scope our extension properties and still have them behave like just any old Table<*>
and paying none of the wrapping overhead.
I.e. for our Characteristic
s, we can define
@JvmInline
value class CharacteristicTable<T : Record>(val value: Table<T>) : Table<T> by value
wrap our tables in the map
@Bean(CHARACTERISTIC_TABLE)
fun characteristicTable(): Map<CharacteristicType, CharacteristicTable<*>> = mapOf(
CharacteristicType.INTEGER to CharacteristicTable(INTEGER_CHARACTERISTIC),
CharacteristicType.FLOAT to CharacteristicTable(FLOAT_CHARACTERISTIC),
CharacteristicType.TEXT to CharacteristicTable(TEXT_CHARACTERISTIC),
CharacteristicType.NOMINAL to CharacteristicTable(NOMINAL_CHARACTERISTIC),
CharacteristicType.ORDINAL to CharacteristicTable(ORDINAL_CHARACTERISTIC),
CharacteristicType.BINARY_PDF to CharacteristicTable(BINARY_CHARACTERISTIC),
CharacteristicType.BINARY_IMAGE to CharacteristicTable(BINARY_CHARACTERISTIC),
CharacteristicType.TABULAR to CharacteristicTable(GROUP_CHARACTERISTIC)
)
and then properly scope our extension properties in the
private val CharacteristicTable<*>.ID get() = this.checkField<Int>("id")
private val CharacteristicTable<*>.NAME get() = this.checkField<String>("name")
private val CharacteristicTable<*>.MACHINE_CODE get() = this.checkField<String>("machine_code")
[...]
while still using them as the Table<*>
they wrap in our queries:
val characteristic: CharacteristicTable<*> = [...]
val ctx: DSLContext = [...]
[...]
val intId = ctx
.insertInto(characteristic) //our wrapped table, used to be the actual Table<*>
.columns(*insertedColumns)
.values(*insertedValues)
.onConflict(characteristic.MACHINE_CODE)
.doUpdate()
.setAllToExcluded()
.returning(characteristic.ID)
.fetchSingle { it[characteristic.ID] }
[...]
... or so I thought.
The problem if we do, though, is that above query will throw a
java.lang.ClassCastException: class my.app.db.config.QualifiedMaps$CharacteristicTable cannot be cast to class org.jooq.QueryPartInternal (my.app.db.config.QualifiedMaps$CharacteristicTable and org.jooq.QueryPartInternal are in unnamed module of loader 'app')
at org.jooq_3.18.0.POSTGRES.debug(Unknown Source)
at org.jooq.impl.AbstractContext.visit(AbstractContext.java:292)
at org.jooq.impl.InsertQueryImpl.lambda$toSQLInsert$11(InsertQueryImpl.java:700)
at org.jooq.impl.AbstractContext.toggle(AbstractContext.java:379)
at org.jooq.impl.AbstractContext.declareTables(AbstractContext.java:623)
at org.jooq.impl.InsertQueryImpl.toSQLInsert(InsertQueryImpl.java:700)
at org.jooq.impl.InsertQueryImpl.lambda$accept0$1(InsertQueryImpl.java:389)
at org.jooq.impl.AbstractContext.toggle(AbstractContext.java:393)
at org.jooq.impl.AbstractContext.data(AbstractContext.java:404)
at org.jooq.impl.InsertQueryImpl.accept0(InsertQueryImpl.java:389)
at org.jooq.impl.AbstractDMLQuery.accept(AbstractDMLQuery.java:670)
at org.jooq.impl.DefaultRenderContext.visit0(DefaultRenderContext.java:726)
at org.jooq.impl.AbstractContext.visit(AbstractContext.java:350)
at org.jooq.impl.AbstractQuery.getSQL0(AbstractQuery.java:491)
at org.jooq.impl.AbstractQuery.execute(AbstractQuery.java:300)
at org.jooq.impl.AbstractDMLQueryAsResultQuery.fetch(AbstractDMLQueryAsResultQuery.java:140)
at org.jooq.impl.ResultQueryTrait.fetchLazy(ResultQueryTrait.java:281)
at org.jooq.impl.ResultQueryTrait.fetchLazyNonAutoClosing(ResultQueryTrait.java:290)
at org.jooq.impl.ResultQueryTrait.fetchSingle(ResultQueryTrait.java:605)
at org.jooq.impl.ResultQueryTrait.fetchSingle(ResultQueryTrait.java:610)
Now why isn't the kotlin class inlined?
From the documentation, we know:
As a rule of thumb, inline classes are boxed whenever they are used as another type.
Well, there's our answer (sort of).
We can of course manually unbox it, but that kind of defeats the purpose of using a value class...
Can we do something to the class to force Kotlin to always inline it?