DataMapper: one-to-many relationship with custom name? - ruby

I'm trying to build a small model consisting of two entities. For the purposes of this question, call them A and B. A has a one-to-many relationship to several Bs; this means that each B belongs_to an A.
In this particular case, I'd like to call the relationship from B back to A something other than a. I think I got close with the following:
class A
include DataMapper::Resource
property :id, Serial
has n, :bs
end
class B
include DataMapper::Resource
property :id, Serial
belongs_to :owner, 'A'
end
The important bit here is the belongs_to :owner, 'A' line in B. With this, I can successfully:
Create and save an instance of A
Query that A for its bs and get an empty array back
Create an instance of B, assigning its owner to be the A I made earlier
However, when I go to save that instance of B, I run into trouble – calling save returns false. If I print the B, I see that it has two attributes: one called owner_id and another called a_id.
What else do I need to do with this model to rename the relationship from B back to A? Is such a rename even possible?

Figured it out. The owning entity (A) needs to explicitly specify the child keys that it wants created for the relationship:
class A
include DataMapper::Resource
property :id, Serial
has n, :bs, :child_key => [ 'owner_id' ]
end
class B
include DataMapper::Resource
property :id, Serial
belongs_to :owner, 'A'
end
With this change, I only see one relationship attribute created on B, and I'm able to save instances of B that I create.

Related

How to make a Many-To-One relationship in DataMapper

I'm trying to create an association between two models:
class Person
include DataMapper::Resource
property :id, Serial
property :name, String
end
class Country
include DataMapper::Resource
property :id, Serial
property :name, String
end
I just need a simple relationship on Person (a country_id).
My first idea was to put a has 1 property on Person:
class Person
include DataMapper::Resource
property :id, Serial
property :name, String
has 1, :country
end
Instead of a country_id on Person table, Datamapper created a person_id on Country.
To get what I need, I had to make it inverse:
class Country
include DataMapper::Resource
property :id, Serial
property :name, String
has 1, :person
end
This way I got my country_id field on Person table, but it really doesn't make any sense for me.
Am I misunderstanding something or is there another way to make this association?
Whenever Model A "has" Model B (either one or many), you add the foreign key to Model B.
The flipside of this is "belongs to" - that's what you put on the Model with the foreign key.
I guess in DataMapper you don't have to add foreign key columns explicitly, but you could still do it if you want.
# Person
property :country_id, Integer
Since the foreign key in on Person, you'd use "belongs to". It seems like the same thing as "has one", but it's not. You generally only need "has one" in special cases like one-to-one relationships.
# Person
belongs_to :country
# Country
has n, :people
# you could use has 1, :person if for some reason every country
# only had 1 person in it

One-to-one DataMapper association

I'm very new to DataMapper, and I'm trying to create models for the following scenario:
I've got a number of users (with a user name, password etc.), who can also be players or referees or both (so Single Table Inheritance is not an option). The base models would be:
class User
include DataMapper::Resource
property :id, Serial
# Other user properties go here
end
class Player
include DataMapper::Resource
property :id, Serial
# Other player properties go here
# Some kind of association goes here
end
class Referee
include DataMapper::Resource
property :id, Serial
# Other referee properties go here
# Some kind of association goes here
end
DataMapper.finalize
I'm not sure, though, what kinds of associations to add to Player and Referee. With belongs_to :user, multiple players can be associated with the same user, which doesn't make sense in my context. In RDBMS terms I guess what I want is a unique constraint on the foreign key in the Players and Referees tables.
How do I accomplish this in my DataMapper model? Do I have to perform the check myself in a validation?
There are different ways you could do this. Here's one option:
class User
include DataMapper::Resource
property :id, Serial
# Other properties...
has 1, :referee, :required => false
has 1, :player, :required => false
end
class Referee
include DataMapper::Resource
# DON'T include "property :id, Serial" here
# Other properties...
belongs_to :user, :key => true
end
class Player
include DataMapper::Resource
# DON'T include "property :id, Serial" here
# Other properties...
belongs_to :user, :key => true
end
Act on the referee/player models like:
u = User.create(...)
u.referee = Referee.create(...)
u.player = Player.create(...)
u.player.kick_ball() # or whatever you want to call
u.player.homeruns
u.referee.flag_play() # or whatever.
See if this works. I haven't actually tested it but it should be good.
The previous answer works other than :required => false is not recognized for has 1 properties.
It's also confusing because for has n properties, you can use new right on the property or otherwise treat it as a collection. In your example, you would be tempted to code
u = User.create ...
u.referee.create ...
But that fails in the case of has 1 because the property is a single value, which begins life as nil and so you have to use the method the previous answer indicates. Also, having to explicitly make the belongs_to association into the key is a little confusing.
It does seem to execute validations and have the right association actions (so u.save will also save the referred-to Referee). I just wish it were more consistent between has n and has 1.

