Rails: validate number of associations - ruby-on-rails

How do you write validations for a number of associations that is externally defined? I've so far written something like this:
class Document
validate :publication_count
private
def publication_count
if publications.count > template.component_count
errors.add(:articles, 'too many')
elsif publications.count < template.component_count
errors.add(:articles, 'not enough')
end
end
Both publications and template are associations. I just get a rollback error with this code, even though the record should be valid.

Your code appears correct, so it seems likely that the associations aren't being set or saved correctly.
Did you check that:
publications and template are both assigned to the Document instance before you save?
the rollback error isn't for a different reason, like uniqueness failure?
this is the actual validation that's failing rather than another one?

Related

Rails: ActiveRecord::RecordNotUnique how to find for which column the exception was raised

Currently there is a unique index on my column with name "index_unique_devices_on_dsn" . when i am saving duplicating record i am getting mysql exception and i am handling that exception using class ActiveRecord::RecordNotUnique . if in the future if i am adding multiple columns and each column having its own unique index then how can i programmatically identify for which column this uniqueness exception was raised. ? i don't want to use .valid? method of rails as it is going to run validation again.
Normally you'd use the non-exception versions like save or create instead of save! and create!. Then check which columns are invalid on the model's validation errors.
user = User.create(name: "Duplicate")
if user.model.errors.include?(:name)
...
end
However, the ActiveRecord::RecordNotUnique exception comes from the database. There's no validation error.
You can fix this by adding your own uniqueness validation to the model which runs before Rails tries to save the model.
class User < ApplicationRecord
validates :name, uniqueness: true
end
Now you'll get a validation error.
However, this requires a query to check uniqueness. That can affect performance, and also lead to race conditions. Imagine you try to create two Users with the same name at the same time. They both check if the name is taken, they both see that it is not, and they both try to insert. One succeeds, one fails with an ActiveRecord::RecordNotUnique.
Instead, use the excellent database_validations gem which turns database errors into validation errors. This is safer, faster, and you only need one way to check for validation errors.
class User < ApplicationRecord
validates :name, db_uniqueness: true
end

Handling unique constraint exceptions inside transaction

I have two Rails models / tables that I want insert as part of transaction.
For example, here are roughly my tables:
Posts: (id)
Comments: (id, post_id, comment_text, user_id)
Commenters: (id, post_id, user_id), unique constraint on (post_id,
user_id)
Right now I'm trying approximately equivalent to:
ActiveRecord::Base.transaction do
Comment.create!(post: post, user: user, comment_text: '...')
begin
Commenters.find_or_create_by!(post: post, user: user)
rescue PG::UniqueViolation
end
end
This works 99.9% of time, but sometimes two concurrent comments will trigger a PG::UniqueViolation.
Even though I'm catching and suppressing the PG::UniqueViolation, the entire transaction fails due to:
ERROR: current transaction is aborted, commands ignored until end of transaction block
I realize I could already achieve this by joining Post and Comment table, but this is a simplified example.
Is there a simpler way to ensure both inserts happen as part of a transaction while still ignoring the unique violation since we can assume that the record already exists?
An exception raised inside the transaction does two things:
Rolls back the transaction so none of the changes made inside the transaction will persist in the database.
The exception propagates outside the transaction block.
So you can move your exception handler outside the transaction call:
begin
ActiveRecord::Base.transaction do
Comment.create!(post: post, user: user, comment_text: '...')
Commenters.find_or_create_by!(post: post, user: user)
end
rescue PG::UniqueViolation
retry
end
You could include a counter to only retry a few times if you wanted more safety.
You should have the associations properly set inside your models, so rails does the validation for you. Then you can simply rescue the possible ActiveRecord::RecordInvalid (with message Validation failed: Attribute has already been taken).
If you want to read some more about uniqueness validtion this should come in handy: http://guides.rubyonrails.org/active_record_validations.html#uniqueness
To me it actually looks like your Commenter model is not really necessary. It just consists of derived information which can also be drawn directly from the Comments model (there you already store post_id and user_id) so you could drop the Commenter class entirely. Then take care to validate Comment on creation by for example setting
class Comment
belongs_to :post
belongs_to :user
validates :user_id, uniqueness: {scope: :post_id}
end
But this way you would allow a user to comment only once.
I propose you drop the uniqueness constraint and construct the information stored in Commenter by making distinct selects on the Comment model.
PS: Models in rails are written in Uppercase and singular while tables are referred to (by symbols) in lowercase and plural.
The error itself happens because of multi-threading behaviour of your app.
You need to rescue ActiveRecord::RecordNotUnique instead of PG-specific error.
Also perhaps put transaction inside begin rescue end block.
And retry to continue with another transaction within rescue block. Something like another answer suggested.

