Correct way to make a DataMapper association - ruby

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.

Related

Datamapper: count favs

I am working on a Sinatra image app. I have defined Datamapper models for my users, the uploaded images, and "Favs". Each user can give a maximum of one "Fav" to each image.
class User
include DataMapper::Resource
property :id, Serial
property :privilege_lvl, Integer, :default => 0
property :name, String, :unique => true
property :password_hash, BCryptHash
has n, :images
has n, :comments
has n, :favs
end
class Image
include DataMapper::Resource
property :id, Serial
mount_uploader :file, ImageUploader
belongs_to :user
property :posted_at, DateTime
has n, :comments
has n, :favs
end
class Fav
include DataMapper::Resource
property :id, Serial
belongs_to :image
belongs_to :user
end
Is there a way to count the total number of Favs a user has received without iterating over all the images of the user and summing up the Favs of each image?
Sounds like you need a counter cache:
This could be as simple as adding a :favs_cache to users and incrementing it whenever favs are added.
You might also want to check out this gem: dm-counter-cache

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.

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

DataMapper save fails but with no errors

When I try to modify and then save a model using DataMapper I get a SaveFailure exception but no errors.
Specifically I see this message:
"MonthlyBill#save returned false, MonthlyBill was not saved"
This is the code doing the saving:
post '/monthly_bills' do
with_authenticated_user do |user|
description = params[:description]
expected_amount = params[:expected_amount]
pay_period = params[:pay_period]
monthly_bill = MonthlyBill.new(:description=>description, :expected_amount=>expected_amount, :pay_period=>pay_period)
user.monthly_bills << monthly_bill
user.save
end
The User model:
class User
include DataMapper::Resource
property :id, Serial
property :email_address, String
property :password, String
has n, :monthly_bills
has 1, :current_pay_period
end
The MonthlyBill model:
class MonthlyBill
include DataMapper::Resource
property :id, Serial
property :description, String
property :expected_amount,Decimal
property :pay_period, Integer
belongs_to :user
end
What is the issue and, more importantly, how can I get DataMapper to tell me more specifically what is wrong?
Hmm - those capitalised properties look worrying to me. I would do...
has n, :monthly_bills
has 1, :current_pay_period #do you really have a CurrentPayPeriod model?!
And then try:
monthly_bill = MonthlyBill.new(:description=>description,:expected_amount=>expected_amount, :pay_period=>pay_period, :user=>user)
monthly_bill.save

DataMapper subclassing & many-to-many self-referential relationships

I'm building a small Ruby application using DataMapper and Sinatra, and I'm trying to define a basic blog model:
The blog has multiple Users
I have a collection of Posts, each of which is posted by a User
Each Post has a set of Comments
Each Comment can have its own set of Comments - this can repeat several levels deep
I'm running into trouble getting the self-referential relation between comments going due to the fact that each Comment belongs_to a Post. My classes right now look like this:
class User
include DataMapper::Resource
property :id, Serial
property :username, String
property :password, String
has n, :post
end
class Post
include DataMapper::Resource
property :id, Serial
property :content, Text
belongs_to :user
has n, :comment
end
class Comment
include DataMapper::Resource
property :id, Serial
property :content, Text
belongs_to :user
belongs_to :post
end
I'm following the guide at Associations and building a new object (CommentConnection) to link two comments together, but my issue is that each subcomment shouldn't belong to a Post as implied by the Comment class.
My first instinct was to extract out a superclass for Comments, so that one subclass could be "top-level" and belong to a post, while the other kind of comment belongs to another comment. Unfortunately, when I do that I run into issues with the comment IDs becoming null.
What's the best way to model this kind of recursive comment relationship in DataMapper?
What you need is a self referential join in Comments, e.g., each Comment can have a parent comment. Try the following:
class Comment
include DataMapper::Resource
property :id, Serial
property :content, Text
has n, :replies, :child_key => [ :original_id ]
belongs_to :original, self, :required => false #Top level comments have none.
belongs_to :user
belongs_to :post
end
This will allow you to have replies to any given comment, although accessing them may get a little nasty (slow) if the volume gets high. If you get this working and want something more sophisticated you could look at nested sets, I believe there is a nested sets plugin for DataMapper but I haven't used.

Resources