Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Problem with :belongs_to and legacy database that doesn't use "*_id" convention #171

Closed
L2G opened this Issue · 6 comments

4 participants

@L2G

I'm experimenting with the public domain MusicBrainz database and learning DataMapper at the same time. So I'm starting off slow:

class Artist
    include DataMapper::Resource

    property :id,  Serial
    property :gid, String, :auto_validation => true

    belongs_to :name, :model => 'ArtistName'
end

class ArtistName
    include DataMapper::Resource

    property :id, Serial
    property :name, String
end

This would work great except for one thing: The parent table, "artist", really doesn't use the name "name_id" to link to the "artist_name" table; instead, it uses "name".

For the life of me I cannot work out any parameters to :belongs_to that will make this work. Adding :parent_key => 'name' fails because then it looks for a column named "name_name". Adding :child_key => 'name' errors out with the message "SystemStackError: stack level too deep".

I have a sneaky feeling the problem is the code in associations/many_to_one.rb that checks to see if the source key (a.k.a. parent key, yes?) has been renamed. But it's too late in the evening for me to wrap my brain around it. :-)

UPDATE: Okay, so I figured out that if I give the relationship a different name like :the_name and use :child_key => "name", I can get this to work. I was just hoping there was a way to do it while still using :name for the relationship and without having to rename columns in the table. Am I missing something obvious or could this be considered a bug?

@emmanuel
Owner

The issue here is attempting to define both a Property and a Relationship with the same name. By default, belongs_to implicitly defines a Property to house a foreign key, which can be overridden by specifying an existing Property using the :child_key option. In this case, passing [:name] as the :child_key will cause a SystemStackError because DataMapper goes round in circles.

Try giving the child key Property a distinct name, eg:

class Artist
  include DataMapper::Resource
  ...
  belongs_to :name, :model => 'ArtistName', :child_key => [:name_value]
end

Or alternately, give the Relationship a distinct name, eg:

class Artist
  include DataMapper::Resource
  ...
  belongs_to :artist_name, :model => 'ArtistName', :child_key => [:name]
end

Hope that helps.

@emmanuel
Owner

Actually, I did not look closely enough at your schema. The :name_id value DataMapper proferred is perfectly appropriate here: Artist.name_id will hold a foreign key relating to the ArtistName.id primary key. Thus, Artist.name_id is not going to be a string value, it is going to be the integer that identifies the ArtistName resource whose name property holds the string value.

If you want to relate Artist and ArtistName on the artist's actual name as a string value, be forewarned that there are good arguments for using synthetic keys (ie., when natural keys change—as they have a tendency to do, despite expectations to the contrary—it can cause headaches). If you still want to relate the two models on the artist name string, try this:

class Artist
  include DataMapper::Resource
  ...
  belongs_to :artist_name, :model => 'ArtistName', :child_key => [:name], :parent_key => [:name]
end

This will configure the Relationship to define an Artist.name property as a foreign key to the ArtistName.name property.

Hope that helps :).

@namelessjon
Owner

As an addendum to what emmanuel says about it being a pain when natural keys change, you might want to look into using dm-constraints. If you have a compatible database (MySQL or Postgres definitely are) you can update the FKs appropriately.

@emmanuel
Owner

@L2G — your update and the issue both leave vague the question of how you are trying to relate these two Models. Either they can be related on an integer foreign key to primary key (eg., Artist.name_id -> ArtistName.id). This is the 'synthetic key' approach.

OR the two models can be related on a natural key. In this case the ArtistName.name string is the only candidate I see. Going this route would result in a mapping like Artist.name_value => ArtistName.name. This is the 'natural key' approach.

If you pursue the natural key approach, understand the restriction that a property and relationship can't share a single name. If control of the column name in the DB is your concern, another thing you could do is to explicitly declare the foreign property on Artist and give it an alternate field name:

class Artist
  include DataMapper::Resource
  ...
  property :name_value, Integer, :field => :name

  belongs_to :name, :model => 'ArtistName', :child_key => [:name_value], :parent_key => [:name]
end

This creates an Artist.name_value property in memory that DataMapper maps to an artists.name column in your DB. I thin k this is probably what you want to do.

BTW, please add any updates as new comments, because I get notified when new comments are added, but I don't get any notifications when existing content is updated.

@L2G

Gotcha. Thanks for your help. I will try your last example when I get a chance, as it looks like an ideal solution. I really appreciate the help with this!

@L2G L2G closed this
@nmccready

The last solution does not work on the master branch. This seems to be a bug. This may be the only solution.
http://workswithruby.com/2008/12/using-datamapper-on-legacy-databases

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.