1

@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 Characteristics. 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 Characteristics, 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?

User1291
  • 7,664
  • 8
  • 51
  • 108
  • 1
    No, there isn't a way to make Kotlin always inline value classes. I think that's just impossible generally. – al3c Apr 20 '23 at 19:13

1 Answers1

0

This doesn't answer your actual question about inlining types, but I think your problem is much better solved differently, as I've also stated on the issue #14972 you've created and linked.

Rather than wrapping jOOQ's generated table, just let jOOQ's generated table extend your type:

You could:

  • Specify a CharacteristicsTable<R : Record> : Table<R> interface
  • Put the relevant methods on that interface directly, or as extension methods
  • Use a generator strategy to make sure all the appropriate tables implement that interface.
Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509