DataMapper: one to many relationship with one of several models?

Here's my models:
class Item
include DataMapper::Resource
property :id, Serial
has 1, :firstitem
has 1, :seconditem
end
class FirstItem
include DataMapper::Resource
property :id, Serial
belongs_to :item
end
class SecondItem
include DataMapper::Resource
property :id, Serial
belongs_to :item
end
Now, my question is this - if I want FirstItem and SecondItem to be different models but want them both to potentially be part of Item (but only one of the two, so a record with FirstItem will not also have a SecondItem), I could make a has 1 relationship for both of them, and only one of them gets filled.
So in a relational database, does it make sense to do this? Is there a better, more efficient way of defining this relationship?
What you want is a polymorphic association, which sadly, DataMapper does not support. Try looking into ActiveRecord instead; you can easily use it with Sinatra.

DataMapper Many-to-Many Delete Constraint

Let's say we have two models with a many-to-many relation:
class Tag
include DataMapper::Resource
property :id, Serial
has n, :articles, through: Resource
end
class Article
include DataMapper::Resource
property :id, Serial
has n, :tags, through: Resource
end
Now if I create an article with a tag: Tag.create(articles: [ Article.create ])
If I run Tag.first.delete now, it returns false since there is a foreign key constraint due to the many-to-many relationship. If I run Tag.first.delete! it deletes the Tag but not the association record in the article_tags table.
If I use dm-contraints and set everything to :destroy it also destroys the Article which is not what I want.
I can do
tag = Tag.first
tag.articles = []
tag.save
tag.destroy
but this seems seems unclean. Is there a better way?
Since Tag and Article are linked through a many-to-many relationship, you'll need to first destroy any 'ArticleTag' join model that references the object you're trying to delete.
#get the tag to delete
tag = Tag.first
#deletes all rows in the article_tags table that reference
#the tag you want to delete
ArticleTag.all(:tag => tag).destroy
#an alternative to the line above--it does the same thing
tag.article_tags.all.destroy
#another alternative--it won't delete the articles only
#the join model that references the tag
tag.articles.all.destroy
#finally, obliterate the tag
tag.destroy

DataMapper - create new value for relational DB

I have a relational DB defined as follows. How can I enter a new value, where B belongs to A. The code given below doesn't seem to work.
Thanks
class A
include DataMapper::Resource
property :id, Serial, :key => true
property :name, String
belongs_to :b
end
class B
include DataMapper::Resource
property :id, Serial, :key => true
property :name, String
has n, :as
end
Create new value
# Create new value
post '/create' do
a = A.new
b = B.new
b.attributes = params
b.belongs_to = a #problem is here
b.save
redirect("/info/#{a.id}")
end
#belongs_to is a model (class) method and you use it to declare ManyToOne relationship.
In your example you should use "<<" method like this:
b.as << a
That will add "a" instance to "as" collection and associate both resources.
[...] How can I enter a new value, where B belongs to A. The code given below doesn't seem to work.
Your code implies you're after A belonging to B, but your question is the reverse so I'll show how to do that, i.e., B belongs to A.
class A
include DataMapper::Resource
property :id, Serial, :key => true
property :name, String
has n, :bs # A has many B's
end
class B
include DataMapper::Resource
property :id, Serial, :key => true
property :name, String
belongs_to :a, :required => false # B has only 1 A
end
Note your has and belongs_to are reversed here. I also added required => false to the belongs_to side because DataMapper will silently refuse to save your model if ever don't have b.a before calling save—once you're comfortable with it you can remove the required false if you desire.
Here are two ways you can use that model:
# Create new value
post '/create' do
a = A.new
a.save
b = B.new
b.attributes = params
b.a = a
b.save
redirect("/info/#{a.id}")
end
This example is generally the same as yours, but I added a save call for A. Note this may not be necessary, I'm not in a good place to test this particular case; in the past I've found DataMapper will save some related objects automatically but not others so I've developed the habit of always saving explicitly to prevent confusion.
# Create new value
post '/create' do
a = A.create
b = a.bs.create(params)
redirect("/info/#{a.id}")
end
In the second example I call create on the many-side of the relationship, this makes a new B, associates it with "a", sets the params given, and saves it immediately. The result is the same as the previous example.
If you're just getting familiar with DataMapper, you may find it helpful to add the following to your app:
DataMapper::Model.raise_on_save_failure = true
This will cause DataMapper to give you errors and backtraces in cases like the above, more info here.

Resources