feat: EXPOSED-819 Exposed R2DBC DAO - #2831
Oleg Babichev (obabichevjb) wants to merge 21 commits into
Conversation
R2DBC DAO API — Key Differences from JDBC DAO
This document highlights behavioral and structural differences between the JDBC DAO ( 1. Relationship properties:
|
| Kind | Read | Write |
|---|---|---|
referencedOn / optionalReferencedOn |
film.director() |
film.director.set(other) |
referrersOn / optionalReferrersOn |
director.films is a SizedIterable<Film> |
read-only |
backReferencedOn / optionalBackReferencedOn |
film.review() — property type is suspend () -> Review |
read-only |
via |
film.actors.toList() |
film.actors = SizedCollection(a1, a2) |
Many-to-many stays a var, as in JDBC: assigning a collection rewrites the link table rows.
2. Creating entities: newSuspend { } is the JDBC new { }
JDBC's new { } issues the INSERT before it returns. Its R2DBC counterpart is newSuspend { }, which
schedules the insert, flushes, and returns a hydrated entity — its init block suspends too, so other
DAO operations can be called from inside:
// JDBC
val broker = Broker.new { name = "Alice" }
broker.id.value // INSERT already happened
// R2DBC
val broker = Broker.newSuspend { name = "Alice" }
broker.id.value // INSERT already happenednew { } also exists, and does something deliberately different: it schedules the insert without any
database access, which is what makes it callable from a non-suspending context — an entity init block,
a factory function, a builder that assembles an object graph before touching the connection:
// R2DBC
val broker = Broker.new { name = "Alice" } // nothing sent yet
broker.id.value // throws IllegalStateException("Entity must be inserted")
flushCache() // now the INSERT goes outThe id is missing because the row does not exist yet; it is available immediately when it does not come
from the database (an explicit new(id) { } argument, or uuid("id").autoGenerate()). Column reads and
writes work before the flush, and so does using the entity as a reference target — the child stores the
very EntityID the parent's insert fills in, and the cache orders inserts so the parent goes first.
flushCache() is rarely required. A pending insert is issued at the first of:
- an explicit
flushCache()orEntityCache.flush(); - any other statement in the transaction — a query flushes the entities of the tables it reads, while an
insert, update, upsert, delete or DDL statement flushes everything pending; - the transaction's commit.
Everything pending for one table goes out as a single batch statement, so scheduling several entities and
flushing once is the cheapest way to insert many rows through the DAO.
3. suspend on all I/O methods
| Method | JDBC | R2DBC |
|---|---|---|
EntityClass.new(init) |
fun new(init): T |
fun new(init): T — deferred, no I/O |
| — | — | suspend fun newSuspend(init): T |
EntityClass.findById(id) |
fun findById(id): T? |
suspend fun findById(id): T? |
EntityClass[id] |
operator fun get(id): T |
suspend operator fun get(id): T |
EntityClass.count(op) |
fun count(op): Long |
suspend fun count(op): Long |
EntityClass.reload(entity) |
fun reload(entity, flush): T? |
suspend fun reload(entity, flush): T? |
EntityClass.warmUpReferences |
fun (…, forUpdate, orderBy) |
suspend fun (…, orderBy) — no forUpdate |
Entity.flush(batch) |
fun flush(batch): Boolean |
suspend fun flush(batch): Boolean |
Entity.delete() |
fun delete() |
suspend fun delete() |
EntityCache.flush() |
fun flush() |
suspend fun flush() |
Transaction.flushCache() |
fun Transaction.flushCache() |
suspend fun R2dbcTransaction.flushCache() |
Hook subscriptions accept suspend lambdas, and EntityLifecycleInterceptor implements
GlobalSuspendStatementInterceptor, so its callbacks suspend as well. Column reads and writes on a
tracked entity stay non-suspending, as do new, detach, find, all, wrapRow and testCache(id).
4. Collections return SizedIterable backed by Flow
R2DBC's SizedIterable<T> extends Flow<T> rather than Iterable<T>, and its count()/empty()
suspend, so collecting takes an explicit toList():
// JDBC
director.films.map { it.title } // eager Iterable.map
// R2DBC
director.films.toList().map { it.title } // collect the Flow, then mapFlows are cold, which is the porting hazard worth spelling out: sizedIterable.map { } is a Flow
operator, so every terminal operator applied to its result re-executes the query, where the
identical-looking JDBC expression runs once.
This also affects eager loading — R2DBC needs two with() overloads (one for SizedIterable, one for
Iterable) where JDBC has one.
5. Explicit attach() / detach() for cross-transaction entity reuse
JDBC auto-attaches an entity when you set a column value, because Column.setValue can query the
database synchronously. R2DBC's setValue cannot suspend, so adoption is explicit — and a write to an
untracked entity throws EntityNotFoundException rather than being silently dropped.
// R2DBC
suspendTransaction {
Broker.attach(broker) // verifies the row exists, then tracks this instance
broker.name = "Bob" // now safe to modify; flushed by the commit
}suspend fun attach(entity, force = false) does nothing if the instance is already tracked; stores it
after verifying the row exists; and replaces a different tracked instance of the same row — unless that
one holds unflushed changes, in which case it throws instead of dropping them. force discards them.
fun detach(entity, force = false) is the inverse and needs no database access: it stops tracking the
entity so it is not flushed at commit, reads keep returning committed values while writes throw, and it
detaches from enclosing transactions too, since tracking spans the whole chain. It throws for
uncommitted values unless force is passed, and is safe to call twice.
6. Missing JDBC features (not yet ported)
ImmutableEntityClass/ImmutableCachedEntityClass— immutable entities with cross-transaction
caching, plusforceUpdateEntityandexpireCacheViewandEntityClass.view { }findWithCacheCondition/testCache(cacheCheckCondition)— theSequence-returning cache scans;
testCache(id)is availablewrapRows(rows, alias)— both alias overloads; the single-rowwrapRow(row, alias)forms existEntityCache.maxEntitiesToStore— the per-entity cache cap is not implementedEntity.writeValues/storeWrittenValues()/lookupInReadValues()— superseded by an internal
staged-values modelforUpdateparameter ofwarmUpReferences/warmUpOptReferences— still onwarmUpLinkedReferencesreferrersOn(table, cache)/optionalReferrersOn(table, cache)— the composite-FK overloads that
also take the cache flag; the(column, cache)and plain(table)forms exist
|
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, |
|
Oleg Babichev (@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>> |
✅ | ✅ | Same. |
var db |
Database |
R2dbcDatabase |
Type changed. |
val writeValues |
✅ | REMOVED | Pending writes are held by the cache's staged-values model. |
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 issues no DELETE when the entity was never flushed — it just drops the scheduled insert. |
fun flush(batch: EntityBatchUpdate? = null): Boolean |
✅ | ✅ | Same. |
fun storeWrittenValues() |
✅ | REMOVED | Staging is internal in R2DBC. |
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 Reference.provideDelegate → Accessor. |
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 / wrapRow(row, alias) |
✅ | ✅ | Same. |
fun wrapRows(rows) |
✅ | ✅ | R2DBC has no wrapRows(rows, alias) overloads. |
fun reload(entity, flush) |
✅ | ✅ | Same. |
fun removeFromCache(entity) |
✅ | ✅ | Same. |
fun forEntityIds(ids) / fun forIds(ids) |
✅ | ✅ | Same. |
fun testCache(id) |
✅ | ✅ | Same. |
fun testCache(cacheCheckCondition) |
✅ | ❌ | Missing. The Sequence-returning cache scan. |
fun new(init): T / fun new(id, init): T |
✅ | ✅ | Semantic change. Non-suspend in R2DBC and only schedules the insert; the id stays unset until the flush. |
fun newSuspend(init): T / fun newSuspend(id, init): T |
— | ✅ | R2DBC-only, and the real counterpart of JDBC's new { }: schedules, flushes, returns a hydrated entity. |
fun attach(entity, force = false) |
— | ✅ | R2DBC-only. Verifies the row exists, then tracks this instance. |
fun detach(entity, force = false) |
— | ✅ | R2DBC-only. Non-suspend; stops tracking across the whole transaction chain. |
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 |
✅ | ✅ | R2DBC drops the forUpdate parameter. |
fun warmUpLinkedReferences |
✅ | ✅ | Same parameters. |
infix fun referencedOn / optionalReferencedOn / referrersOn / optionalReferrersOn / backReferencedOn / optionalBackReferencedOn |
Member on EntityClass |
Member on EntityClass |
Same location and call-site syntax; return types differ (see relationships section). R2DBC needs no backReferencedOnOpt — its backReferencedOn takes an unconstrained REF — and drops the composite-FK referrersOn(table, cache) overloads. |
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<*>, ...> |
✅ | ✅ | LinkedHashMap in JDBC, ConcurrentHashMap in R2DBC. |
fun find(klass, id) |
✅ | ✅ | Same. |
fun store(entity) |
✅ | ✅ | Same. |
fun store(klass, entity) |
✅ | ❌ | Missing. Only the single-argument form. |
fun remove(table, entity) |
✅ | ✅ | Same. |
fun findAll(klass) |
✅ | ✅ | Returns Collection in JDBC, List in R2DBC. |
fun scheduleInsert(klass, entity) |
✅ | ✅ | Same. |
fun scheduleUpdate(klass, entity) |
✅ | ✅ | Same. |
fun getOrPutReferrers(sourceId, key, refs) |
✅ | ✅ | Same parameter order; R2DBC takes a suspending producer. |
fun getReferrers(sourceId, key) |
✅ | ✅ | Same. |
fun clear(flush) |
✅ | ✅ | Same. |
fun clearReferrersCache() |
✅ | ✅ | Same. |
fun flush() / fun flush(tables) |
✅ | ✅ | Same. |
var maxEntitiesToStore |
✅ | ❌ | Missing. R2DBC has no per-entity cache cap. |
companion object: fun invalidateGlobalCaches(created) |
✅ | ❌ | Missing. Tied to ImmutableCachedEntityClass. |
EntityHook / EntityChange
Same public API in both modules — subscribe, unsubscribe, EntityChange(EntityClass, EntityID, EntityChangeType) — except that R2DBC's subscribe/unsubscribe take a suspend (EntityChange) -> Unit, and so does withHook. 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 the returned Reference is Accessor-based. |
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 name; returns a public BackReference instead of a bare ReadOnlyProperty. |
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) |
| 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() — the property type is suspend () -> Parent |
| 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; separate SizedIterable and Iterable overloads) |
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<...> (internal in JDBC) |
BackReference<...> — public, getValue returns suspend () -> Parent |
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. R2DBC's DaoEntityID also does not override invokeOnNoValue, so an unflushed id throws instead of auto-flushing. |
New classes (R2DBC has them; JDBC doesn't)
| R2DBC class | Purpose |
|---|---|
Accessor / OptionalAccessor |
Runtime many-to-one accessor (suspend invoke() to read, set(value) to write). |
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.
0db09a4 to
932cda2
Compare
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 |
|
Oleg Babichev (@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)
} |
|
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. |
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?
There was a problem hiding this comment.
I agree, especially because I got confused initially by the choice of the word -showcase.
I would vote for splitting this into 2 separate and dedicated samples -> exposed-jdbc-dao-sample + exposed-r2dbc-dao-sample.
Could the addition of these samples also please be removed into its own separate PR? Would make reviewing this again in the future a little bit easier 🙏
There was a problem hiding this comment.
I will move it to separate PR
| * 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?
There was a problem hiding this comment.
It (and related readValues) have always been public for JDBC DAO
There was a problem hiding this comment.
This field doesn't exist anymore, but visibility of cache data could be revisited anyway.
|
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. |
| @@ -1,7 +1,5 @@ | |||
| distributionBase=GRADLE_USER_HOME | |||
There was a problem hiding this comment.
I agree, especially because I got confused initially by the choice of the word -showcase.
I would vote for splitting this into 2 separate and dedicated samples -> exposed-jdbc-dao-sample + exposed-r2dbc-dao-sample.
Could the addition of these samples also please be removed into its own separate PR? Would make reviewing this again in the future a little bit easier 🙏
| * 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.
It (and related readValues) have always been public for JDBC DAO
5f438ff to
862d338
Compare
* 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
…BC-to-R2DBC migration docs and standalone showcase build
… entity which is already modified in the current cache.
…-r2dbc-dao-sample
c1ecb8a to
573f980
Compare
I would give that question more attention, and check what is the primary use case of reattaching entities between transactions. Does it allow to do something better what could not be done just by reading entities from database? Is the reattached entities more or less just a container for entity class and entity id? |
d55880f to
e45da8b
Compare
|
I haven't synchronised the code in a month or so but had some issues recently I am gonna look into in the next few days if there is a occasion to do so. Wanted to share some of these Issues but again note that this may be only due to outdated code! I believe this came after the entity existed for quite some time already. DetailsType: IllegalStateException Suppressed (submission trace): Message: The connection is closed. Unable to send anything DetailsType: ExposedR2dbcException Message: The connection is closed. Unable to send anything SQL: (normal statement here) org.jetbrains.exposed.v1.dao.r2dbc.EntityClass.findById(EntityClass.kt) org.jetbrains.exposed.v1.dao.r2dbc.EntityClass.findById(EntityClass.kt:228) ….abuse.IPAbuseHandler$getIpReport$2$reportInDb$1(IPAbuseHandler.kt:108) ┌ Repeating 2 Times [Block 1] ├ ….abuse.IPAbuseHandler$getIpReport$2$reportInDb$1(IPAbuseHandler.kt:-1) └End of Repeat [Block 1] kotlinx.coroutines.intrinsics.UndispatchedKt.startUndspatched(Undispatched.kt:66) kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43) kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:165) kotlinx.coroutines.BuildersKt.withContext(Unknown Source) ….abuse.IPAbuseHandler$getIpReport$2(IPAbuseHandler.kt:108) ┌ Repeating 2 Times [Block 2] ├ ….abuse.IPAbuseHandler$getIpReport$2(IPAbuseHandler.kt:-1) └End of Repeat [Block 2] ….abuse.IPAbuseHandler(IPAbuseHandler.kt:97) ….abuse.IPAbuseHandler(IPAbuseHandler.kt:107) ….abuse.IPAbuseHandler(IPAbuseHandler.kt:374) ….modserver.BingoNetServer(BingoNetServer.kt:102) ….modserver.BingoNetServer$start$1(BingoNetServer.kt:-1) Caused By: Type: R2dbcNonTransientResourceException The connection is closed. Unable to send anything org.jetbrains.exposed.v1.dao.r2dbc.EntityClass.findById(EntityClass.kt) org.jetbrains.exposed.v1.dao.r2dbc.EntityClass.findById(EntityClass.kt:228) ….abuse.IPAbuseHandler$getIpReport$2$reportInDb$1(IPAbuseHandler.kt:108) ┌ Repeating 2 Times [Block 1] ├ ….abuse.IPAbuseHandler$getIpReport$2$reportInDb$1(IPAbuseHandler.kt:-1) └End of Repeat [Block 1] kotlinx.coroutines.intrinsics.UndispatchedKt.startUndspatched(Undispatched.kt:66) kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:43) kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:165) kotlinx.coroutines.BuildersKt.withContext(Unknown Source) ….abuse.IPAbuseHandler$getIpReport$2(IPAbuseHandler.kt:108) ┌ Repeating 2 Times [Block 2] ├ ….abuse.IPAbuseHandler$getIpReport$2(IPAbuseHandler.kt:-1) └End of Repeat [Block 2] ….abuse.IPAbuseHandler(IPAbuseHandler.kt:97) ….abuse.IPAbuseHandler(IPAbuseHandler.kt:107) ….abuse.IPAbuseHandler(IPAbuseHandler.kt:374) ….modserver.BingoNetServer(BingoNetServer.kt:102) ….modserver.BingoNetServer$start$1(BingoNetServer.kt:-1)PS: I updated the code just now. will inform of things if i see anything. |
Also I thought about your message here again. One instance where i have long existing entities right now is for connections from clients. If i permanently need to re request them from the database that seems at minimum annoying if not inefficient (idk if a change listener that updates values live is possible). |
e45da8b to
4f87e71
Compare


No description provided.