Can I determine the order of validation error messages in Rails?

I want to get the order of validation messages to go in the same order as they do on our form.
We have three classes:
class User
accepts_nested_attributes_for :pledges
end
class Pledge
accepts_nested_attributes_for :companies
validates_presence_of :pledgor_surname
end
class Company
validates_presence_of :name
end
In one form, we potentially have to take attributes for all three, so we get params like the following:
{"pledges_attributes"=>
{"0"=>
{"pledgor_surname"=>"",
"id"=>"230",
"companies_attributes"=>
{"0"=>
{"id"=>"125",
"name"=>""
}
}
}
}
}
When I call #user.update(params), it fails validation as I'd expect. But the errors#full_messages list looks like this:
["Company name can't be blank", "Pledgor surname can't be blank"]`
And the errors appear on the page in the same order.
Short of hacking the messages object, is there a way to tell Rails which order to place the messages in, or at least which of pledgor errors and company errors should go first?
No, they are returned in a hash and hashes do not provide reliable ordering. This could be overridden, which is typically done by adding files to the lib folder and specifying your overrides in the rails config.
Edit According to the comment below since Ruby 1.9.3, hashes are actually ordered, so ignore what I said.

ActiveRecord uniqueness validation prevents update

I'm writing a web app using Rails, part of which includes giving users the ability to leave reviews for things. I wanted to put a validation in the review model to ensure that one user can't leave multiple reviews of the same item, so I wrote this:
class NoDuplicateReviewValidator < ActiveModel::Validator
def validate(record)
dup_reviews = Review.where({user_id: record.user,
work_id: record.work})
unless dup_reviews.length < 1
record.errors[:duplicate] << "No duplicate reviews!"
end
end
end
This validator has the desired behavior, i.e. it guarantees that a user can't review a work twice. However, it has the undesired side-effect that a user can't update an already existing review that he/she left. I'm using a really simple
def update
#review.update(review_params)
respond_with(#work)
end
in the reviews controller. How can I change either the validator or the update method so that duplicate reviews are prevented but updates are allowed?
I'm very new to Rails and web development, so I'm sure I've done something goofy here. I didn't use one of the built-in unique validators because what is unique is the user/work pair; there can more than one review by the same user of different works, and there can be more than one review of the same work by different users.
You can use validates_uniqueness_of on multiple attributes, like this:
validates_uniqueness_of :user_id, :scope => :work_id
Then a user would not be allowed to review a already reviewed work.
#Sharvy Ahmed's answer is definitely the best, as long as the case is simple enough – the OP's case seems like one of them.
However, if the conditions are more complex, you may need/want to write your custom validation. For that purpose, here's an example (checked with Rails 6.0).
class NoDuplicateReviewValidator < ActiveModel::Validator
def validate(record)
dup_reviews = Review.where(user_id: record.user,
work_id: record.work)
dup_reviews = dup_reviews.where.not(id: record.id) unless record.new_record?
if dup_reviews.count > 0
record.errors[:duplicate] << "No duplicate reviews!"
end
end
end
The idea is,
In create, all the relevant DB records retrieved with where can and should be used to judge the uniqueness. In the example new_record? is used to check it out, but it is actually redundant (because nil id matches no records).
In update, the DB row of the record to update must be excluded from the unique comparison. Otherwise, the update would always fail in the validation.
The count method is slightly more efficient in terms of DB transaction.

rails singular_collection_ids validation

I have a form in rails app that allows to update singular_collection_ids attribute (relation type is has_many through). And I also need to validate it before update.
The problem that is validation requires previous value of object, but there is no method singular_collection_ids_was to provide this value. Also singular_collection_ids method works directly with join table with no temporary values, so
self.class.find(id).singular_collection_ids
inside validation did not help.
Is there any way to get previous value in stage of validation?
Not sure it works (and it's definitely quirky) but you could try this :
class Player
has_many :join_models, before_remove: :prevent_achievement_removal
def prevent_achievement_removal( join_model )
errors.add( :base, "Cannot remove an achievement this way !" )
raise ::ActiveRecord::RecordInvalid
end
end
according to the doc, if the callback raises any error, the record is not removed. However, something does not really feel right about this... I'll try to think about a better solution.

Resources