From: Gavin_King/Cirrus%<CI...@ci...> - 2002-05-30 01:57:40
|
QUOTE: ====================================================================== Gavin - I readily admit that the following are random thoughts which are only somewhat related to lifecycle objects. . . First off, the relationship between the layers of real world business objects, OO modeling of business objects, and relational database persistence is murky at best. Hibernate does a great job between the bottom two layers, but there are naturally a lot of places where the mappings between the layers is difficult. A change in state of business objects should represent a *real* change in state, and that change should be persisted (assuming the change is committed, etc) regardless of the dependency or hierarchy of those business objects. For example, if a Person is loaded with a many-to-one child entity of Shoes, changes made to the Person's Shoes are no more or less real then changes made to the Person. The same would be true if a new Person were created with Shoes. These types of changes are examples where the real world and the OO world mesh really well. Things get more interesting when business objects are removed (avoiding the term delete). If a Person is removed, he will take his Shoes with him, but he will leave behind his Projects. In this case, the dependency / hierarchy of the business objects determines how their persistence is handled. Even more complex, Shoes may or may not be company issue - Person's take non-Company issue Shoes with them when they leave, but Company issue Shoes stay with the Company. In this case its a flag in the child entity or the fact that the child has multiple parents that prevents it from being removed. Here the real world and the persistence layer mesh nicely allowing Shoes to persist without a Person, while the OO world would have GC'ed the Shoes. To completely blur the lines and dive into some of the implementation details, conceptually what should happen when an existing (persisted) Person is given new Shoes to replace his old ones? What if the Person gives his old Shoes to a different Person? . . . . The first & second design goals of Hibernate - Support for Java programming constructs & natural OO and declarative programming model - really imply the idea of a transparent persistence service, persistence that just *there* without having to worry about it, "Just use objects the way you always have." The key exception would be the concept of explicit deletes, which is actually an improvement on OO for many business purposes. Towards those goals, I would assume that all entities recursively cascade save() and update() unless told not to by an overloaded method (or visa versa for backwards compatibility). In the real world and the OO world, the state of a parent includes the state of its children, so a cascading, recursive behavior is intuitive. The decision to cascade saves and updates is really based on efficiency, and only the application can know when to take advantage of persisting select portions of the graph - thus I would not want to make the decision static by placing it in the mapping file. One other reason to *not* cascade saves and updates would be architecture (such as ejb), but the original implementation of save and update would still be available. Second, after taking the other cascading operations out of the mapping file, I would use the tag 'dependant' to indicate a relationship in which deleting the parent results in the child being deleted. Also, since the child lives and dies with the parent, I would allow de-referencing by the parent to implicitly delete the child if the call to parent.update() is allowing cascades. Third, merge the functionality of insert(), save(), and update() into one new method (persist?). Assuming that these operations cascade, it becomes a chore to keep track of which portions of your graph are new (save) or existing (update). For a cascading persist operation, persist() could walk the tree and assume that entities with assigned IDs are existing and those without IDs are new. One place where this could be a problem is an implicit delete. If PersonA gives her dependent Shoes to PersonB, Shoes will be deleted. When PersonB is persisted, Shoes will already have an ID but will not be in the database. I would be prepared to catch this type of SQL error and assume the above scenario, recovering by using an INSERT instead of an UPDATE. Finally, my most questionable idea - add a PersistentLifcycle method: boolean impliciteDelete(Session, ParentEntity) to allow dependant entities to determine if they should be deleted. This would give dependant entities a chance to examine their own state for flags (Company issue or non-Company issue) and possibly check for other parents who might be holding a reference to them. Checking for parent references could get messy. . . I don't have a good answer for this. And there you have it. I'm interested to know if you think this is a valid direction for Hibernate. Apologies if I paraphrased your design goals to much for my own purposes :) If you think this will generate useful discussion, feel free to post it to the public or dev forum. Thanks for your ear, Eric Everman ================================================================== >The decision to cascade saves and updates is really based on efficiency, >and only the application can know when to take advantage of persisting >select portions of the graph - thus I would not want to make the decision >static by placing it in the mapping file. In my opinion, the current design gives maximum flexibility here. There is a tension between two conflicting goals: * dont want to update() objects unecessarily or delete() objects incorrectly * dont want to force the application to have to spell out every single save/update/delete in code I think what we have is a good balance. Hibernate never does an update or delete unless you explicitly request it *somewhere*. However, by specifying it in the mapping file, you can save having to request it *every time*. >Second, after taking the other cascading operations out of the mapping >file, I would use the tag 'dependant' to indicate a relationship in which >deleting the parent results in the child being deleted. Also, since the >child lives and dies with the parent, I would allow de-referencing by the >parent to implicitly delete the child if the call to parent.update() is >allowing cascades. The first part of this (cascading deletes without cascading save + update) can be addressed by allowing cascade="delete" in the mapping file. This was something I was prepared to add anyway. The second part - deleting objects dereferenced by the parent - is in general a Hard Problem. You would probably be able to make it work for children one level deep fairly easily - but anything beyond that would have very deep implications for the complexity + efficiency of Hibernate. I already implemented something like this for collections. The only way I could really make it work with any expectation of efficiency was with the following restrictions: * collections may be referenced by exactly one parent object/collection * collections may only be retrieved by retrieving their parent Even then, the code for collections got real hairy, as anyone who has looked at RelationalDatabaseSession can testify. Basically I regularly start itching to rip out support for subcollections/toplevel-collections purely for the purpose of simplifying understandability of that code. > Finally, my most questionable idea - add a PersistentLifcycle method: > boolean impliciteDelete(Session, ParentEntity) > to allow dependant entities to determine if they should be deleted. > This would give dependant entities a chance to examine their own state > for flags(Company issue or non-Company issue) and possibly check for > other parents who might be holding a reference to them. Checking for > parent references could get messy. . . I don't have a good answer for > this. If this would be useful, It would be trivial to do. I would add a new interface: interface CascadeVetoer { public boolean vetoCascade(Object parent, Operation action); } or something...... But would it really be _that_ useful? >Third, merge the functionality of insert(), save(), and update() into one >new method (persist?). Assuming that these operations cascade, it becomes >a chore to keep track of which portions of your graph are new (save) or >existing (update). For a cascading persist operation, persist() could walk >the tree and assume that entities with assigned IDs are existing and those >without IDs are new. This would be very nice. I think we can actually just add this behaviour straight into update(), with rules like: 1. if reached object is associated with session, ignore it (currently throws exception) 2. if reached object has no id property, throw exception (current behaviour) 3. if reached object has a null id, save it (currently throws exception) 4. if reached object has non-null id, update it (current behaviour) The most problematic is (3). How do we detect null values for primitive ids? I guess we just consider that an unsupported case.... One further complication: foo -> bar -> baz foo, baz need updating. bar needs saving. Under the semantics I just proposed, session.update(foo) would cascade to session.save(bar). I suspect that what you would really want is for the next cascade to be session.update(baz), not session.save(baz) so we would need to remember that we are cascading an update, not just a save. This way, we would support two distinct programming styles * find/update/delete (with versioned data) * find/load/save/delete (with versioned or unversioned data) Oh, I just realised something I forgot about before. At present insert() cascades to save(). Is this best? Should insert() cascade to insert()? Don't know what is the best semantics here.... peace Gavin |