DataMapper: multiple has-and-belongs-to-many relationships between the same models? - ruby

How can I set up multiple relationships of the has n, :through => Resource type between the same models with DataMapper?
For instance, in a news CMS I would have something like this:
class User
include DataMapper::Resource
has n, :written_articles, 'Article', :through => Resource
has n, :edited_articles, 'Article', :through => Resource
property :name, String # etc.
end
class Article
include DataMapper::Resource
has n, :authors, 'User', :through => Resource
has n, :editors, 'User', :through => Resource
property :title, String # etc.
end
However, this doesn't work. The database just has one relationship table in which both an author and editor must be specified for each relation, which doesn't even make sense.
How can I do something like this?

You cannot do it using anonymous Resource - the code you provided will create single relational model, UserArticle, that is unable to handle two many-to-many relations (at least automatically). You would need to create a separate explicit relational model, e.g ArticleEditor, to handle this.
class ArticleEditor
include DataMapper::Resource
belongs_to :article, :key => true
belongs_to :user, :key => true
end
and in your models state
has n, :article_editors
has n, :editors (or :edited_articles), :through => :article_editors

Related

Unable to destroy a parent object

I have a large and complicated User model that looks something like this:
class User
class Link
include DataMapper::Resource
property :id, Serial, :key => false
belongs_to :follower, :model => 'User', :key => true
belongs_to :followed, :model => 'User', :key => true
end
include DataMapper::Resource
property :id, Serial
property :username, String, :required => true
has n, :links_to_followers, :model => 'User::Link', :child_key => [:followed_id]
has n, :links_to_followed, :model => 'User::Link', :child_key => [:follower_id]
has n, :comments
has 1, :profile_image
end
My problem is that Datamapper is not letting me Destroy it. I thought this was a result of Datamapper not wanting to destroy an object with un-destroyed child objects so I put in a method destroy_deep that calls destroy on the links_to_followers, links_to_followed, underlying comments, and the profile image (these are all destroyed correctly).
However, even if I call user.destroy after that, the user is not destroyed. There are no error messages of any kind. Is there some kind of cascading delete command that I am missing?
I resolved this.
Apparently to debug destroy, object.errors isn't useful. Instead track exceptions like:
begin
u.destroy
rescue Exception => e
p e
end
The solution was that one of the children fields didn't map back to User. I had a class Like that belonged to User, but User didn't have n Likes.

Why doesn't my associations go both ways in ActiveRecord?

So I'm connecting to a legacy database. I have two tables, Sites and States.
A Site has one State and a State can belong to many Sites
# Sites.rb
has_one :state, :primary_key => "StateKey", :foreign_key => "StateKey"
# States.rb
belongs_to :sites, :class_name => "Sites", :primary_key => "SiteKey", :foreign_key => "SiteKey"
As you can see I have to manually set the foreign keys and primary keys.
So this works:
Sites.first.state # one record returned (the state)
This does not:
States.first.sites # nil returned. Doesn't even appear to hit AR
What am I doing wrong?
Thanks.
You should use the pair has_many, belongs_to:
# Sites.rb
belongs_to :state, :primary_key => "StateKey", :foreign_key => "StateKey"
# States.rb
has_many :sites, :class_name => "Sites", :primary_key => "StateKey", :foreign_key => "StateKey"
Take a look at this guide.
When you have a one-to-many association it is standard practice to use belongs_to and has_many in the two model classes. has_one is a special case of has_many. belongs_to says that the foreign key is in the model declaring the association, and has_one, has_many say the foreign key is in the other model.

MongoMapper: how do I create a model like this

Suppose we have two models, Task and User.
So a user can have many tasks and tasks should be able to have many users too. But, a task should also have a unique creator who is also a user.
Exemple:
A task in this context is like this:
Task ID, Task Creator, Users who should do the task
User_1 creates a task and he is then the creator.
User_1 specifies User_2 and User_3 as users who should do the task. So these two last users are not creators of task.
How do I create this models so that if I have a task object, I can find it's creator and users who should complete it. And how do I do, if I have a user, to find all tasks he created and all tasks he should complete.
You'll need a many-to-many relationship between the Tasks and Users, and you need an additional one-to-many relationship between Users and Tasks, pointing to the creator (User).
Something along these lines: (I usually use Mongoid, so double-check the syntax for the relations in the MongoMapper API - link below.. you might to manually specify :foreign_key and :class)
The idea is that you have two relationships between the models, one which models the many-to-many relationship
with which you get either to the assigned_users or assigned_tasks, and a one-to-many relationship with which you get to either the creator of a task, or the created_tasks for a given user. If you chose these names for the relationships, it will be clear which is which.
class Task
include MongoMapper::Document
key :title, String , :required => true
key :user_ids , Array
has_many :users, :in => user_ids # , :as => :assigned_users
key :creator_id , ObjectId
belongs_to: user, :as => :creator
end
class User
include MongoMapper::Document
key: name, String, :required => true
has_many :tasks # , :as => :assigned_tasks
has_many :tasks, :as => :created_tasks
end
See:
http://mongomapper.com/documentation/plugins/associations.html
The answer suggested by Tilo is correct about how to model the data, but the example code is incorrect and will not work. The :as option is for polymorphic associations, you want to use the :foreign_key option. Also, you can't have two associations named the same. See below for revised code.
class Task
include MongoMapper::Document
key :title, String , :required => true
key :assigned_user_ids, Array
has_many :assigned_users, :in => :assigned_user_ids
key :creator_id , ObjectId
belongs_to :creator, :class => User
# userstamps! also accomplishes the above
end
class User
include MongoMapper::Document
key: name, String, :required => true
has_many :created_tasks, :foreign_key => :creator_id, :class => Task
# inverse of many :in is still in the works
# see https://github.com/jnunemaker/mongomapper/pull/259
# this is a decent workaround for now
def assigned_tasks
Task.where(:assigned_user_ids => self.id)
end
end
See also:
MongoMapper userstamps! documentation

