Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

validates_presence_of with belongs_to associations, the right way

I'm investigating on how validates_presence_of actually works. Suppose I have two models

class Project < ActiveRecord::Base
  [...]
  has_many :roles
end

and

class Role < ActiveRecord::Base
  validates_presence_of :name, :project

  belongs_to :project
end

I want it so that role always belongs to an existing project but I just found out from this example that this could lead to invalid (orphaned) roles saved into the db. So the right way to do that is to insert the validates_presence_of :project_id in my Role model and it seems to work, even if I think that semantically has more sense to validate the presence of a project instead of a project id.

Besides that I was thinking that I could put an invalid id (for a non existing project) if I just validate the presence of project_id, since by default AR doesn't add integrity checks to migrations, and even if I add them manually some DB does not support them (i.e. MySQL with MyISAM or sqlite). This example prove that

# with validates_presence_of :name, :project, :project_id in the role class
Role.create!(:name => 'foo', :project_id => 1334, :project => Project.new)
  AREL (0.4ms)  INSERT INTO "roles" ("name", "project_id") VALUES ('foo', NULL)
+----+------+------------+
| id | name | project_id |
+----+------+------------+
| 7  | foo  |            |
+----+------+------------+

Of course I won't write code like this, but I want to prevent this kind of wrong data in DB.

I'm wondering how to ensure that a role ALWAYS has a (real and saved) project associated.

I found the validates_existence gem, but I prefer to not add a gem into my project unless is strictly necessary.

Any thought on this?

Update

validates_presence_of :project and adding :null => false for the project_id column in the migration seems to be a cleaner solution.

like image 955
Fabio Avatar asked May 27 '11 16:05

Fabio


2 Answers

Rails will try a find on the id and add validation error if an object with an id is not found.

class Role < AR::Base
  belongs_to :project
  validates_presence_of :project, :name
end


Role.create!(:name => "admin", :project_id => 1334)# Project 1334 does not exist
# => validation error raised

I see your problem also wants to deal with the situation where the author object is provided but is new and not in db. In the case the presence check doesnt work. Will solve.

Role.create!(:name => "admin", :project => Project.new) # Validation passes when it shouldn't.

Update: To some extent you can mitigate the effect of passing a dummy new object by doing a validation on the associated :project.

class Role < ActiveRecord::Base
  belongs_to :project
  validates_presence_of :project
  validates_associated :project
end

If Project.new.valid? is false then Role.create!(:name => "admin", :project => Project.new) will also raise an error. If however, Project.new.valid? is true then the above will create a project object when saving.

Does using validates_associated :project help you?

like image 165
Aditya Sanghi Avatar answered Sep 28 '22 00:09

Aditya Sanghi


I tried a lot of combinations of validators, but the cleanest solution is to use the validates_existence gem. With that I can write code like this

r = Role.new(:name => 'foo', :project => Project.new) # => #<Role id: nil, name: "foo", project_id: nil, created_at: nil, updated_at: nil> 
r.valid? # => false 
r.errors # => {:project=>["does not exist"], :project_id=>["does not exist"]} 

So my final model is as simple as

class Role < ActiveRecord::Base
  belongs_to :project
  validates_existence_of :project
  # or with alternate syntax
  validates :project, :existence => true
  [...]
end

With db validation plus Aditya solution (i.e. :null => false in the migration and validates_presence_of :project in the model) Role#valid? will return true and Role#save will raise an exception at database level when project_id is null.

like image 37
Fabio Avatar answered Sep 28 '22 02:09

Fabio