-
-
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
Adding entity to @OneToMany property doesn't create the relationship #3142
Comments
From reading through the mentioned issue I understand that removing items from the |
@Incanus3 I think this is similar to standard JPA. Bidirectional relations are usually persisted/updated from the owning side. In your case that would be In standard JPA you would always write code like this: public void addMember(Customer member) {
this.members.add(member);
member.setGroup(this);
} if you want to call |
@jnehlmeier Is right but there is something subtle that I can explain and in that sense explain how this could be considered a bug. With OneToMany with a cascade=PERSIST | ALL ... Ebean WILL automatically set the bi-directional relationship (so I this case set the Group on the Customer) IF the beans being cascaded to [Customer in this case] is in a NEW state or DIRTY state. That is, it is not working as you had expected here because the Customer bean instance is in LOADED state and not DIRTY. What you might have desired would be [when the cascaded bean is in loaded state and not dirty] for ebean to then check the relationship property, check the Group on the Customer instance and if that Group on the Customer was either NULL or different [by id value] to the cascading Group instance ... then automatically set that Group. That would make the Group property dirty on Customer and we'd see a So maybe this is a bug in that sense. Hmm. |
Hi guys and thanks for the quick replies.
Yeah, that's exactly what I have desired :)
Hmm, I didn't know about this and it does seem kinda inconsistent - why should it set the reversed ( But, and this is also in reaction to @jnehlmeier, there is actually a problem with this removal and that's actually a bigger problem for us (meaning harder to work around). We solved the reported problem by making the "primary" ebean-handled property private and adding a second property using a simple delegate: // in the entity:
@OneToMany(mappedBy = "category", cascade = [CascadeType.ALL])
private var _products: List<ProductResource> = arrayListOf()
@delegate:Transient
var products by OneToManyRelationship(
oneToManyProperty = ProductCategoryResource::_products,
manyToOneProperty = ProductResource::category,
)
// the delegate definition:
class OneToManyRelationship<OtM, MtO>(
private val oneToManyProperty: KMutableProperty1<OtM, List<MtO>>,
private val manyToOneProperty: KMutableProperty1<MtO, OtM?>,
) {
operator fun getValue(owner: OtM, property: KProperty<*>): List<MtO> = oneToManyProperty.get(owner)
operator fun setValue(owner: OtM, property: KProperty<*>, newValue: List<MtO>) {
val oldValue = oneToManyProperty.get(owner)
oneToManyProperty.set(owner, newValue)
val add = newValue - oldValue.toSet()
val remove = oldValue - newValue.toSet()
// these will be saved automatically when the OtM side is saved
add.forEach {
manyToOneProperty.set(it, owner)
}
// these entities will not be saved automatically when the OtM side is saved,
// because ebean save doesn't cascade into removed related entities
remove.forEach {
manyToOneProperty.set(it, null)
}
}
} As you can notice in the last comment, this works and correctly updates the related (many) entities, but the removed related entities aren't saved when the "primary" (one) entity is saved, so the relationship is not persistently removed. On one hand this is kinda to be expected, because the save only cascades to the related entities referenced by the current value (after removal) of the There are two main reasons that make this a problem for us:
Again, I'd like to stress I definitely don't expect the ebean maintainers to go and "fix" this for us, just because it would be convenient for us, but if you don't consider this an inconsistency that should be fixed, I'd really like to know if there is some kind of solution to work around this problem without mixing the "set properties" and the "save" steps. Also, as a side not, I noticed in some other issues (mainly #431) that there is (or used to be) a Sorry for this really long reaction, it got a bit out of hand, and thank you guys for all the great work you're doing, we really appreciate it. |
Ok, quite a lot here - I'm not sure I'll cover it all. Firstly we need some "big picture" about when cascading a OneToMany is appropriate I think. "Ownership" type relationship vs "Assignment" type relationshipFor myself I look to classify OneToMany relationships as "Ownership" or "Assignment". TLDR Ownership type relationships are appropriate for cascading (and orphan removal). "Ownership" type relationships can be identified by asking a few questions. Taking Orders and Order Lines as a classic example:
There is a strong "Ownership" relationship - An Order Line is "owned" by its associated Order. It frequently means the parent and child have are created at the exact same time and deleted at the exact same time / the parent and child share the same "Lifecycle". For "Assignment" type relationships we ask the same questions and get different answers. For the purpose of explaining this I'm going to use Customer and Group from above and I'll make up the answers.
In-Memory modify and FlushWith JPA, the approach in general terms is to modify the in-memory model (modify beans, add/remove them from collections etc) and then Flush the entire model to the database. This tends to encourage developers to think about adding/removing beans from collections and using cascade persist ... even for "Assignment type" relationships ... and even when this is a relatively bad idea. The problem with the "add/remove from collection and flush/cascade" type approach is that it requires the collection to be loaded into memory. If the collection is large this can really hurt but it can actually be inefficient even when the collection is small. If the only thing we are doing is "assignment" or "unassignment" [set the foreign key value] this can be done with a "stateless update" / single sql update statement and without loading any beans or collections into memory. If we are cascade persisting from Group to its associated Customers in order to unassigned a customer from the group that is a really inefficient way to do it compared to: customer.setGroup(null);
customer.save(); ... or a stateless update.
PrivateOwned was the old feature that became JPA "Orphan Removal" which is an attribute of Orphan removal almost always should go along with
I'd say because this is an "unassignment". It is the case of using
This is maybe why we don't have many people asking about this. That all said, we still could consider this a bug and perform the "unassignment" on cascade case.
If they are they are being pretty quite about it. I think the main point is to see if you can see "Ownership" vs "Assignment" type relationships and maybe see why I think its a bad idea to lean on cascade PERSIST for "Assignment" type relationships. For example, if a Group can have thousands of Customers in its collection then it's pretty obvious adding/removing from the Group customers collection is getting expensive and doing "assignment" / "unassignment" via that approach is not good. Note for JPA folks. With JPA we generally always need the in-memory model to reflect what we want to persist. With Ebean we explicitly specify what we persist (insert, update, delete) and can even turn off cascade on a per transaction basis. That means with Ebean we only care about the in-memory model state of the beans that we are explicitly persisting. For example, we can add/remove beans from collections and that only matters if we cascade persist that collection. This is why JPA folks will tend to follow Jen's example for maintaining bi-directional relationships correctly all the time but some Ebean folks will not bother doing that. |
Hi guys, I'm sorry for the long delay in responding, I was away for a few weeks and then I completely forgot about this. As for @rbygrave's response, everything you say makes perfect sense to me, when viewed with the mindset you describe, especially when talking about "assignment" type relationship, although I'm a bit surprised there aren't more people struggling with this. Maybe people just understand this distinction without having to be told like me :). But, if I were sure my specific relationship (not the customer-group one I've used in the example, I've only used that for the sake of familiarity, our domain in this case is actually document-subdocuments, which is basically the order-items type), and if I didn't mind the inefficiency of having to load the whole collection into memory, am I to understand that if I annotate it with Thank you again for your patience with me. |
… isn't new or dirty That is, previously the relationship back from the child to the parent was being updated when the child bean was new or dirty BUT NOT in the case when the child bean was loaded and unchanged. This fix includes that case updating the ManyToOne side of the relationship on the child when the save is cascaded.
… isn't new or dirty (#3366) That is, previously the relationship back from the child to the parent was being updated when the child bean was new or dirty BUT NOT in the case when the child bean was loaded and unchanged. This fix includes that case updating the ManyToOne side of the relationship on the child when the save is cascaded.
Expected behavior
When I add entry to
@OneToMany
property or reset it to a new list containing the entry and save the referencing entity, I'd expect this new relationship to be persisted.Actual behavior
Neither the referencing entity's
@OneToMany
property, nor the referenced entity's@ManyToOne
property reflects the new relationship after reload.Steps to reproduce
BeanList
instead of replacing the whole list, but it doesn't solve the issueserver.save(devs)
call and the save correctly cascades to theDefaultPersister.saveAssocMany()
method where themembers
property is processed andsaveRecurse()
is called withrob
, but when theCustomer.group
property is processed here, theprop.valueAsEntityBean(request.entityBean())
call returnsnull
, which means the reversed property's value is never set.The text was updated successfully, but these errors were encountered: