I'm trying to solve validation of associated object with condition.
User doesn't need to have filled author_bio until he is author. So app needs to ensure, that author can't create post without author_bio and author_bio can't be deleted if user already created any post.
class User < ApplicationRecord
has_many :posts, foreign_key: 'author_id', inverse_of: :author
validates :author_bio, presence: { if: :author? }
def author?
posts.any?
end
end
class Post < ApplicationRecord
belongs_to :author, class_name: 'User', inverse_of: :posts, required: true
end
Unfortunately this doesn't validate author on creation of new post:
user = User.first
user.author_bio
=> nil
post = Post.new(author: user)
post.valid?
=> true
post.save
=> true
post.save
=> false
post.valid?
=> false
So how can I prevent creating of new post by user without author_bio? I can add second validation to Post model, but this is not DRY. Is there any better solution?
The answer here seems to be use of validates_associated once you have your associations correctly set up (including inverse_of which you have, but stating for others, rails in many cases misses them or creates them incorrectly)
so to adjust the classes here:
class User < ApplicationRecord
has_many :posts, foreign_key: 'author_id', inverse_of: :author
validates :author_bio, presence: { if: :author? }
def author?
posts.any?
end
end
class Post < ApplicationRecord
belongs_to :author, class_name: 'User', inverse_of: :posts
validates :author, presence: true
validates_associated :author
end
Now when you try running what you did before:
user = User.first
user.author_bio
=> nil
post = Post.new(author: user)
post.valid?
=> false
post.save
=> false
Does not allow you to save since author_bio is empty
Only thing to watch out there was to set up correct associations, otherwise rails was confused and skipping validation on User class since it thought the relationship is not yet in existence.
NOTE: I removed required: true from belongs_to since in rails 5 is default, therefore you won't need validates :author, presence: true either only in rails 5.
Related
I have the models Account and User. Both models have an email attribute.
An Account has_many :users and a User belongs_to :account
I would like to validate the uniqueness of the email accross both models when an Account is being created so the Account email is invalid if it's taken by a User (since the account email later becomes the admin user email).
I added a scope to the email constraint in the Account model but it is not working (the form is not being rejected).
Account model:
has_many :users
validates :email, uniqueness: { scope: :users, case_sensitive: false }
What is the correct way to implement this? Do I need to add an index to the DB?
This is an alternative method based on a separate table and a polymorphic assocation:
class CreateEmailAddresses < ActiveRecord::Migration[6.1]
def change
create_table :email_addresses do |t|
t.string :email, unique: true
t.references :entitity, polymorphic: true
t.timestamps
end
add_index :email_addresses, [:entity_type, :entity_id], unique: true
end
end
class EmailAddress < ApplicationRecord
validates_uniqueness_of :email
validates_uniqueness_of :entity_id, scope: :entity_type
belongs_to :entity, polymorphic: true
end
class User < ApplicationRecord
has_one :email_address,
as: :entity,
dependent: :destroy
delegate :email, to: :email_address
accepts_nested_attributes_for :email_address
end
class Account < ApplicationRecord
has_one :email_address,
as: :entity,
dependent: :destroy
delegate :email, to: :email_address
accepts_nested_attributes_for :email_address
end
It avoids having to restructure your domain or create a user simply to create a email but will cause issues with authentication libraries such as Devise as well as lacking a real foreign key to guarentee referential integrity which can lead to orphaned records.
IMO not a great solution as it most likely will create as many problems as it solves.
An application level validation doesn't actually guarentee the uniqueness of your data - it merely catches a lot of the cases where users try to input duplicates and provides feedback.
So while you could naively implement a custom validation that queries both tables it won't even survive the race condition caused by a double clicking senior citizen or any insertion method that circumvents validations.
Since there is no way (AFAIK) to create indices across tables so you might want to just restructure your domain and add an "owner" (call it whatever you want) to the accounts table:
class AddOwnerToAccounts < ActiveRecord::Migration[6.1]
def change
add_reference :accounts, :owner, null: false, foreign_key: { to_table: 'users' }
end
end
class Account < ApplicationRecord
has_many :users
belongs_to :owner,
class_name: 'User',
inverse_of: :owned_accounts
delegate :email, to: :owner
end
class User < ApplicationRecord
belongs_to :account
has_many :owned_accounts,
class_name: 'Account',
foreign_key: :owner_id,
inverse_of: :owner
end
This an only be achieved with a custom validation method.
validate :email_unique_for_account_and_user
private
def email_unique_for_account_and_user
if User.where(email: email).where.not(id: id).exists? ||
Account.where(email: email).where.not(id: id).exists?
errors.add(:email, :taken)
end
end
IMO it is not possible to define a unique index over multiple database tables.
In my Rails 5 app I have the following setup:
class Client < ApplicationRecord
has_one :address, :as => :addressable, :dependent => :destroy
accepts_nested_attributes_for :address, :allow_destroy => true
end
class Company < Client
has_many :people
end
class Person < Client
belongs_to :company
end
class Address < ApplicationRecord
belongs_to :addressable, :polymorphic => true
validates :city, :presence => true
validates :postal_code, :presence => true
end
A person can belong to a company but doesn't necessarily have to.
Now I want to validate a person's address only if that person doesn't belong to a company. How can this be done?
There might be other approaches as well, but based on my experience, something like this should work.
validates :address, :presence => true, if: -> {!company}
Hope this helps.
Validations can take either an if or unless argument, which accept a method, proc or string to determine whether or not to run the validation.
In your case:
validates :address, presence: true, unless: :company
Update according to comments
The above only takes care of skipping the validation itself, but due to accepts_nested_attributes_for OP still saw errors when trying to persist a missing address. This solved it:
accepts_nested_attributes_for :address, reject_if: :company_id
Nabin's answer is good but wanted to show another way.
validate :address_is_present_if_no_company
def address_is_present_if_no_company
return if !company_id || address
errors.add(:address, "is blank")
end
I have a the following models:
class Part
belongs_to :note, inverse_of: :part, dependent: :destroy
class Note
has_many :attachments, as: :attachable, dependent: :destroy
has_one :part, inverse_of: :note
end
class Attachment
belongs_to :attachable, polymorphic: true, touch: true
end
When I add attachments to a note, I do it via the part
def update
#part = current_user.parts.find params[:id]
#part.note.update note_params
…
end
What I find strange is the following:
def update
#part = current_user.parts.find params[:id]
#part.note.update note_params
#part.note.attachments.any? # => false
#part.note.attachments.any? # => true
end
Why does the first call return false? Since I need this in my view, I'm left with calling #part.note.reload, but I can't understand what is going on.
Thanks
Associations are cached for performance reasons. Use association(true) to bypass the cache and force Rails to refetch the current state after you have done something to change it:
#part.note(true).attachments.any?
See Association Basic: Controlling Caching.
I am using Ruby on Rails v3.2.2. I would like to solve the issue related to the validation of a foreign key when using accepts_nested_attributes_for and validates_associated RoR methods. That is, I have following model classes:
class Article < ActiveRecord::Base
has_many :category_associations, :foreign_key => 'category_id'
accepts_nested_attributes_for :category_associations, :reject_if => lambda { |attributes| attributes[:category_id].blank? }
validates_associated :category_associations
end
class CategoryAssociation < ActiveRecord::Base
belongs_to :article, :foreign_key => 'article_id'
belongs_to :category, :foreign_key => 'category_id'
validates :article_id, :presence => true
validates :category_id, :presence => true
end
... and I have following controller actions:
class ArticlesController < ApplicationController
def new
#article = Article.new
5.times { #article.category_associations.build }
# ...
end
def create
#article = Article.new(params[:article])
if #article.save
# ...
else
# ...
end
end
end
With the above code ("inspired" by the Nested Model Form Part 1 Rails Cast) my intent is to store category associations when creating an article (note: category objects are already present in the database; in my case, I would like just storing-creating category associations). However, when I submit the related form from the related view file, I get the following error (I am logging error messages):
{:"category_associations.article_id"=>["can't be blank"], :category_associations=>["is invalid"]}
Why it happens since validates_associated seems to run the method article.category_association.valid? but only if the article.category_association.article_id is not nil? How can I solve the problem with the presence validation of the article_id foreign key?
However, if I comment out the validates :article_id, :presence => true in the CategoryAssociation model class, it works as expected but it seems to be not a right approach to do not validate foreign keys.
If I comment out the validates_associated :category_associations in the Article model class, I still get the error:
{:"category_associations.article_id"=>["can't be blank"]}
Use inverse_of to link the associations and then validate the presence of the associated object, not the presence of the actual foreign key.
Example from the docs:
class Member < ActiveRecord::Base
has_many :posts, inverse_of: :member
accepts_nested_attributes_for :posts
end
class Post < ActiveRecord::Base
belongs_to :member, inverse_of: :posts
validates_presence_of :member
end
Since you have a possible nested form with accepts_nested_attributes_for, therefore in CategoryAssociation you need to make the validation conditional, requiring presence for only for only updates:
validates :article_id, presence: true, on: :update
Aside from Active Record associations, you should have a foreign key constraints at the db level.
If you're stucked with this kind of errors too, try to replace:
validates :article_id, :presence => true
validates :category_id, :presence => true
with:
validates :article, :presence => true
validates :category, :presence => true
worked for me.
Validations will run on create or save (as you'd expect), so ask yourself, "at each one of those is there a saved instance being referred to?", because without a save an instance won't have an id as it's the database that assigns the id.
Edit: Like I've said in the comments, if you're going to downvote then leave a comment as to why.
Take the following code (Rails 3.0.10):
User < AR
has_many :providers
Provider < AR
belongs_to :user
validates_presence_of :user
user = User.new
user.providers.build
# so both models not yet saved but associated with each other
user.valid?
=> false
user.errors
=> {:providers=>["is invalid"]}
user.providers.first.errors
=> {:user_id=>["can't be blank"]}
Why can't Provider see that it has a not yet saved associated user model available? Or in other words - how can I deal with that so that the validation is still present? Or maybe I'm doing something wrong?
Note, that I'm looking for a clean solution, so suggesting a before validation callback in Provider model saving the User model to the database is a no-go.
Use :inverse_of
class User < ActiveRecord::Base
has_many :providers, :inverse_of => :user
end
class Provider < ActiveRecord::Base
belongs_to :user, :inverse_of => :providers
validates :user, :presence => true
end