-
-
Notifications
You must be signed in to change notification settings - Fork 260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Q: is there a way to invalidate lazy-loaded entity bean field? #3414
Comments
What database are you using? If it is Postgres then you could use the InsertOptions.ON_CONFLICT_UPDATE which does this atomically. For non-Postgres you could ponder if you want to use a SQL MERGE.
You could try using BeanState.setPropertyLoaded("properties", false) which was technically there for another reason (stateless update) but should do what you are looking for.
Well Postgres users would look to use InsertOptions.ON_CONFLICT_UPDATE for this case I'd say. |
hi @rbygrave and thanks for your answer. sadly our clients use different dbs for this, mostly SQL server and oracle. we do use postgres for our internal tables where we can choose, but not for this. yeah, pg upsert would be great for this, but I wasn't really trying to solve the conditional insert/update case here, we can live with that. our problem was with the already loaded bean having stale state afterwards. I'm not sure how I should get to the thanks again, your help is greatly appreciated. |
Something like: BeanState beanState = database.beanState(myEntityBean);
beanState.setPropertyLoaded("properties", false); |
Hmm, this doesn't seem to work when db is not default. When I access the property after calling jakarta.persistence.PersistenceException: No registered default server
at io.ebean.bean.InterceptReadWrite.loadBean(InterceptReadWrite.java:709)
at io.ebean.bean.InterceptReadWrite.preGetter(InterceptReadWrite.java:836)
at cz.sentica.qwazar.ea.core.entities.EaObject._ebean_get_properties(EaObject.kt:1)
at cz.sentica.qwazar.ea.core.entities.EaObject.getProperties(EaObject.kt:262) even though the entity has the |
This is kinda strange, when I add a break point at the start of |
created a minimal test for this, just to be on the same page: import cz.sentica.qwazar.ea.core.entities.EaObject
import cz.sentica.qwazar.ea.core.entities.EaObjectProperty
import cz.sentica.qwazar.ea.core.entities.query.QEaObject
import cz.sentica.qwazar.ea.core.entities.query.QEaObjectProperty
import io.ebean.DatabaseFactory
import io.ebean.config.CurrentUserProvider
import io.ebean.config.DatabaseConfig
import io.ebean.datasource.DataSourceConfig
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test
class OneToManyPropertyInvalidationTest {
private val dsConfig = DataSourceConfig().also {
it.username = "sa"
it.password = "sa"
it.url = "jdbc:h2:mem:eadb"
it.driver = "org.h2.Driver"
it.addProperty("quoteReturningIdentifiers", false)
it.addProperty("NON_KEYWORDS", "VALUE")
}
private val dbConfig = DatabaseConfig().also {
it.name = "ea"
it.isDefaultServer = false
it.currentUserProvider = CurrentUserProvider { "test" }
it.isDdlRun = true
it.isDdlGenerate = true
it.setDataSourceConfig(dsConfig)
it.setClasses(
listOf(
EaObject::class.java,
EaObjectProperty::class.java,
),
)
}
private val database = DatabaseFactory.create(dbConfig)
@Test
fun itWorks() {
val inserted = EaObject(packageId = 0, name = "test")
database.save(inserted)
database.save(EaObjectProperty(eaObject = inserted, property = "test", value = "initial"))
// this works np
QEaObjectProperty(database)
.where().eaObject.eq(inserted).property.eq("test")
.findOne()!!.value shouldBe "initial"
val found = QEaObject(database).where().id.eq(inserted.id).findOne()!!
// these work fine too
found.name shouldBe "test"
found.properties.first().property shouldBe "test"
found.properties.first().value shouldBe "initial"
QEaObjectProperty(database)
.where().eaObject.eq(inserted).property.eq("test")
.asUpdate().set(QEaObjectProperty._alias.value, "updated").update()
// correctly updated in db
QEaObjectProperty(database)
.where().eaObject.eq(inserted).property.eq("test")
.findOne()!!.value shouldBe "updated"
// this is out-of-date now, which is logical
found.properties.first().value shouldBe "initial"
database.beanState(found).setPropertyLoaded(EaObject::properties.name, false)
// this fails with
// jakarta.persistence.PersistenceException: No registered default server
// at io.ebean.bean.InterceptReadWrite.loadBean(InterceptReadWrite.java:709)
// at io.ebean.bean.InterceptReadWrite.preGetter(InterceptReadWrite.java:836)
// at cz.sentica.qwazar.ea.core.entities.EaObject._ebean_get_properties(EaObject.kt:1)
// at cz.sentica.qwazar.ea.core.entities.EaObject.getProperties(EaObject.kt:262)
// at cz.sentica.qwazar.ea.db.services.OneToManyPropertyInvalidationTest.itWorks(OneToManyPropertyInvalidationTest.kt:90)
found.properties.first().value shouldBe "updated"
}
} |
after I put breakpoints in both
|
ok, so the difference here is that for those in the last |
I'm not sure how to continue debugging this - I though I'd try to see where this |
Hmm, so I found a reference to Anyway, is there something more I can do about this at this point? Should I create a minimal repo with the above test? |
Now I found a really strange behavior. While reading through val beanList = (found.properties as BeanList)
beanList.reset(beanList.owner(), beanList.propertyName()) this actually seems to be working when debugging and if I pass all the way to the end, the test actually passes, but if I just run the test (without debug), it fails because the last |
further investigation shows that
|
so, as a final note on my investigation of this path (using private fun DefaultServer.refreshBeanInternal(bean: EntityBean) {
val desc = this.descriptor(bean.javaClass)
val pc = DefaultPersistenceContext()
val id = desc.getId(bean)
desc.contextPut(pc, id, bean)
val query = this.createQuery(desc.type())
// don't collect AutoTune usage profiling information
// as we just copy the data out of these fetched beans
// and put the data into the original bean
query.isUsageProfiling = false
query.setPersistenceContext(pc)
query.setMode(SpiQuery.Mode.REFRESH_BEAN)
query.setId(id)
// make sure the query doesn't use the cache
query.setBeanCacheMode(CacheMode.OFF)
val dbBean = query.findOne() ?: throw EntityNotFoundException(
"Bean not found during lazy load or refresh. Id:" + id + " type:" + desc.type(),
)
desc.resetManyProperties(dbBean)
} this does indeed work, but
yes, I know this isn't a solution that you proposed and there's nothing wrong with this not behaving as I hoped, I was just trying to find a different solution since I wasn't successful with the one you proposed (hopefully we can still work on that). on the other hand, I'm still not sure why |
so, going back to the original |
so, it seems I finally found a workable solution - after resetting the lazy-loaded property I can do @Suppress("CAST_NEVER_SUCCEEDS")
val intercept = (found as EntityBean)._ebean_getIntercept()
val context = intercept.persistenceContext()
context.clear(EaObjectProperty::class.java, changedPropId) after doing this, when I iterate over |
just for completeness, the complete current solution is: foundObject.properties.first().value shouldBe "initial"
val beanList = (foundObject.properties as BeanList)
beanList.reset(beanList.owner(), beanList.propertyName())
@Suppress("CAST_NEVER_SUCCEEDS")
val intercept = (foundObject as EntityBean)._ebean_getIntercept()
val context = intercept.persistenceContext()
context.clear(EaObjectProperty::class.java, changedPropId)
foundObject.properties.first().value shouldBe "updated" (both assertions pass) |
hmm, I encountered one (hopefully last) problem with this solution. this works well with a bean instance loaded from database, but if I create a new instance (using the bean class constructor), then save it in the database, then (after inserting/updating some properties related to it using direct query) I reset the property (as above), on the next access I get Cannot invoke "io.ebean.bean.BeanCollectionLoader.loadMany(io.ebean.bean.BeanCollection, boolean)" because "this.loader" is null
java.lang.NullPointerException: Cannot invoke "io.ebean.bean.BeanCollectionLoader.loadMany(io.ebean.bean.BeanCollection, boolean)" because "this.loader" is null
at io.ebean.common.AbstractBeanCollection.lazyLoadCollection(AbstractBeanCollection.java:90)
at io.ebean.common.BeanList.init(BeanList.java:136)
at io.ebean.common.BeanList.iterator(BeanList.java:322) I would expect loader to be the server that was used to save the instance, but this is not the case. BTW, I also tried to call |
Hi guys, I have a question regarding lazy-loaded bean fields. Let's say we have these two entities with 1-N relationship:
BTW there is a definite ownership here -
EaObjectProperty
makes no sense without theEaObject
, should die with it and will never be transferred to anotherEaObject
.And then we have a code like this:
The problem here is that if
eaObject.properties
have been access (and thus loaded) before calling this function, after calling it, the attribute is out-of-date, because theeaObject
instance has no way of knowing that the value in database has changed. So my question is 1) is there a way to invalidate theeaObject.properties
value on the instance so that it will be reloaded on next access and 2) if not, is there some better way to implement the update function which is reasonably efficient and will make the passed-ineaObject
instance aware of the change? To be honest, we're currently only using@OneToMany
attributes for reading, because 1) we've hit some problems with them not being correctly (from our perspective, not a bug, just different expectations) persisted on change in the past and 2) it's not very efficient to load all related obect if we just want to update one.The text was updated successfully, but these errors were encountered: