feat: EXPOSED-819 Exposed R2DBC DAO - #2831
Conversation
R2DBC DAO API — Key Differences from JDBC DAO
This document highlights behavioral and structural differences between the JDBC DAO ( 1. Relationship properties:
|
| Method | JDBC | R2DBC |
|---|---|---|
EntityClass.findById(id) |
fun findById(id): T? |
suspend fun findById(id): T? |
EntityClass.count(op) |
fun count(op): Long |
suspend fun count(op): Long |
Entity.delete() |
fun delete() |
suspend fun delete() |
Entity.flush(batch) |
fun flush(batch): Boolean |
suspend fun flush(batch): Boolean |
Entity.refresh(flush) |
fun refresh(flush) |
suspend fun refresh(flush) |
EntityClass[id] |
operator fun get(id): T |
suspend operator fun get(id): T |
EntityClass.reload(entity) |
open fun reload(entity): T? |
suspend fun reload(entity): T? |
EntityCache.flush() |
fun flush() |
suspend fun flush() |
EntityCache.clear(flush) |
fun clear(flush) |
suspend fun clear(flush) |
Transaction.flushCache() |
fun Transaction.flushCache() |
suspend fun R2dbcTransaction.flushCache() |
Entity hook subscriptions also accept suspend lambdas:
// JDBC
EntityHook.subscribe { change -> /* non-suspend */ }
// R2DBC
EntityHook.subscribe { change -> /* suspend */ }4. Collections return SizedIterable backed by Flow
R2DBC's SizedIterable is Flow-based rather than Iterable-based. This means collecting results requires an explicit toList() call:
// JDBC
broker.clients.map { it.name } // direct iteration
// R2DBC
broker.clients().toList().map { it.name } // collect Flow, then mapThis also affects eager loading — R2DBC requires two with() overloads (one for SizedIterable, one for Iterable) where JDBC has one.
5. Explicit attach() for cross-transaction entity reuse
JDBC auto-attaches entities to the current transaction when you set a column value (inside Column.setValue). R2DBC can't do this because the auto-attach would require a suspend call inside a non-suspend operator.
R2DBC provides an explicit attach() method instead:
// R2DBC
suspendTransaction {
val broker = Broker.findById(id)!!
// ... later, in a different transaction context:
Broker.attach(broker) // explicitly re-attach
}6. Missing JDBC features (not yet ported)
The following JDBC DAO features have no R2DBC equivalent yet:
ImmutableEntityClass/ImmutableCachedEntityClass— immutable entities with cross-transaction cachingfindWithCacheCondition— cache-first lookup with fallback to DB querywarmUpReferences/warmUpOptReferences— bulk eager-loading helpers (R2DBC has equivalent private helpers but issues per-parent queries instead of bulkcompoundOrqueries for composite FKs)
|
Last time I asked you said its not usable yet or sth. How would you describe this 1? What stuff should I be aware of? Also the attachment stuff isn't clear to me yet. I didnt find a good explanation in the doc either in a quick scan. from the tutorial rn: What do I need to stay aware of if some fields of a entity could be changed while sth else still has it "cached"? |
|
Also build exposed with team city should skip detect. right now it fails with detekt weighted issues error. A Seperate detekt pipeline is good though. |
|
settings.gradle.kts is missing a include("exposed-dao-r2dbc") rn |
|
Warning merging is currently not possible I think. After the swap of couroutine version 1.10.2 to 1.11.0 I have gotten a error. Reducing the version to 1.10.2 removes said error. java.lang.NoSuchMethodError: 'java.lang.Object kotlinx.coroutines.BuildersKt.runBlockingK$default(kotlin.coroutines.CoroutineContext, kotlin.jvm.functions.Function2, int, |
|
obabichevjb I found a major issue it seems.
unlike what its saying here at least this is incorrect.
I decided to avoid attaching issues in the future by using a custom wrapper for all of fields in entity classes similar to how the reference work. Yet I get the following error: org.jetbrains.exposed.r2dbc.dao.exceptions.R2dbcEntityNotFoundException: Entity BBUser, id=1 not found in the database After some more investigation there are seemingly 2 entitys with the same id but different locations so the === says false while the attatch returns early since it thinks its already in the cache. R2dbcEntityClass.kt:153 checks if in the cache but only some entity given the top description this seem incorrect. for the bottom thrown exception I would also say that a more detailed exception would fit better since the not found is a bit misleading. |
JDBC DAO → R2DBC DAO: Public API DiffA class-by-class comparison of public members only. Excluded:
|
| Public member | JDBC | R2DBC | Notes |
|---|---|---|---|
val id: EntityID<ID> |
✅ | ✅ | Constructor parameter. |
var klass: EntityClass<ID, Entity<ID>> |
✅ | ✅ | internal set in both. |
var db |
Database |
R2dbcDatabase |
Type changed. |
val writeValues |
✅ | ✅ | Same. |
var _readValues |
✅ | ✅ | Same. |
val readValues |
Auto-fetches from DB on first miss | Throws if _readValues == null |
Semantic change. R2DBC's getter cannot suspend. |
fun refresh(flush: Boolean = false) |
✅ | ✅ | Same signature. |
fun delete() |
✅ | ✅ | R2DBC also skips DELETE SQL when entity was not flushed yet. |
fun flush(batch: EntityBatchUpdate? = null): Boolean |
✅ | ✅ | Same. |
fun storeWrittenValues() |
✅ | ✅ | R2DBC drops the ColumnWithTransform.unwrapRecursive branch — minor logic diff. |
operator fun Column<T>.getValue/setValue |
✅ | ✅ | Same. |
operator fun CompositeColumn<T>.getValue/setValue |
✅ | ✅ | Same. |
operator fun EntityFieldWithTransform.getValue/setValue |
✅ | ✅ | Same. |
operator fun Reference.getValue/setValue |
✅ | REMOVED | Replaced by Accessor pattern in relationships/. |
operator fun OptionalReference.getValue/setValue |
✅ | REMOVED | Replaced by OptionalAccessor. |
fun Column<T>.lookup(): T |
✅ | ✅ | R2DBC throws on isDatabaseGenerated() columns before flush; JDBC silently auto-flushes via the same path. |
fun Column<T>.lookupInReadValues(found, notFound) |
✅ | REMOVED | Not ported. |
infix fun via(table) |
✅ | ✅ | Same. |
fun via(sourceColumn, targetColumn) |
✅ | ✅ | Same. |
EntityClass<ID, T>
| Public member | JDBC | R2DBC | Notes |
|---|---|---|---|
fun all() |
✅ | ✅ | Same. |
fun find(op) / fun find(op: () -> Op<Boolean>) |
✅ | ✅ | Same. |
fun findById(id) (both ID and EntityID<ID> overloads) |
✅ | ✅ | Same. |
fun findByIdAndUpdate / findSingleByAndUpdate |
✅ | ✅ | Same. |
fun count(op) |
✅ | ✅ | Same. |
fun searchQuery(op) |
✅ | ✅ | Same. |
fun wrap(entityID, row) / wrapRow / wrapRows |
✅ | ✅ | Same. |
fun reload(entity, flush) |
✅ | ✅ | Same. |
fun removeFromCache(entity) |
✅ | ✅ | Same. |
fun forEntityIds(ids) |
✅ | ✅ | Same. |
fun testCache(id) / fun testCache(cacheCheckCondition) |
✅ | ✅ | Same. |
fun new(init): T |
✅ | ✅ | Suspend in R2DBC; eagerly flushes and returns T (matches JDBC semantics). |
fun new(id, init): T |
✅ | ✅ | Suspend in R2DBC; same semantics. |
fun newDeferred(init): Flow<T> |
— | ✅ | R2DBC-only. Non-suspend; schedules the insert and flushes the entire cache on first collection. For batching / graph-build. |
fun newDeferred(id, init): Flow<T> |
— | ✅ | R2DBC-only. |
fun attach(entity) |
— | ✅ | R2DBC-only. |
fun forIds(ids: List<ID>): SizedIterable<T> |
✅ | ✅ | Ported. |
fun view(op: () -> Op<Boolean>): View<T> |
✅ | ❌ | Missing (View class isn't ported either). |
fun expireCache() |
✅ | ❌ | Missing. Tied to ImmutableCachedEntityClass. |
fun findWithCacheCondition(cond, op) |
✅ | ❌ | Missing. Tied to ImmutableCachedEntityClass. |
fun warmUpReferences / warmUpOptReferences / warmUpLinkedReferences |
✅ | ✅ | Same public surface. |
infix fun referencedOn / optionalReferencedOn / referrersOn / optionalReferrersOn / backReferencedOn / optionalBackReferencedOn |
Member on EntityClass |
Member on EntityClass |
Same location and same call-site syntax; return types differ (see relationships section). |
infix fun via(table) / fun via(sourceColumn, targetColumn) |
Member on Entity (only via Entity.via) |
Same place | Same. |
Operator get(id) |
✅ | ✅ | Same. |
EntityCache
| Public member | JDBC | R2DBC | Notes |
|---|---|---|---|
val data: Map<IdTable<*>, ...> |
✅ | ✅ | Same. |
fun find(klass, id) |
✅ | ✅ | Same. |
fun store(entity) / fun store(klass, entity) |
✅ | ✅ | Same. |
fun remove(table, entity) |
✅ | ✅ | Same. |
fun findAll(klass) |
✅ | ✅ | Same. |
fun scheduleInsert(klass, entity) |
✅ | ✅ | Same. |
fun scheduleUpdate(klass, entity) |
✅ | ✅ | Same. |
fun getOrPutReferrers(sourceId, key, refs) |
✅ | ✅ | Same parameter order/naming. |
fun getReferrers(sourceId, key) |
✅ | ✅ | Same. |
fun clear(flush) |
✅ | ✅ | Same. |
fun clearReferrersCache() |
✅ | ✅ | Same. |
fun updateEntities(table) |
✅ | ✅ | Same. |
fun flush() / fun flush(tables) |
✅ | ✅ | Same. |
var maxEntitiesToStore |
✅ | ✅ | Same. |
companion object: fun invalidateGlobalCaches(created) |
✅ | ❌ | Missing. Tied to ImmutableCachedEntityClass. |
EntityHook / EntityChange
Same public API in both modules — subscribe, unsubscribe, EntityChange(EntityClass, EntityID, EntityChangeType). R2DBC has its own copy because the lifecycle is owned by R2DBC's EntityClass.
Relationships layer
Setup-time DSL (member functions on EntityClass, matches JDBC layout)
| JDBC | R2DBC | Status |
|---|---|---|
infix fun referencedOn(column) |
infix fun referencedOn(column) |
Same name, but returned Reference is based on Accessor |
infix fun optionalReferencedOn(column) |
same | Same. |
infix fun referrersOn(column) / referrersOn(column, cache) |
same | Same name; return type changed. |
infix fun optionalReferrersOn(column) / optionalReferrersOn(column, cache) |
same | Same. |
infix fun backReferencedOn(column) |
same | Same. |
infix fun optionalBackReferencedOn(column) |
same | Same. |
infix fun via(table) (on Entity) |
same | Same. |
Runtime read/write API
| Operation | JDBC | R2DBC |
|---|---|---|
| Read many-to-one | entity.parent (property access) |
entity.parent() (suspend invoke) |
| Write many-to-one | entity.parent = x |
entity.parent.set(x) or entity.parent(x) |
| Read one-to-many | entity.children returns SizedIterable<Child> |
entity.children returns a DeferredQuery<Child> (implements SizedIterable<Child>, Flow-based, produced by Referrers.getValue) |
| Read back-reference | entity.backRef |
entity.backRef() (suspend invoke) |
| Read many-to-many | entity.tags returns SizedIterable<Tag> |
entity.tags returns InnerTableLinkAccessor (Flow-based SizedIterable<Tag>) |
| Write many-to-many | entity.tags = SizedCollection(...) |
Same syntax — deferred until flush in R2DBC |
| Eager load | query.with(Entity::prop) |
query.with(Entity::prop) (suspend) |
Removed exported classes
| JDBC | Replacement in R2DBC |
|---|---|
Reference<REF, RID, T> |
Reference<ID, Parent, REF> + runtime Accessor<ID, Parent, REF> |
OptionalReference<REF, RID, T> |
OptionalReference<ID, Parent, REF> + runtime OptionalAccessor<ID, Parent, REF> |
Referrers<ParentID, Parent, ChildID, Child, REF> |
Referrers<ParentID, Parent, ChildID, Child, REF> — setup class; getValue returns a DeferredQuery<Child> (no dedicated runtime accessor) |
BackReference<...> |
BackReference<...> (same class name, different runtime API) |
InnerTableLink<...> (also acts as runtime accessor) |
Split into InnerTableLink (setup) and InnerTableLinkAccessor (runtime, delegates SizedIterable to DeferredQuery via Kotlin's by) |
Missing classes (JDBC has them; R2DBC doesn't)
| JDBC class | Note |
|---|---|
ImmutableEntityClass |
Read-only entity marker. |
ImmutableCachedEntityClass |
Process-wide read cache. |
View<T> |
Filtered entity view as SizedIterable. |
DaoEntityIDFactory |
EntityID factory registration. |
New classes (R2DBC has them; JDBC doesn't)
| R2DBC class | Purpose |
|---|---|
Accessor / OptionalAccessor |
Runtime many-to-one accessor (suspend invoke, set, invoke(value)). |
InnerTableLinkAccessor |
Runtime many-to-many delegate; SizedIterable behavior delegated to DeferredQuery via Kotlin's by. |
DeferredQuery (internal) |
Lazy SizedIterable used by Referrers.getValue and InnerTableLinkAccessor to defer query execution. |
ExperimentalR2dbcDaoApi |
Opt-in annotation gating the public R2DBC DAO API surface. |
|
| Pattern | Share | What the code reads from the new entity |
|---|---|---|
A — chain to mapper/DTO: Foo.new { … }.toResponse() |
~30% | ID + generated timestamps |
B — read .id.value immediately |
~15% | ID |
| C — return entity to caller | ~20% | Caller reads ID + audit |
D — capture as val, reuse as FK in same tx |
~25% | ID (as FK for more .new { }) |
| E — fire-and-forget | ~10% | Nothing |
~90% of call sites need the persisted entity or its ID immediately.
If new { } returned a builder requiring .flush(), idioms like
Foo.new { … }.id.value, Foo.new { … }.mapToModel(),
val parent = Parent.new { … }; Child.new { this.parent = parent }
would all break on every single write. That's a lot of friction on the migration
path from JDBC DAO.
newDeferred { } — the ~10% escape hatch
For the graph-build / batch-insert case (e.g. bulk-loading a parent + children in
one transaction), newDeferred { } returns Flow<T>. It schedules without SQL;
collecting the flow triggers one cache flush that batch-INSERTs everything.
Creating parent-child entities in memory
At the current moment it's not possible to create some entities connected via references in memory (without flushing to the database. It could be good idea to implement that case too, if we choose the current way. But at the current moment in terms of communication with database it matches jdbc. Under the hood jdbc also makes flush (if I understood everything correct) in the moment, when the referenced entity set to another entity.
* feat: eager loading, many-to-many * feat: Complete migration of EntityTests * feat: Extract trimToFirst
feat: Change new() method, now it is suspend. Alternative newDeferred() returns Flow of the Entity. feat: Fixes after review. DefferedQuery to work with collections on relations; Container for initialized entities
0db09a4 to
932cda2
Compare
…BC-to-R2DBC migration docs and standalone showcase build
4d57a04 to
252a8cf
Compare
|
Tried the things again. Still some issue it seems: 12:21:48.536 [reactor-tcp-epoll-4 Coroutine LLC (@coroutine)#63] ERROR de.hype.bingonet.server.BBLogger - Entity BingoEventAPIPlayerData, id=CompositeID(bingo_id=55, mc_uuid=1cedf17e-d9b0-47d3-a90a-92611306c44f) not found in the database |
|
obabichevjb I had some patches previously to get it running and i afterwards merged in the changes. I laso tried to fix the issue myself so maybe sth went wrong there. I just did it base on top of your current branch and I dont get that error at least. But its not working either. Ill investigate further |
|
I noticed an freeze. Its not validated to come from your code yet though When i ran class BBRoleEntity(id: EntityID<CompositeID>) : CompositeEntity(id) {
companion object : CompositeEntityClass<BBRoleEntity>(BBRoles)
val user by BBRoles.user.transformFlat().selfAttaching(Companion)
val role by BBRoles.role.transformFlat().selfAttaching(Companion)
}
object BBRoles : CompositeIdTable("user_roles") {
// user is stored using the project's bbUser column type and used as part of the composite id
val user = bbUser("user").entityId()
// role is stored as enum-by-name
val role = enumerationByName<BNRole>("role").entityId()
init {
addIdColumn(user)
addIdColumn(role)
}
}
fun <T : Any> Column<EntityID<T>>.transformFlat(
table: IdTable<T> = this.table as IdTable<T>,
cacheResult: Boolean = false
): EntityFieldWithTransform<EntityID<T>, T> = EntityFieldWithTransform(
column = this,
transformer = columnTransformer(
{
object : EntityID<T>(table, it) {}
}, {
it.value
}
),
cacheResult = cacheResult
)
context(it: Entity<ID>)
fun <ID : Any, E : Entity<ID>, CT : Any?> Column<CT>.selfAttaching(
entityClass: EntityClass<ID, E>
): AttachDelegate<ID, E, CT> = AttachDelegate(this, entityClass)
fun <WrappedType, DatabaseType, SuperEntityId : Any> EntityFieldWithTransform<DatabaseType, WrappedType>.selfAttaching(
entityClass: EntityClass<SuperEntityId, Entity<SuperEntityId>>
): AttachWrappedDelegate<WrappedType, DatabaseType, SuperEntityId> =
AttachWrappedDelegate(this, entityClass)
abstract class AttatchHandler<Type> {
abstract fun get(): Type
abstract suspend infix fun set(value: Type)
operator fun invoke(): Type = get()
override fun equals(other: Any?): Boolean {
return other == get()
}
override fun hashCode(): Int {
return get().hashCode()
}
override fun toString(): String {
return get().toString()
}
}
class AttachHandle<ID : Any, E : Entity<ID>, CT : Any?>(
private val column: Column<CT>,
private val entity: E,
private val entityClass: EntityClass<ID, *>,
private val prop: KProperty<*>
) : AttatchHandler<CT>() {
override fun get(): CT = with(entity) { column.getValue(entity, prop) }
override suspend infix fun set(value: CT) {
entityClass.attach(entity)
with(entity) { column.setValue(entity, prop, value) }
}
}
class AttachWrappedHandle<WrappedType, DatabaseType, SuperEntityId : Any>(
private val column: EntityFieldWithTransform<DatabaseType, WrappedType>,
private val entity: Entity<SuperEntityId>,
private val entityClass: EntityClass<SuperEntityId, *>,
private val prop: KProperty<*>
) : AttatchHandler<WrappedType>() {
override fun get(): WrappedType = with(entity) {
column.getValue(entity, prop)
}
override suspend infix fun set(value: WrappedType) {
entityClass.attach(entity)
with(entity) { column.setValue(entity, prop, value) }
}
}
class AttachDelegate<ID : Any, E : Entity<ID>, CT : Any?>(
private val column: Column<CT>,
private val entityClass: EntityClass<ID, E>
) {
operator fun getValue(thisRef: E, property: KProperty<*>): AttachHandle<ID, E, CT> {
@Suppress("UNCHECKED_CAST")
return AttachHandle(column, thisRef, entityClass as EntityClass<ID, *>, property)
}
}
class AttachWrappedDelegate<WrappedType, DatabaseType, SuperEntityId : Any>(
private val column: EntityFieldWithTransform<DatabaseType, WrappedType>,
private val entityClass: EntityClass<SuperEntityId, Entity<SuperEntityId>>
) {
operator fun getValue(
thisRef: Entity<SuperEntityId>,
property: KProperty<*>
): AttachWrappedHandle<WrappedType, DatabaseType, SuperEntityId> {
@Suppress("UNCHECKED_CAST")
return AttachWrappedHandle(column, thisRef, entityClass, property)
}
}
suspend operator fun <T : Number> AttatchHandler<T>.plusAssign(value: T) {
val result = when (value) {
is Int -> (get().toInt() + value.toInt())
is Long -> (get().toLong() + value.toLong())
is Double -> (get().toDouble() + value.toDouble())
is Float -> (get().toFloat() + value.toFloat())
else -> error("Unsupported numeric type: ${value::class}")
}
set(result as T)
}
suspend operator fun <T : Number> AttatchHandler<T>.minusAssign(value: T) {
val result = when (value) {
is Int -> (get().toInt() - value.toInt())
is Long -> (get().toLong() - value.toLong())
is Double -> (get().toDouble() - value.toDouble())
is Float -> (get().toFloat() - value.toFloat())
else -> error("Unsupported numeric type: ${value::class}")
}
set(result as T)
}
suspend operator fun <T : Number> AttatchHandler<T>.divAssign(value: T) {
val result = when (value) {
is Int -> (get().toInt() / value.toInt())
is Long -> (get().toLong() / value.toLong())
is Double -> (get().toDouble() / value.toDouble())
is Float -> (get().toFloat() / value.toFloat())
else -> error("Unsupported numeric type: ${value::class}")
}
set(result as T)
}
suspend operator fun <T : Number> AttatchHandler<T>.timesAssign(value: T) {
val result = when (value) {
is Int -> (get().toInt() * value.toInt())
is Long -> (get().toLong() * value.toLong())
is Double -> (get().toDouble() * value.toDouble())
is Float -> (get().toFloat() * value.toFloat())
else -> error("Unsupported numeric type: ${value::class}")
}
set(result as T)
} |
… entity which is already modified in the current cache.
|
Ill test it shortly. But what exactly does this mean? Like do I need to refresh it all the time before I can use it or sth? |
|
HacktheTime Thank you for rising the issues, I really appreciate it. It's the first version of the module and I expect that there are many edge cases which are not covered in the code yet. I understood the problem with But from my perspective we should not be able to attach the entity if it's already in the cache and was modified, because in this case we will silently loose updates. So I extended |
I think I have a good way to rephrase my question and make it more specific. Attaching is supposed to make the entity reuseable in another transaction right? But if I cant reuse it what is the point of attach or sth? Is attach essentially a helper for bad design that now just has stricter limits? Essentially an may cause Issues in some cases and bad design but im will add something with which you can do it anyway? And my usecase might be limitated with that so its an issue with my code stylewise already? Also I think that freeze Issue still persists. I am trying to make a reproduceable test in just exposed codebase but havent been successful just yet. Also for future development. I think it would be good to in the future make the ongoing coding jitpack compatible. That way testing is easier and to avoid merge issues etc as a whole. (If I remeber the sources dont get generated correctly by it either) |
|
I have finally found a trick to have a look at the issue. But I wasnt able to make a test for it really. Main stacktrace: Extra (after main) (connected via BBUser.fromUserId from main) Extra (after main) (connected via BBUser.fromUserId from main in the BBUserType get()) |
Leonid Stashevsky (e5l)
left a comment
There was a problem hiding this comment.
left some comments, please check them out.
I'm still reviewing the core part of the PR
| @@ -1,7 +1,5 @@ | |||
| distributionBase=GRADLE_USER_HOME | |||
There was a problem hiding this comment.
Should we have a separate exposed-r2dbc-dao-sample?
| @@ -0,0 +1,129 @@ | |||
| package org.jetbrains.exposed.v1.dao.r2dbc.tests.shared | |||
There was a problem hiding this comment.
Missing Test suffix in the file name and class name
| import org.jetbrains.exposed.v1.dao.r2dbc.ExperimentalR2dbcDaoApi | ||
|
|
||
| /** | ||
| * An exception that provides information about an [entity] that could not be accessed |
There was a problem hiding this comment.
please consider simplify the KDoc
| import org.jetbrains.exposed.v1.dao.r2dbc.ExperimentalR2dbcDaoApi | ||
| import java.util.UUID | ||
|
|
||
| /** Base class for an [Entity] instance identified by an [id] comprised of a wrapped [java.util.UUID] value. */ |
There was a problem hiding this comment.
Please consider dropping code explaining KDocs. The documentation for these classes supposed to explain how to use them and when. please check other KDocs as well
| } | ||
|
|
||
| override fun addAll(elements: Collection<T>): Boolean { | ||
| val toAdd = elements.filter { it !in set } |
There was a problem hiding this comment.
if elements has the same element twice it will be added both times
|
|
||
| /** | ||
| * API marked with this annotation is experimental. | ||
| * The shape of the R2DBC DAO API may change in incompatible ways while it stabilizes. |
There was a problem hiding this comment.
| * The shape of the R2DBC DAO API may change in incompatible ways while it stabilizes. | |
| * The shape of the R2DBC DAO API may change in incompatible ways while it stabilizes even in patch releases. |
| // carried over into a new transaction. Both are cleared even though writeValues should not | ||
| // have reached readValues at this point. | ||
| // | ||
| // TODO ALIGN_WITH_JDBC: when ImmutableCachedEntityClass is ported, preserve its _readValues here. |
There was a problem hiding this comment.
please log a YT issue for that
| } | ||
|
|
||
| /** | ||
| * Property delegate for [CompositeColumn] — splits [value] into its real-column parts via |
There was a problem hiding this comment.
please consider giving example of how to use it instead of explaining the code
| * In case of a transaction failure, both [writeValues] and [readValues] are cleared before rollback | ||
| * to ensure that no stale data is carried over into a new transaction. | ||
| */ | ||
| val writeValues = LinkedHashMap<Column<Any?>, Any?>() |
There was a problem hiding this comment.
should this be public?
|
I have gotten things working it seems (with coroutine downgraded to 1.10.2). Curious to see how well it works in production (hobby project) I personally dislike the constant swapping between = and set all the time. In my opinion a constant suspend invoke() and infix set is the cleaner solution. that would need an adjustment of the getValue Delegate in Entity mainly. I also went in and added support for references to be more directly accessible in entity via delegates. This way I can define a custom helper method such as user() and define the user column(reference table and id column ) once and afterwards i don't have to bother about it anymore to use reffersOnsuspend etc. |


No description provided.