DataMapper => One-to-Many filter - ruby

class Task
include DataMapper::Resource
has 1, :list, :through => Resource
end
class List
include DataMapper::Resource
has n, :tasks, :through => Resource
end
A list has many tasks. Suppose I have a task with id = 1.
How do I do to find the list that has this task?
I tried : List.first(:tasks => task) but it always returns nil.
Thank you.

what about Task.first(:id => 1).list? By the way, you should really change the definitions of your models. I recommend you to read the datamapper documentation thoroughly.
class Task
include DataMapper::Resource
belongs_to :list
end
class List
include DataMapper::Resource
has n, :tasks
end
Doesn't that look much nicer? Oh and I hope you defined keys. These are important for well working associations. And if id is the key for Task your query would simplify to Task.get(1).list.

You can used a nested condition like this:
List.first(:tasks => { :id => task.id })
but given a task it would be simpler to use task.list

Why do you have has 1 through resource? I would do Task.belongs_to :list and then List.has n, :tasks so you could write:
List.first :"tasks.id" => task.id
Although if you already got the task then it's simpler to just write task.list :)

Related

Datamapper: How to count total score from has_many objects

I just started learning some database basics. I am using Ruby and the datamapper gem
I have two simple objects:
class Quote
include DataMapper::Resource
property :id, Serial
property :saying, String, :required => true
property :score, Integer, :default => 5
belongs_to :user
end
and
class User
include DataMapper::Resource
property :id, Serial
has n, :quotes
end
No I would like to get the total score of a user. The total score is the sum of the scores of all associated quotes of a user.
I tried something like
#totalscore = #user.quotes.inject(0) {|count, q| count + q.score}
but I guess this can't be the way I am supposed to use a database, right?
Any help is appreciated!
Best,
Tobi
I am not running the code, but by looking at the docs, I think something like this should work:
#totalscore = #user.quotes.sum :score

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.

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

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

DataMapper filter records by association count

With the following model, I'm looking for an efficient and straightforward way to return all of the Tasks that have 0 parent tasks (the top-level tasks, essentially). I'll eventually want to return things like 0 child tasks as well, so a general solution would be great. Is this possible using existing DataMapper functionality, or will I need to define a method to filter the results manually?
class Task
include DataMapper::Resource
property :id, Serial
property :name , String, :required => true
#Any link of type parent where this task is the target, represents a parent of this task
has n, :links_to_parents, 'Task::Link', :child_key => [ :target_id ], :type => 'Parent'
#Any link of type parent where this task is the source, represents a child of this task
has n, :links_to_children, 'Task::Link', :child_key => [ :source_id ], :type => 'Parent'
has n, :parents, self,
:through => :links_to_parents,
:via => :source
has n, :children, self,
:through => :links_to_children,
:via => :target
def add_parent(parent)
parents.concat(Array(parent))
save
self
end
def add_child(child)
children.concat(Array(child))
save
self
end
class Link
include DataMapper::Resource
storage_names[:default] = 'task_links'
belongs_to :source, 'Task', :key => true
belongs_to :target, 'Task', :key => true
property :type, String
end
end
I would like to be able to define a shared method on the Task class like:
def self.without_parents
#Code to return collection here
end
Thanks!
DataMapper falls down in these scenarios, since effectively what you're looking for is the LEFT JOIN query where everything on the right is NULL.
SELECT tasks.* FROM tasks LEFT JOIN parents_tasks ON parents_tasks.task_id = task.id WHERE parents_tasks.task_id IS NULL
You parents/children situation makes no different here, since they are both n:n mappings.
The most efficient you'll get with DataMapper alone (at least in version 1.x) is:
Task.all(:parents => nil)
Which will execute two queries. The first being a relatively simple SELECT from the n:n pivot table (WHERE task_id NOT NULL), and the second being a gigantic NOT IN for all of the id's returned in the first query... which is ultimately not what you're looking for.
I think you're going to have to write the SQL yourself unfortunately ;)
EDIT | https://github.com/datamapper/dm-ar-finders and it's find_by_sql method may be of interest. If field name abstraction is important to you, you can reference things like Model.storage_name and Model.some_property.field in your SQL.

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

Resources