#418 open
Hax

Inconsistent views when creating and accessing associations

Reported by Hax | June 26th, 2008 @ 10:13 AM

Just getting started with DM and I apologise if I have misunderstood the scope of the identity map semantics and intended behaviour of associations.

With a basic join model, accessing the association from the "has side" before the join model is saved leaves the association/collection empty even after the save occurs.

See pastie for details http://www.pastie.org/222592

Not sure if this is intended, but it seems to break the identity map / object equivalence illusion as you can obtain another instance of the object from the model after saving the join table and the association collection on that instance now works. Even though obj1 == obj2 => true, the instances behave quite differently. Is this intended ?

Also, from the join table (ie the belongs_to side) the association does indeed exist (even before the save). It seems inconsistent/unexpected that the association is only visible from one side, and only visible from the other side after saving ... and then only if you haven't previously attempted to access the association.

The identity map seems to only actually apply to the key field(s) so while the object identities are the same, the actual object instances are different and their contents are thus independent and can actually be quite different, eg:

obj1 = Model.first

obj2 = Model.first

obj1 == obj2

=> true

ob1.name = "Ben"

obj2.name = "Joe"

obj1 == obj2

=> true

obj1.name == obj2.name

=> false

Is this intended ? Documentation may need to cover this unless I missed it somewhere.

Q: In light of transactions, would/could the approach being used/planned only create one actual object instance for each database identity referenced within a transaction context ? Changes to an object/object-graph would remain isolated to that transaction context until the transaction commits. I realise this is probably a can of worms ... but :) extending the database notion of transaction isolation to the in-memory object model would be really cool especially for maintaining object graph consistency in a multi-threaded server.

