PlankTon
PlankTon

Reputation: 12605

Rails: Validating association after save?

I have a User model which has many roles. Roles contains a user_id field, which I want to validate_presence_of

The issue is: if I assign a role to user upon create, the validation fails because no user_id is set. Now, I do want to validate that a user_id exists, but I need to save the user before checking that.

The code currently looks like this:

@user = User.new(params[:user])
@user.roles << Role.new(:name => 'Peon') unless @user.has_roles?
if @user.save
  # ...

The only ways I can think of getting around the problem involves either disabling the validation, which I don't want to do, or double-saving to the DB, which isn't exactly efficient.

What's the standard way for handling this issue?

Upvotes: 11

Views: 8080

Answers (4)

rambo
rambo

Reputation: 383

For anyone Googling for a solution to this problem for a has_many :through association, as of 5/December/2013 the :inverse_of option can't be used in conjunction with :through (source). Instead, you can use the approach suggested by @waldyr.ar. For example, if our models are set up as follows:

class User < ActiveRecord::Base
  has_many :roles
  has_many :tasks, through: roles
end

class Role < ActiveRecord::Base  
  belongs_to :user  
  belongs_to :task  
end

class Task < ActiveRecord::Base
  has_many :roles
  has_many :users, through: roles
end

We can modify our Role class as follows to validate the presence of both task and user before saving

class Role < ActiveRecord::Base  
  belongs_to :user  
  belongs_to :task  
  before_save { validates_presence_of :user, :task }
end

Now if we create a new User and add a couple tasks like so:

>> u = User.new  
>> 2.times { u.tasks << Task.new }

Running u.save will save the User and the Task, as well as transparently build and save a new Role whose foreign keys user_id and task_id are set appropriately. Validations will run for all models, and we can go on our merry way!

Upvotes: 1

Chris Salzberg
Chris Salzberg

Reputation: 27374

After researching a bit, this solution seems to be easiest. First, in your Role model, instead of validating user_id, validate user:

validates :user, :presence => true

Then, in your User model, add :inverse_of => :user to your has_many call:

has_many :roles, :inverse_of => :user

Then it works as expected:

irb(main):001:0> @user = User.new
=> #<User id: nil, created_at: nil, updated_at: nil>
irb(main):002:0> @user.roles << Role.new(:name => "blah")
=> [#<Role id: nil, user_id: nil, name: "blah", created_at: nil, updated_at: nil>]
irb(main):003:0> @user.roles[0].user
=> #<User id: nil, created_at: nil, updated_at: nil>
irb(main):004:0> @user.save
  (0.1ms)  begin transaction
 SQL (3.3ms)  INSERT INTO "users" ("created_at", "updated_at") VALUES (?, ?)  [["created_at", Fri, 04 Jan 2013 02:29:33 UTC +00:00], ["updated_at", Fri, 04 Jan 2013 02:29:33 UTC +00:00]]
 User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."id" = 3 LIMIT 1
 SQL (0.2ms)  INSERT INTO "roles" ("created_at", "name", "updated_at", "user_id") VALUES (?, ?, ?, ?)  [["created_at", Fri, 04 Jan 2013 02:29:34 UTC +00:00], ["name", "blah"], ["updated_at", Fri, 04 Jan 2013 02:29:34 UTC +00:00], ["user_id", 3]]
  (1.9ms)  commit transaction
=> true
irb(main):005:0> @user.roles.first
=> #<Role id: 4, user_id: 3, name: "blah", created_at: "2013-01-04 02:29:34", updated_at: "2013-01-04 02:29:34">

Note, however, that this still produces two SQL transactions, one to save the user and one to save the role. I don't see how you can avoid that.

See also: How can you validate the presence of a belongs to association with Rails?

Upvotes: 10

Andrew Hubbs
Andrew Hubbs

Reputation: 9436

I think you can get around the validation problem if you change your code to look like this:

@user = User.new(params[:user])
@user.roles.new(:name => 'Peon') unless @user.has_roles?
if @user.save
  # ...

If that doesn't work, you could try changing you validation to this:

class Role < ActiveRecord::Base
  belongs_to :user
  validates :user_id, :presence => true, :unless => Proc.new() {|r| r.user}
end

Upvotes: 5

waldyr.ar
waldyr.ar

Reputation: 15244

You must take a look at ActiveRecord's Callbacks. Probably you will use the before_validation to do it.

Upvotes: 3

Related Questions