How can I have two many-to-many relationships to the same model in DataMapper?

edit: Updated question to show my use of :child_key => [:comparison_id] as suggested in the comment.
I have two models that look like this:
class Comparison
include DataMapper::Resource
property :id, Serial
end
class Msrun
include DataMapper::Resource
property :id, Serial
property :name, String
end
Comparison come from comparing two sets of Msruns. I thought I would represent this through two many-to-many relationships from Comparison to Msrun, but I am beating my head against the wall as to how to do this in DataMapper. I know that many-to-many relationships are available by adding something like this:
has n, :whatevers, :through => Resource
However, this will only make one many-to-many relationship between the two models. I have also tried creating two join models and manually specifying the relationships, and manually specifying the child key for each relationship like so:
# Join model for the comparison-msrun many-to-many relationship.
class First
include DataMapper::Resource
belongs_to :msrun, :key => true
belongs_to :comparison, :key => true
end
# Join model for the comparison-msrun many-to-many relationship.
class Second
include DataMapper::Resource
belongs_to :msrun, :key => true
belongs_to :comparison, :key => true
end
class Comparison
include DataMapper::Resource
property :id, Serial
has n, :firsts
has n, :msrun_firsts, 'Msrun', :through => :firsts, :child_key => [:msrun_id]
has n, :seconds
has n, :msruns_seconds, 'Msrun', :through => :seconds, :child_key => [:msrun_id]
end
class Msrun
include DataMapper::Resource
property :id, Serial
property :name, String
has n, :firsts
has n, :comparison_firsts, 'Comparison', :through => :firsts, :child_key => [:comparison_id]
has n, :seconds
has n, :comparison_seconds, 'Comparison', :through => :seconds, :child_key => [:comparison_id]
end
Running automigrate results in the following error:
rake aborted!
No relationships named msrun_firsts or msrun_first in First
What am I doing wrong here? How can I make this work?
What you're observing, is the fact that relationships are stored in a set like object under the hood, more specifically, a set that uses the relationship's name as discriminator. So what happens in your case, is that the latter definition overwrites the former, as sets don't allow duplicate entries (and in our case, replace the older entry with the newer, for the set's purposes, identical one).
There are practical reasons for this. It makes no sense to declare two supposedly different relationships on one model, but name them the same. How would you distinguish them when trying to access them? This manifests itself in DM's implementation, where a method named by the relationship name gets defined on the Resource. So what DM ends up doing in your case of trying to add a duplicate to the set, is that it will just use the latter options to generate the implementation of that method. Even if it were to accept duplicate relationship names, the latter relationship would lead to an overwritten/redefined version of the same method, thus leaving you with the same net effect.
As a consequence, you would need to define differently named relationships on your models. When you think about it, it really makes sense. To help DM with inferring the model to use, you can pass the model name (or the constant itself) as the 3rd parameter to the has method, or as the 2nd parameter for belongs_to
class Comparison
include DataMapper::Resource
property :id, Serial
has n, :firsts
has n, :first_msruns, 'Msrun', :through => :firsts
has n, :seconds
has n, :second_msruns, 'Msrun', :through => :seconds
end
class Msrun
include DataMapper::Resource
property :id, Serial
property :name, String
has n, :firsts
has n, :first_comparisons, 'Comparison', :through => :firsts
has n, :seconds
has n, :second_comparisons, 'Comparison', :through => :seconds
end
Hope that helps!
As per the DataMapper docs
I believe you can do:
class Msrun
include DataMapper::Resource
property :id, Serial
property :name, String
has n, :firsts #This line could probably be omitted
has n, :first_comparisons, 'Comparison', :through => :firsts
has n, :seconds #This line could probably be omitted
has n, :second_comparisons, 'Comparison', :through => :seconds
end

Correct way to make a DataMapper association

I want to have a table of users. These users shall have n contacts and n messages..
My code is:
...
class User
include DataMapper::Resource
property :id, Serial, :key => true
property :nickname, String
has n, :contacts
has n, :messages
end
class Contact
include DataMapper::Resource
belongs_to :user
property :id, Serial, :key => true
property :authgiven, String
has 1, :user
end
class Message
include DataMapper::Resource
belongs_to :user
property :id, Serial, :key => true
property :data, String
end
#apply models (validation etc.)
DataMapper.finalize
...
There are no errors initializing DataMapper, but when I try to create a new User or whatever, save always returns false... Can someone please point out what is wrong?
I'm quite new to DataMapper, it always worked for me with simple tables without relationships, so I believe it has to do with the way I declared the 1:n relationship...
Hey you should remove that has 1, :user line from Contact model and you should be good.

Resources