Comments and changes to this ticket

  • Dan Kubb

    Dan Kubb June 26th, 2008 @ 12:29 PM

    Hax, yes this is intended. In fact its one of the primary things that separates DataMapper from other ORMs. Read the first section "Identity Map" on the following page:

    http://datamapper.org/why.html

    We use the Identity Map pattern to do this described in PoEAA:

    http://martinfowler.com/eaaCatal...

    I'm not sure what should happen within Transactions. It would be relatively simple to have an isolated IdentityMap within a transaction so that objects within the transaction block are different from objects outside; but I haven't thought of all the pluses and minuses for this technique. Reconciling the inner-objects with the outer-objects after the transaction is complete would be a little more difficult I think.

    Sam, do you have any opinion on this?

  • Hax

    Hax June 28th, 2008 @ 06:03 AM

    Hi Dan,

    Thanks for the link on the Identity Map pattern. After reading this, I am now even more surprised as it seems to imply that you should only have a single object instance per row in the table which is not what you get with DM.

    The Identity Map pattern states:

    "If you aren't careful you can load the data from the same database record into two different objects. Then, when you update them both you'll have an interesting time writing the changes out to the database correctly."

    What I am seeing definitely seems to contradict what the identity map pattern seeks to prevent. I do get more than one object instance of the same database row, and each instance can have different property values and relationships as demonstrated in my ticket.

    Aside from the identity map issue, the inconsistency in the relationship visibility is still a concern. It just seems 'off' to me that (as per my pastie) the Enrolment object has the association to the Student and Subject objects, but the Student and Subject objects don't have the inverse relation.

  • Dan Kubb

    Dan Kubb June 28th, 2008 @ 11:34 AM

    Sorry I must've been partly asleep when I read your example.

    IdentityMap is only active inside a repository scope, so try wrapping the code in a repository block.

    I do understand what you're saying about the two way inconsistency. Its definitely a problem.

    I want to update the relationship code so that when you define a relationship in either direction only a single object is created. So whether the has() or belongs_to() side is defined with exactly corresponding arguments, they'd share the same relationship object and know what the association on the other end is called. (as of right now there's no way to know what the corresponding association on the other side is called without guessing each time)

    That way when adding a resource to many to one association, the underlying relationship can also update any in-memory objects for the corresponding one to many association on the other side.

    Would you mind providing a patch with a failing spec that shows the behavior you expect to happen?

  • Sam Smoot

    Sam Smoot June 29th, 2008 @ 10:48 PM

    Regarding bi-directional associations. Completely against it. It'll throw a real wrench into Strategic-Eager-Loading for one (right now we just have to overwrite Relationship#get_parent/child).

    You can use the Query::Path to find complementary relationships anyways.

    Someone wants to write a plugin, that's fine. But automatically adding methods I didn't specifically define or include (most likely on purpose)? No thanks. That's too close for comfort to AR territory.

    @Hax:

    There's no way around scoping an IdentityMap. WeakRef doesn't GC. Objects don't finalize. So a global IM is out of the question. It's a Ruby limitation. If someone can find a reliable way around it, I'm all ears though.

  • Dan Kubb

    Dan Kubb June 30th, 2008 @ 12:20 AM

    Sam, would you mind clarifying what you mean by bi-directional associations?

    Each time I've heard them mentioned, they were in reference to when you use belongs_to, having it automatically create the has n at the other end, and vice versa.That's not what I was talking about.

  • Hax

    Hax June 30th, 2008 @ 07:58 PM

    @Dan:

    Thanks for the info, I will try it out when I get back to working with DM again. It will be great to have the relationship stuff eventually sorted as you describe. I hope to send you some specs as requested by the end of the week.

    @Sam:

    I believe you are referring to something else, what I want for bi-directional associations is that if I have specified 'has n, :enrolments' on say the Student model class, and on the Enrolment class I have specified 'belongs_to :Student' then when I create a new Enrolment object giving it a student, that particular student object should immediately reflect the association in its enrolments collection. Currently enrolment.student does return the student, but student.enrolments does not return the enrolment.

  • Sam Smoot

    Sam Smoot July 1st, 2008 @ 10:10 AM

      • → State changed from “new” to “open”

    Ok, sorry, I completely confused the issue. My apologies guys. :-)

    @Dan:

    Relationship#name should return the name of the association. So it can't be the same Relationship for both sides. But Associations can have Proxy#complement that returns the Relationship with the complementary Query::Path. Which should be really easy I think. If you think it'd be useful.

    @Hax:

    OK, makes sense. Yes, zoo.exhibits.new(:name => 'Bob') should add the complementing association if one is defined. We'll work on that. I think it should probably happen through an #"#{association_name}_keys=(key_arrays)" method. Shouldn't be hard to add, but that's unsafe until we finish off :protect => true for mass-assignment.

    =======

    So... we don't actually need to do anything for the case when you assign a Exhibit to zoo.exhibits. When the Exhibit#zoo_id is set, then Relationship#get_parent should be able to hand you back the right Zoo without actually making a database call. So it's perhaps a little more expensive this way if you actually touch all your association attributes after such an assignment, but it's much cheaper if you don't.

    To ensure that scenario works then, we just have to make sure to fix association repository bindings (which are broken right now I believe), and ensure that Relationship#get_parent checks the IdentityMap before doing the Strategic-Eager-Loading thing.

    BTW: Relationship should take a :repository option. If it's present, it's bound to that repository. If it's not, it defaults to the repository of the instance passed in to #get_parent/get_children. (The latter being the desired behaviour 9 times out of 10.) So that's the fix for that. Also, I've noticed the repository_name argument takes precendence over the repository stack in the resource/model #repository methods I think. But since most methods will probably pass a repository_name, this is a potential problem.

    There's a fine-line to walk there anyways. Because the stack as well is "explicit" from the user's POV. I'm actually concerned that DM3 had it "right", and DM9 might have a few issues with the use of the stack (or rather lack thereof). But I'll have to verify that one way or another. I could be off-base.

    Either way, these are the issues generally related to this request. Nothing too big. But there's a few things to work through until we can call it done.

Please Login or create a free account to add a new comment.

You can update this ticket by sending an email to from your email client. (help)

Create your profile

Help contribute to this project by taking a few moments to create your personal profile. Create your profile »

Shared Ticket Bins

People watching this ticket

Tags