Given a simple relationship where Person has_many Telephones. And a telephone only contains a telephonenumber which must be unique!
class Telephone < ActiveRecord::Base
validates_presence_of :contact_id
belongs_to :contact
validates :telephone, {:presence => true, :uniqueness => true}
end
class Contact < ActiveRecord::Base
has_many :telephones
validates_associated :telephones
has_many :emails
has_many :addresses
validates_presence_of :firstname
accepts_nested_attributes_for :telephones, :allow_destroy=>true
validates_presence_of :lastname
end
test "telephone number must be unique" do
john = contacts :johndoe #johndoe is a person with 1 existing number
2.times do
john.telephones.build :telephone=> "123" # 123 doesnt exist yet
end
puts Telephone.count # this gives 1
john.save
puts Telephone.count # this gives 3 !!!! ???
assert not(john.valid?) # This validates unless I remove the save above
end
Can someone explain the outcome of this test.
just calling valid? fails, but that is mentioned in the rdoc (must save first)
saving first does make valid? pass
BUT now I actually have 3 records in the database which breaks my unique requirement.
Is there a better way to do this? I don't understand the outcome of this test, it really goes against my expectations.
Ok if you read the ruby documentation you will notice that they mention that validating a model is not sufficient for uniqueness. YOU MUST use database unique constraints whenever possible. Otherwise it is possible when using two processes/threads/whatever that both will do a validation check, pass as unique, and then insert same values.
tl;dr: Add a unique constraint to the db column.
Related
If you are saving a has_many :through association at record creation time, how can you make sure the association has unique objects. Unique is defined by a custom set of attributes.
Considering:
class User < ActiveRecord::Base
has_many :user_roles
has_many :roles, through: :user_roles
before_validation :ensure_unique_roles
private
def ensure_unique_roles
# I thought the following would work:
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but the above results in duplicate, and is also kind of wonky because it goes through ActiveRecord assignment operator for an association (which is likely the cause of it not working correctly)
# I tried also:
self.user_roles = []
self.roles = self.roles.to_a.uniq{|r| "#{r.project_id}-#{r.role_id}" }
# but this is also wonky because it clears out the user roles which may have auxiliary data associated with them
end
end
What is the best way to validate the user_roles and roles are unique based on arbitrary conditions on an association?
The best way to do this, especially if you're using a relational db, is to create a unique multi-column index on user_roles.
add_index :user_roles, [:user_id, :role_id], unique: true
And then gracefully handle when the role addition fails:
class User < ActiveRecord::Base
def try_add_unique_role(role)
self.roles << role
rescue WhateverYourDbUniqueIndexExceptionIs
# handle gracefully somehow
# (return false, raise your own application exception, etc, etc)
end
end
Relational DBs are designed to guarantee referential integrity, so use it for exactly that. Any ruby/rails-only solution will have race conditions and/or be really inefficient.
If you want to provide user-friendly messaging and check "just in case", just go ahead and check:
already_has_role = UserRole.exists?(user: user, role: prospective_role_additions)
You'll still have to handle the potential exception when you try to persist role addition, though.
Just do a multi-field validation. Something like:
class UserRole < ActiveRecord::Base
validates :user_id,
:role_id,
:project_id,
presence: true
validates :user_id, uniqueness: { scope: [:project_id, :role_id] }
belongs_to :user, :project, :role
end
Something like that will ensure that a user can have only one role for a given project - if that's what you're looking for.
As mentioned by Kache, you probably also want to do a db-level index. The whole migration might look something like:
class AddIndexToUserRole < ActiveRecord::Migration
def change
add_index :user_roles, [:user_id, :role_id, :project_id], unique: true, name: :index_unique_field_combination
end
end
The name: argument is optional but can be handy in case the concatenation of the field names gets too long (and throws an error).
I’m sure this is a “duh" newbie-type question, but I’ve been at it for days and cannot figure out why my code isn’t setting a relationship in the database correctly. I have a simple belongs_to relationship between two models.
class Pod < ActiveRecord::Base
belongs_to :instigator, :class_name => “User"
attr_accessor :instigator, :instigator_id, :title
validates_presence_of :instigator, :title
validates_associated :instigator
end
and
class User < ActiveRecord::Base
has_many :instigated_pods, inverse_of: :instigator, :class_name => "Pod", as: "instigator"
end
Then I want to test them with rspec and Factory Girl using (what I think) are, again, pretty simple factories
FactoryGirl.define do
factory :pod do
title "Test pod"
instigator
end
factory :user, aliases: [:instigator] do
username
end
end
With this setup, most tests pass, but my PodsController update test kept failing, and I finally found a test that shows why.
require 'rails_helper'
RSpec.describe Pod, type: :model do
it "saves the relationship to the database" do
pod = FactoryGirl.create(:pod)
expect(pod.save).to be_truthy
expect(pod.reload.instigator).to_not be_nil # passes - cached?
pod_from_database = Pod.find(pod.id)
expect(pod_from_database.instigator).to_not be_nil # <- fails
end
end
It seems that something is preventing the pod.instigator_id from being set in the database, so the relationship isn’t persisting. And I have no clue why!!!
I tried setting validates_presence_of :instigator_id, but that makes most of the standard rspec tests fail, and I saw this from the Rails Guides:
If you want to be sure that an association is present, you'll need to test
whether the associated object itself is present, and not the foreign key used
to map the association.
class LineItem < ActiveRecord::Base
belongs_to :order
validates :order, presence: true
end
In order to validate associated records whose presence is required, you must
specify the :inverse_of option for the association:
class Order < ActiveRecord::Base
has_many :line_items, inverse_of: :order
end
Any help straightening this out would be appreciated!
While I’m not altogether clear why, it appears that removing the line
attr_accessor :instigator, :instigator_id, :title
solves the problem. It appears to be blocking writing these attributes. Any indicator why would be appreciated!
attr_accessor is a ruby method that makes a getter and a setter(make them able to be read and to be written), this is not Database fields which called attr_accessible in Rails 3.
what you did is declare an ruby methods this is why it doesn't worked.
read more here:
http://rubyinrails.com/2014/03/17/what-is-attr_accessor-in-rails/
I'm still pretty new to testing in Rails 3, and I use RSpec and Remarkable. I read through a lot of posts and some books already, but I'm still kind of stuck in uncertainty when to use the association's name, when its ID.
class Project < ActiveRecord::Base
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :project
end
Because of good practice, I want to protect my attributes from mass assignments:
class Task < ActiveRecord::Base
attr_accessible :project # Or is it :project_id??
belongs_to :project
end
First of all, I want to make sure that a project never exists without a valid task:
class Task < ActiveRecord::Base
validates :project, :presence => true # Which one is the...
validates :project_id, :presence => true # ...right way to go??
end
I also want to make sure that the assigned project or project ID is always valid:
class Task < ActiveRecord::Base
validates :project, :associated => true # Again, which one is...
validates :project_id, :associated => true # ...the right way to go?
end
...and do I need the validation on :presence when I use :associated??
Thanks a lot for clarifying, it seems that after hours of reading and trying to test stuff using RSpec/Shoulda/Remarkable I don't see the forest because of all the trees anymore...
This seems to be the right way to do it:
attr_accessible :project_id
You don't have to put :project there, too! It's anyway possible to do task.project=(Project.first!)
Then check for the existence of the :project_id using the following (:project_id is also set when task.project=(...) is used):
validates :project_id, :presence => true
Now make sure than an associated Project is valid like this:
validates :project, :associated => true
So:
t = Task.new
t.project_id = 1 # Value is accepted, regardless whether there is a Project with ID 1
t.project = Project.first # Any existing valid project is accepted
t.project = Project.new(:name => 'valid value') # A new valid project is accepted
t.project = Project.new(:name => 'invalid value') # A new invalid (or an existing invalid) project is NOT accepted!
It's a bit a pity that when assigning an ID through t.project_id = it's not checked whether this specific ID really exists. You have to check this using a custom validation or using the Validates Existence GEM.
To test these associations using RSpec with Remarkable matchers, do something like:
describe Task do
it { should validate_presence_of :project_id }
it { should validate_associated :project }
end
validates :project, :associated => true
validates :project_id, :presence => true
If you want to be sure that an association is present, you’ll need to
test whether the foreign key used to map the association is present,
and not the associated object itself.
http://guides.rubyonrails.org/active_record_validations_callbacks.html
attr_accessible :project_id
EDIT: assuming that the association is not optional...
The only way I can get it to thoroughly validate is this:
validates_associated :project
validates_presence_of :project_id,
:unless => Proc.new {|o| o.project.try(:new_record?)}
validates_presence_of :project, :if => Proc.new {|o| o.project_id}
The first line validates whether the associated Project is valid, if there is one. The second line insists on the project_id being present, unless the associated Project exists and is new (if it's a new record, it won't have an ID yet). The third line ensures that the Project is present if there is an ID present, i.e., if the associated Project has already been saved.
ActiveRecord will assign a project_id to the task if you assign a saved Project to project. If you assign an unsaved/new Project to project, it will leave project_id blank. Thus, we want to ensure that project_id is present, but only when dealing with a saved Project; this is accomplished in line two above.
Conversely, if you assign a number to project_id that represents a real Project, ActiveRecord will fill in project with the corresponding Project object. But if the ID you assigned is bogus, it will leave project as nil. Thus line three above, which ensures that we have a project if project_id is filled in -- if you supply a bogus ID, this will fail.
See RSpec examples that test these validations: https://gist.github.com/kianw/5085085
Joshua Muheim's solution works, but I hate being not be able to simply link a project to a task with an id like this:
t = Task.new
t.project_id = 123 # Won't verify if it's valid or not.
So I came up with this instead:
class Task < ActiveRecord:Base
belongs_to :project
validates :project_id, :presence => true
validate :project_exists
private
def project_exists
# Validation will pass if the project exists
valid = Project.exists?(self.project_id)
self.errors.add(:project, "doesn't exist.") unless valid
end
end
I have a basic invoice setup with models: Invoice, Item, LineItems.
# invoice.rb
class Invoice < ActiveRecord::Base
has_many :line_items, :dependent => :destroy
validates_presence_of :status
before_save :default_values
def default_values
self.status = 'sent' unless self.status
end
end
# item.rb
class Item < ActiveRecord::Base
has_many :line_items
validates_presence_of :name, :price
end
# line_item.rb
class LineItem < ActiveRecord::Base
belongs_to :item
belongs_to :invoice
before_save :default_values
validates_presence_of :invoice_id
validates :item_id, :presence => true
end
There is more in the model but I only presented the above for simplicity.
I get the following errors:
2 errors prohibited this invoice from being saved:
Line items invoice can't be blank
Status can't be blank
So two problems:
If I remove validates :invoice_id, :presence => true I don't get the Line items invoice can't be blank error message anymore, but why? I do want to validate the invoice_id on line_items, ALL line_items are supposed to have an invoice_id. How can I validate the invoice_id on line_items without getting an error?
Why am I getting the Status can't be blank error if I set it as a default value? I can probably set it up on the invoices_controller but I think the default value should be set in the model, right? How can I validate the presence of status and still have a default value in the model for it?
Both of these validation errors are occurring because validations get called before save (and before the before_save callback).
I'm assuming that you're using a nested_form to create the invoice and it's line items at the same time. If this is the case, you don't want to validates :invoice_id, :presence => true on the line items - the invoice and the line items are coming in at the same time, and the invoice hasn't been saved yet, so it doesn't have an id. If you leave the validation in, you'll need to create and save an empty invoice first, and then create the line items later so the invoice_id is available. If you only want to make sure invoice_id is still set after any edits, you can enforce this via validates :invoice_id, :presence => true, :on => :update this will skip the validation when the line item is being created (and the invoice_id isn't available yet).
You're running into problems with validates :status, :presence => true for similar reasons - the values coming in via the request are being validated against, and the "status" value isn't there. The before_save callback runs after validation. You can set the default value in the before_validation or after_initialization callback and the values will be there when validations are run.
Check out the Callbacks documentation for Rails for more info.
I'll start with 2:
before save is being executed only before save, meaning, after the object passed validation and is about to be saved. If the validation fails - it won't be executed.
as for 1:
Can you give an example of how you're trying to create an invoice?
Problem 1
Try validates_associated which checks that the associated models are all valid
Problem 2
Like most of the answers say before_save gets called after validations. The magic you're looking for is after_initialize which gets run after an object's initialize method is called.
class Invoice < ActiveRecord::Base
after_initialize :default_values
validates :status, presence: true
private
def default_values
self.status ||= 'sent'
end
end
Is it posible to validate the uniqueness of a child model's attribute scoped against a polymorphic relationship?
For example I have a model called field that belongs to fieldable:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :name, :scope => :fieldable_id
end
I have several other models (Pages, Items) which have many Fields. So what I want is to validate the uniqueness of the field name against the parent model, but the problem is that occasionally a Page and an Item share the same ID number, causing the validations to fail when they shouldn't.
Am I just doing this wrong or is there a better way to do this?
Just widen the scope to include the fieldable type:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :name, :scope => [:fieldable_id, :fieldable_type]
end
You can also add a message to override the default message, or use scope to add the validation:
class Field < ActiveRecord::Base
belongs_to :fieldable, :polymorphic => :true
validates_uniqueness_of :fieldable_id, :scope => [:fieldable_id, :fieldable_type], :message => 'cannot be duplicated'
end
As a bonus if you go to your en.yml, and enter:
activerecord:
attributes:
field:
fieldable_id: 'Field'
You are going to replace the default 'subject' that rails add to the errors with the one you specify here. So instead of saying: Fieldable Id has been already taken or so, it would say:
Field cannot be duplicated