Raz
Raz

Reputation: 9771

Validate that the associated model exists when set in Rails

Suppose we have the following Post model in Rails:

class Post < ActiveRecord::Base
  belongs_to :user
end

user is not required, but when it is set, we want it to be a valid association (we don't want to have a post that has a user_id that isn't in the database). Failing tests:

expect(Post.new).to be_valid
expect(Post.new(user_id: 0)).not_to be_valid
expect(Post.new(user: User.create)).to be_valid

One solution would be to do:

validates_presence_of :user, unless: "user_id.nil?"

This will do the required task but isn't really pretty as we don't want to write it for each belongs_to we have in our app.

Is there a way to tell rails to check for the existence of the associated model? Isn't that something we want to have by default? (Why would we want an association_id that doesn't point to anything?)

Upvotes: 1

Views: 552

Answers (1)

tmw
tmw

Reputation: 482

To guarantee that an association is valid at the database level, MySQL and PostgreSQL have the ability to enforce the validity of foreign key columns on a table. Since Rails 4.2, these foreign key constraints can be created as part of a migration.

You could do this in at least two ways in your migration. Either pass the foreign_key attribute to add_reference:

add_reference :posts, :user, index: true, foreign_key: true

Or follow add_reference with add_foreign_key, which also gives you the option of resetting the key when the user is deleted:

add_reference :posts, :user, index: true
add_foreign_key :posts, :user, on_delete: :nullify

Combine the two approaches by passing a hash to the foreign_key attribute of add_reference (since Rails 4.2.1):

add_reference :posts, :user, index: true, foreign_key: {on_delete: :nullify}

If a query breaks the foreign key constraint by updating a user_id to point to a non-existent user, the query will fail and raise an exception in Rails.

As you've mentioned, you could also create a validation to check that a User exists before creating a Post. However, this won't be as strong of a guarantee as a foreign key constraint on the database, since a User could be deleted in the time between the validation passing and the Post being created.

To help keep references valid at the application level, it would generally be better practice to avoid passing a user id directly to the Post constructor. Instead, try to always use a User object as input, for example by calling User.find(:id).

Upvotes: 1

Related Questions