#1166 ✓not-applicable
Michal Hantl

Cannot use new resources in a Hash

Reported by Michal Hantl | December 24th, 2009 @ 05:45 AM

class Record

include DataMapper::Resource
property :id, Serial


class DMTest < Test::Unit::TestCase

def test_use_records_as_hash_keys
  g1 = Record.new
  g2 = Record.new
  h = {g1 => 1 , g2 =>2}
  assert_equal [g1, g2], h.keys


<[#<Chat::Record @id=nil>, #<Chat::Record @id=nil>]> expected but was
<[#<Chat::Record @id=nil>]>.

Comments and changes to this ticket

  • MarkMT

    MarkMT December 24th, 2009 @ 11:19 AM

    Are you sure that this is a DM issue - surely the problem is that until they're saved g1 and g2 have the same value, whereas keys have to be unique. Would ActiveRecord behave differently?

  • Michal Hantl

    Michal Hantl December 25th, 2009 @ 02:46 AM

    IMO, it's a DM issue. Ruby behaves differently.

    The way I see it, it doesn't provide value to have this behaviour.

    On the contrary it provides:
    1. confusion - before you get to know about this behaviour 2. random bugs - when you don't realize this 'feature' collides with your code 3. longer code - when you want to use records as a hash keys add .object_id

  • MarkMT

    MarkMT December 25th, 2009 @ 09:41 PM

    Starting again after lighthouse comment preview ate my comment the first time (aaaagghh!)...

    Michal, DM is ruby, so just saying ruby behaves differently doesn't really get to the heart of the issue...

    Of course hash keys must be unique and in ruby this is defined in terms of the eql? operator. The ruby API documentation states that eql? is usually synonymous with == , but that there are exceptions. And while at the object level, == is only true if both operands represent the same object, the method is typically overridden in subclasses to provide class-specific behavior. Also the ruby Hash documentation states that "If you need to use instances of your own classes as keys in a Hash, it is recommended that you define both the eql? and hash methods". So there is absolutely no reason to assume that a class behaves in a particular way with respect to key duplication without first checking its API documentation.

    In the case of DataMapper::Resource, both == and eql? are overridden, and as the DM public API documentation states, eql? is only true for two model instances "if they are the same object (identical object_id) or if they are both of the same model and all of their attributes are equivalent" (again as defined in terms of the eql? operator).

    In your case, g1 and g2 do have different object_id's, but prior to saving, their attributes are all identical, resulting in the predictable and consistent behavior you observe.

    If you don't like this, (and since you say you don't want to use the object_id's as keys) you could just override the eql? method in your own code to provide behavior more to your liking, or you could see if you can convince Dan to modify the default behavior in dm-core (it's a very small code change - you might like to submit a patch). Before you decide, you might also want to consider the following...

    If you repeat the test you posted but instead use plain vanilla instances of the ruby Object class, what you will find (at least what I found - using ruby 1.8.7) is that although the hash keys are now distinct, the test does gives inconsistent results. Why? Because assert_equal requires that both the values and the order of the elements in arrays h.keys and [g1, g2] be identical. However the order of the elements in h.keys is not guaranteed to always be the same. So even if you modify eql? to do what you want, the test you've written is not always going to pass.

  • Michal Hantl

    Michal Hantl December 26th, 2009 @ 02:48 AM

    Hi, It's cool that you gave this a thought.

    Michal, DM is ruby, so just saying ruby behaves differently doesn't really get to the heart of the issue...

    What I ment is that in Ruby Object.new != Object.new, where in DM Resource.new == Resource.new.

    I can see the benefit in having eql? overriden in a way you described with primitive-like mostly data types like String.new('aa') == String.new('aa').

    But with two records - whats happens if I save them? They both get different id and all of a sudden are not equal.

    But the point I want to make is deeper though. I treat my Resources as objects, not data.

    If you repeat the test you posted but instead..

    Point was to demonstrate that the array keys get merged into one.
    Btw in JRuby world we are lucky to have order in our hashes.

    Pretend you have this method

    def my_method g1, g2
    {g1 => 1 , g2 =>2} end

    Now don't tell me you expect the result hash to have one key:).

  • Michal Hantl

    Michal Hantl December 26th, 2009 @ 02:54 AM

    Just one thing.

    You suggested I override the default behaviour.
    I might do so for record.save, to let it throw exceptions.

    However I don't want to mess DM in my project just becayse I like it that way.
    If I get to work with other people on the same project I don't want them to lose the confidence in what they know about DM.

  • MarkMT

    MarkMT December 26th, 2009 @ 12:02 PM

    "But with two records - whats happens if I save them? They both get different id and all of a sudden are not equal."

    Not sure if you caught my point here. What I meant to suggest was to define eql? so that it would only return true if the two operands were the same object. In this case the two records will be un-eql? both before and after you save.

    "Now don't tell me you expect the result hash to have one key"

    Sure I do :-).

    Good luck.

  • Dan Kubb (dkubb)

    Dan Kubb (dkubb) December 29th, 2009 @ 04:36 PM

    • State changed from “new” to “unconfirmed”

    Most of the behaviour in DM now is actually built up from simple axioms. All of the following should return true:

    # new/clean resource
    Post.new == Post.new
    Post.new.hash == Post.new.hash
    # new/dirty resource
    Post.new(:title => 'DataMapper') == Post.new(:title => 'DataMapper')
    Post.new(:title => 'DataMapper').eql?(Post.new(:title => 'DataMapper'))
    Post.new(:title => 'DataMapper').hash == Post.new(:title => 'DataMapper').hash
    # saved/clean resource
    Post.get(1) == Post.get(1)
    Post.get(1).hash == Post.get(1).hash
    # with an Identity Map
    repository do
    # without an Identity Map
    not Post.get(1).equal?(Post.get(1))

    If any of these is incorrect, we should probably discuss those first, and then work out how this effects Hash behaviour (which is really only one use (edge-)case that DM resources are expected to be used with).

    There was some discussion on how object equality/equivalency should work in other ORMs a couple of months ago: http://djwonk.com/blog/2009/10/21/broken-equality-in-mongomapper/

  • Dan Kubb (dkubb)

    Dan Kubb (dkubb) February 5th, 2010 @ 03:06 AM

    • State changed from “unconfirmed” to “not-applicable”

    There have been no objections or comments on this ticket, so I will assume everyone agrees that the behaviour I outlined above is correct. Closing this ticket.

Please Sign in or create a free account to add a new ticket.

With your very own profile, you can contribute to projects, track your activity, watch tickets, receive and update tickets through your email and much more.

New-ticket Create new ticket

Create your profile

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

People watching this ticket