Rails validation uniqueness using scope with 2 different table columns - ruby-on-rails

I've tables - rooms (id, room_number), facility_rooms (id, facility_id, room_id - reference column)
I want to ensure room_number should be unique per facility. So how can we write the rails validation with scope to refers the columns from 2 different tables? In this, I want the uniqueness in combination of room_number from rooms table and facility_id from facility_rooms table.

You can add the validation in your model like below:
# facility_room.rb
class FacilityRoom
validates :facility_id, uniqueness: { scope: :room_id, message: 'add validation error message here.' }
end
You can also add a unique index at the database level to avoid duplication in case the model validation is not able to catch the duplicates while processing concurrent requests:
class AddUniqueIndexToFacilityRooms < ActiveRecord::Migration
def change
add_index :facility_rooms, [:facility_id, :room_id], unique: true
end
end

Related

SQL How to get only 1 true value

I am building an application and I need to be able to sing a lead teacher
I need to prevent that 2 teachers share the title of lead for a particular class
class CreateClassroomTeachers < ActiveRecord::Migration[5.2]
def change
create_table :classroom_teachers do |t|
t.belongs_to :classroom
t.belongs_to :teacher
t.boolean :lead, default: false
end
add_index :household_people, [:classroom_id, :teacher_id], unique: true
# Only one teacher in a classroom can be lead
end
end
I have this in my model
class ClassroomTeacher < ApplicationRecord
belongs_to :classroom
belongs_to :teacher
validate :only_one_is_lead_teacher
def only_one_is_lead_teacher
if lead
if ClassroomTeacher.where(classroom_id: classroom_id, lead: true).count > 0
errors.add(:lead, "There can only be one (1) lead teacher per classroom")
end
end
end
end
The problem on this is that on Create I can have 2 or more teachers be lead
Thanks for the help
There's several ways for achieving this with constraints, triggers etc. – depending on what your respective database server supports.
What should work at least in Postgres (even though it might be slightly hacky) is to set a unique index on %i[classroom_id lead] and make sure that lead is either true or NULL. This should work because Postgres treats NULL values as distinct, meaning that it doesn't complain if multiple NULL values are stored in a column that has a uniqueness constraint on it.
If you want to solve it in code (which personally I would not recommend, because your database might be access by things other than your code and even your code can work around it, e.g. by directly writing to the database instead of using ActiveRecord's higher level methods), here's how I've done this in the past:
class ClassroomTeacher < ActiveRecord::Base
before_save :ensure_only_one_lead_teacher
private
def ensure_only_one_lead_teacher
# We don't have to do this unless the record is the one who should be the (new) lead.
return unless lead?
# Set all other records for the class room to lead = false.
self.class.where(classroom_id: classroom_id).update_all(lead: false)
# Now if the record gets persisted, it will be the only one with lead = true.
end
end
A probably slightly more "correct" approach would be to ensure the uniqueness after the record has been persisted:
class ClassroomTeacher < ActiveRecord::Base
after_commit :ensure_only_one_lead_teacher
private
def ensure_only_one_lead_teacher
# We don't have to do this unless the record is the one who should be the (new) lead.
return unless lead?
# Set all other records for the class room to lead = false. Note that we now have to exclude
# the record itself by id because it has already been saved.
self.class.where.not(id: id).where(classroom_id: classroom_id).update_all(lead: false)
end
end
As per the migration, attributes for the model are
ClassroomTeacher: classroom_id, teacher_id, lead
Considering teachers are being added to class:
/controller file
def create
ClassroomTeacher.create(teacher_id: data1, classroom_id: data2, lead: data3)
end
Possible sample data with ideal values would be:
id classroom_id teacher_id lead
1 1 3 false
2 2 4 true
3 1 2 false
4 1 5 true
Now you need to avoid any new teachers being added to the classroom as lead. Model validation code could be
validate :only_one_is_lead_teacher
def only_one_is_lead_teacher
if self.lead
class_obj = ClassroomTeacher.where(classroom_id: self.classroom_id, lead: true).first
if class_obj.present?
self.errors.add(:lead, "There can only be one (1) lead teacher per classroom")
return false
end
end
end

ActiveRecord validates inclusion in list - list isn't updated after new associated model created

I have a Company model and an Employer model. Employer belongs_to :company and Company has_many :employers. Within my Employer model I have the following validation:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
I'm running into a problem where the above validation fails. Here is an example setup in a controller action that will cause the validation to fail:
company = Company.new(company_params)
# company_params contains nested attributes for employers
company.employers.each do |employer|
employer.password = SecureRandom.hex
end
company.employers.first.role = 'Admin' if client.employers.count == 1
company.save!
admin = company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update(some_attr: 'some_val')
On the last line in the example code snippet, admin.update will fail because the validation is checking to see if company_id is included in the list, which it is not, since the list was generated before company was saved.
Obviously there are ways around this such as grabbing the value of company.id and then using it to define admin later, but that seems like a roundabout solution. What I'd like to know is if there is a better way to solve this problem.
Update
Apparently the possible workaround I suggested doesn't even work.
new_company = Company.find(company.id)
admin = new_company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update
# Fails validation as before
I'm not sure I understand your question completely, but there is an issue in this part of the code:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
The validation is configured on the class-level, so it won't work well with updates on that model (won't be re-evaluated on subsequent validations).
The docs state that you can use a block for inclusion in, so you could try to do that as well:
validates :company_id, inclusion: {in: ->() { Company.pluck(:id).prepend(nil) }}
Some people would recommend that you not even do this validation, but instead, have a database constraint on that column.
I believe you are misusing the inclusion validator here. If you want to validate that an associated model exists, instead of its id column having a value, you can do this in two ways. In ActivRecord, you can use a presence validator.
validates :company, presence: true
You should also use a foreign key constraint on the database level. This prevents a model from being saved if there is no corresponding record in the associated table.
add_foreign_key :employers, :companies
If it gets past ActiveRecord, the database will throw an error if there is no company record with the given company_id.

