j256 / ormlite-core

Core ORMLite functionality that provides a lite Java ORM in conjunction with ormlite-jdbc or ormlite-android
http://ormlite.com/
ISC License
577 stars 212 forks source link

Multiple object instances due to foreign reference cycles? #278

Open ewills opened 1 year ago

ewills commented 1 year ago

I've got a scenario where a DatabaseTable A has an eager ForeignCollectionField referring to table B, which has a foreign DatabaseField referring to table C, which has a foreign field referring back to table A. When querying for an uncached instance of A, I'm seeing:

  1. Check the cache (weak ReferenceObjectCache) for a cached instance when mapping rows and return it https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/stmt/mapped/BaseMappedQuery.java#L49 otherwise:
  2. Create a new instance https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/stmt/mapped/BaseMappedQuery.java#L54
  3. Iterate resultsFieldTypes https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/stmt/mapped/BaseMappedQuery.java#L58
  4. Recursively assign fields, eventually calling createForeignObject to create a new instance of A while iterating the fields on an instance of C https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/field/FieldType.java#L574
  5. The instance of A from step 4 is hydrated and added to the object cache https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/field/FieldType.java#L1077
  6. The instance of A from step 2 is added to the object cache https://github.com/j256/ormlite-core/blob/master/src/main/java/com/j256/ormlite/stmt/mapped/BaseMappedQuery.java#L94

At this point I've got the instance of A from step 2 (and 6) in my object cache along with an instance of C that has a foreign DatabaseField referring to the instance of A from step 4.

I could avoid this particular scenario by lowering maxForeignAutoRefreshLevel on either B's reference to C or C's reference to A, but we add columns to our data model often and I'd rather not consider these possible cycles each time we do this!

So my question is: would it make sense to modify BaseMappedQuery mapRow() to add newly-created instances to the object cache prior to resolving foreign references (i.e., between steps 2 and 3 above)? That way (I think) the subsequent mapRow calls for the foreign references would be find the cached instance from step 2 rather than creating a new instance?

I can also imagine that it might be a bad idea to add partially-hydrated instances to the object cache, so this idea may be a nonstarter!

j256 commented 1 year ago

Sorry to not respond sooner @ewills. Interesting. You are pushing the limits of my experience in this area. It might be interesting to have 2 caches. One specifically for the call frame that is tracking the objects that are are partially hydrated so they don't get doubly created. Then the other to hold the hydrated objects. I agree that putting the partially hydrated instances in the cache might be bad unless there was some sort of lock in place.