Rails validates uniqueness cross models

I have two models: event.rb and bag.rb
An event has an attribute called slug and a bag has an attribute called bag_code.
I generate a view based on the bag_code attribute. E.g. if the bag-code is "4711" I load records based on that code.
Now for some occasions, a user can define a slug URL in an event and in this case it should overwrite the bag-code.
What I don't want is that a user can choose a slug with a value which is already a bag_code (in this case it should be forbidden to choose the slug "4711") as this would cause troubles in my view, so it has to be unique in two models attributes.
I tried to solve this via scope
validates_uniqueness_of :slug, scope: [:bag_code]
but that would only work within the same model.
The association between my models is:
event.rb
has_many :bags
A custom validation method would work for this case. Here is how you would set it up:
event.rb
validates :slug, uniqueness: true
validate :slug_is_unique_from_bag_codes
private
def slug_is_unique_from_bag_codes
if Bag.find_by bag_code: slug
errors.add :base, "The slug is already being used as a bag code"
end
end

Can validate_uniqueness_of work with custom scopes?

I'm working on an RoR project and I'd like to have a uniqueness validation on one of my models that checks against a custom scope:
class Keyword < ActiveRecord::Base
belongs_to :keyword_list
scope :active, -> { where("expiration > ?", DateTime.now) }
validates :name, uniqueness: { scope: [:active, :keyword_list_id] }
end
Only, this doesn't work. It checks the database for an active column, which doesn't exist and throws this error:
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column keywords.active does not exist
So, my question is there any way to make this work, or do I have to write a custom validator? And if so, are there any tips on what it should look like to hitting the database too much?
No, you will have to write a custom validation.
Try this.
# In app/models/keyword.rb
validate :freshness
private
def freshness
if Keyword.active.find_by(name: self.name, keyword_list_id: self.keyword_list_id)
errors.add(:base, "Name is not fresh enough.") # Fails the validation with error message
end
end
Here's another interesting point, you cannot rely on validates_uniqueness_of, or any other uniqueness validator in rails, because validations are not run atomically, meaning that if two identical records are inserted at the same time, and there is no SQL constraint validating uniqueness, the ruby validations will pass and both records will be inserted.
What I'm trying to say here is that if your validations are mission-critical, use a SQL constraint.

Rails: Validate unique combination of 3 columns

Hi I wan't to validate the unique combination of 3 columns in my table.
Let's say I have a table called cars with the values :brand, :model_name and :fuel_type.
What I then want is to validate if a record is unique based on the combination of those 3. An example:
brand model_name fuel_type
Audi A4 Gas
Audi A4 Diesel
Audi A6 Gas
Should all be valid. But another record with 'Audi, A6, Gas' should NOT be valid.
I know of this validation, but I doubt that it actually does what I want.
validates_uniqueness_of :brand, :scope => {:model_name, :fuel_type}
There is a syntax error in your code snippet. The correct validation is :
validates_uniqueness_of :car_model_name, :scope => [:brand_id, :fuel_type_id]
or even shorter in ruby 1.9.x:
validates_uniqueness_of :car_model_name, scope: [:brand_id, :fuel_type_id]
with rails 4 you can use:
validates :car_model_name, uniqueness: { scope: [:brand_id, :fuel_type_id] }
with rails 5 you can use
validates_uniqueness_of :car_model_name, scope: %i[brand_id fuel_type_id]
Depends on your needs you could also to add a constraint (as a part of table creation migration or as a separate one) instead of model validation:
add_index :the_table_name, [:brand, :model_name, :fuel_type], :unique => true
Adding the unique constraint on the database level makes sense, in case multiple database connections are performing write operations at the same time.
To Rails 4 the correct code with new hash pattern
validates :column_name, uniqueness: {scope: [:brand_id, :fuel_type_id]}
I would make it this way:
validates_uniqueness_of :model_name, :scope => {:brand_id, :fuel_type_id}
because it makes more sense for me:
there should not be duplicated "model names" for combination of "brand" and "fuel type", vs
there should not be duplicated "brands" for combination of "model name" and "fuel type"
but it's subjective opinion.
Of course if brand and fuel_type are relationships to other models (if not, then just drop "_id" part). With uniqueness validation you can't check non-db columns, so you have to validate foreign keys in model.
You need to define which attribute is validated - you don't validate all at once, if you want, you need to create separate validation for every attribute, so when user make mistake and tries to create duplicated record, then you show him errors in form near invalid field.
Using this validation method in conjunction with ActiveRecord::Validations#save does not guarantee the absence of duplicate record insertions, because uniqueness checks on the application level are inherently prone to race conditions.
This could even happen if you use transactions with the 'serializable' isolation level. The best way to work around this problem is to add a unique index to the database table using ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the rare case that a race condition occurs, the database will guarantee the field's uniqueness.
Piecing together the other answers and trying it myself, this is the syntax you're looking for:
validates :brand, uniqueness: { scope: [:model_name, :fuel_type] }
I'm not sure why the other answers are adding _id to the fields in the scope. That would only be needed if these fields are representing other models, but I didn't see an indication of that in the question. Additionally, these fields can be in any order. This will accomplish the same thing, only the error will be on the :model_name attribute instead of :brand:
validates :model_name, uniqueness: { scope: [:fuel_type, :brand] }

Resources