Rails custom validation checking for duplicates - ruby-on-rails

A table has following fields: badge_id, output_id, timely, removed, updated_at. For each badge_id, there can't have two valid records with the same output_id. But it doesn't mean that (badge_id, output_id) is a unique combination. Removed column indicates the current row has been removed or not. Basically delete or update operation triggers inserting a new row in the table with the latest change. So for example, we have a record like this:
badge_id| output_id| removed| timely | updated_at
1 | 1 | N | Y | 2013-11-26
To remove that record, we actually insert another row and now it reads like
badge_id| output_id| removed| timely | updated_at
1 | 1 | N | Y | 2013-11-26
1 | 1 | Y | Y | 2013-11-27
Because the latest record of (badge_id: 1, output_id: 1) has removed column set, it means that combination has been deleted. But I can't have two rows of same (badge_id: 1, output_id: 1), both have removed as "N" like:
badge_id| output_id| removed| timely | updated_at
1 | 1 | N | N | 2013-11-26
1 | 1 | N | Y | 2013-11-27
So every time to add a new output_id for a certain badge_id, I have to check for duplication. But usual validates uniqueness of (badge_id, output_id) from ActiveModel doesn't work here. How do I write a clean custom validation for this? Thanks.
UPDATE:
I think I might have missed some key points. A record can be added and then deleted and then added repeatedly. So a combination of (badge_id, output_id, removed) isn't unique either. When add a new record, we need to check for (badge_id, output_id), whether latest record has removed set as 'Y' or not.
So for possible answer like
validate_uniqueness_of :badge_id, scope: [:output_id],
conditions: -> { where(removed: "N") }
At the condition where clause, it should have order by updated_at desc and the first one has removed: 'N'. How do I fit that kind of condition into this one line code? Or there's a better way of doing this?

You could do something like:
validates :unique_badge_and_output_ids
Then unique_badge_and_output_ids could be:
def unique_badge_and_output_ids
unless Object.find_by_badge_id_and_output_id_and_removed(self.badge_id, self.output_id, self.removed).blank?
self.errors.add "record already exists" # obviously a better error here would be ideal
end
end

You can specify an SQL condition on validates_uniqueness_of:
http://api.rubyonrails.org/classes/ActiveRecord/Validations/ClassMethods.html#method-i-validates_uniqueness_of
It is also possible to limit the uniqueness constraint to a set of
records matching certain conditions. In this example archived articles
are not being taken into consideration when validating uniqueness of
the title attribute:
class Article < ActiveRecord::Base
validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
end
So in your example:
class YourModel < AR::Base
validate_uniqueness_of :badge_id, scope: [:output_id],
conditions: -> { where(removed: "N") }
end

Hopefully I understand your use case properly.
Try validating the uniqueness of the removed and scoping it to both the badge_id and output_id columns but only when the removed field is N:
class Model < ActiveRecord::Base
validates_uniqueness_of :removed,
scope: [:badge_id, :output_id],
conditions: -> { where.not(removed: 'Y') }
end
There's a chance it might just work.

Related

Ruby On Rails + PostgreSQL: If not matching data found, return the row as nil

I have these two models:
class ModelA < ApplicationRecord
has_one :model_b
has_one :model_b
end
class ModelB < ApplicationRecord
belongs_to :model_a
end
Data in DB tables:
model_a
id | ...
1 | ...
2 | ...
3 | ...
model_b
id | model_a_id | value_a | value_b
1 | 1 | abc | def
2 | 2 | ghi | jkl
For every record in the the model_a, I want to get a record from table model_b - I can get it like this.
ModelA.joins('LEFT JOIN model_b ON model_b.model_a_id = model_a.id')
This query would return me the rows with ID 1 and 2 from the table model_a. However, I would like to get returned also the row with ID 3 from the table model_a and for this row, I would want to get returned the associated (in this case, non-existing) row from model_b with these values:
value_a: NULL
value_b: NULL
How do I do that? I tried to play with different JOINS, with CASE IF/ELSE/END, but I happened to not find the right combination.
As I need to be able to filter/query these data, I believe it would be probably better to solve this on the PSQL level, rather than on Rails.
EDIT: RIGHT JOIN returns me only the first 2 rows form model_a.
EDIT2: This is the desired output:
modal_a.id | modal_b.value_a | modal_b.value_b
1 | abc | def
2 | ghi | jkl
3 | null | null
Thank you advance.
That's called a left outer join
ModelA.joins('LEFT OUTER JOIN model_b ON model_b.model_a_id = model_a.id')
It will return all ModelA records even if no modelB record is present.
In pure rails...
ModelA.includes(:model_b)
To explicitly include the columns that may have nil...
records = ModelA.includes(:model_b).select('*, model_b.value_a as model_b_value_a, model_b.value_b as model_b_value_b')
This lets you do records.first.id to see the model_a id, and records.first.model_b_value_a etc to see the value from model_b
For records without an associated model_b record, records.first.model_b_value_a will return nil

How to control non-fixed cardinality on a relation in Rails?

I'm writting an app that manages appointments to different services. Each service "capacity" is determined by one or more Timetables, meaning that service A may have 2 "desks" form June 1 to June 30 while having only 1 from July 1 to August 31, so I can create 2 appointments for '2020-06-03 9:00' but only 1 for '2020-07-03 9:00'.
Everything is modeled just right and I have a custom validator for Appointments on create that checks the cardinality but that isn't enough to prevent two users creating the last available appointment at the same time is it?
How can I control the correct cardinality of this kind of relation without blocking the whole Appointments table?
Appointment creation is done in one place and one place only in the code, in Appointment.create_appointment(params) , is there a way to make that method locked in rails?
There are several ways to implement such restrictions, and the best is to let the database handle hard constrains.
Option one
Considering you have two models, Timetable and Appointment, add available_slots integer column to the Timetable model and decrease that number upon appointment creation and let the database raise an exception if that number goes below zero. In this case, Postgress will lock the column while updating it at the same time, preventing race conditions.
So Timetable could look like:
+----+--------------+--------------+-----------------+
| ID | time_from | time_to | available_slots |
+----+--------------+--------------+-----------------+
| 1 | '2020-03-21' | '2020-04-21' | 2 |
| 2 | '2020-04-22' | '2020-05-21' | 3 |
+----+--------------+--------------+-----------------+
In MySQL, you would make it an unsigned integer, but since Postgres doesn't support it, you have the option to add a positive number check constrain to the available_slots column:
Pure SQL:
ALTER TABLE timetables ADD CONSTRAINT available_slots CHECK (available_slots > 0)
A migration will look like:
class AddPositiveConstraintToTimetable < ActiveRecord::Migration[6.0]
def self.up
execute "ALTER TABLE timetables ADD CONSTRAINT available_slots CHECK (available_slots > 0)"
end
def self.down
execute "ALTER TABLE timetables DROP CONSTRAINT available_slots."
end
end
Add to Appointment model the logic that will decrease available_slots:
belongs_to :timetable
before_create :decrease_slots
def decrease_slots
# this will through an exception from the database
# in case if available_slots are already 0
# that will prevent the instance from being created.
timetable.decrement!(:available_slots)
end
Catch the exception from AppointmentsController:
def create
#appointment = Appointment.new(appointment_params)
# here will be probably some logic to find out the timetable
# based on the time provided by the user (or maybe it's in the model).
if #appointment.save
redirect_to #appointment, notice: 'Appointment was successfully created.'
else
render :new
end
end
Option two
Another way to do it is to add a new model, for example, AvailableSlot that will belong to Appointment and Timetable, and each record in the table will represent an available slot.
For example, if Timetable with id 1 will have three available slots, the table will look like:
Timetable.find(1).available_slots
+----+---------------+
| ID | timetable_id |
+----+---------------+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
+----+---------------+
Then add a unique index constrain to the available_slot_id column in the appointments table:
add_index :appointments, :available_slot_id, unique: true
So every time you create an appointment and associate it with an available slot, the database will, through an exception, if there is a record with the same available_slot_id.
You will have to add logic to find an available slot. A raw example in Appointment model:
before_create :find_available_slot
def find_available_slot
# first find a timetable
timetable = Timetable.where("time_from >= ? AND time_to <= ?", appointment_time, appointment_time).first
# then check if there are available slots
taken_slots = Appintment.where(timetable.id: timetable.id).size
all_slots = timetable.available_slots.size
raise "no available slots" unless (all_slots - taken_slots).positive?
# huzzah! there are slots, lets take the last one
self.available_slot = timetable.available_slots.last
end
That code can be simplified if you add a status column to available_slots that will be changed when an appointment is created, but I leave it to you to figure that out.
These options are based on similar approaches that I've seen on a production Rails applications with a lot of concurrent transactions going on (millions per day) that could cause raise conditions.

Unlimited arbitrary properties (key/value pairs) for an ActiveRecord model

Using ruby on rails, I have a Customer table that I want to be able to add unlimited properties (key value pairs) to. I'm not sure what the key/value pairs will be yet so I'm not sure how to do this. For example, one customer could be:
Customer 1 properties:
color: 'yellow'
brand: 'nike'
sales: '33'
Customer 2 properties:
color: 'red'
phone_number: '1111111111'
purchases: '2'
Basically, customers can have any number of properties in a key/value pair.
How can I do this?
The "traditional" way to do this is with the Entity-Attribute-Value, or EAV pattern. As the name suggests, you'll create a new table with three columns: one for the "entity," which in this case is the Customer, one for the "attribute" name or key, and one for the value. So you'd have a table like this:
customer_properties
+----+-------------+--------------+------------+
| id | customer_id | key | value |
+----+-------------+--------------+------------+
| 1 | 1 | color | yellow |
| 2 | 1 | brand | nike |
| 3 | 1 | sales | 33 |
| 4 | 2 | color | red |
| 5 | 2 | phone_number | 1111111111 |
| 6 | 2 | purchases | 2 |
+----+-------------+--------------+------------+
You'll definitely want an INDEX on key and maybe on value (and customer_id, of course, but Rails will do that for you when you use relation or belongs_to in your migration).
Then in your models:
# customer.rb
class Customer < ActiveRecord::Base
has_many :customer_properties
end
# customer_property.rb
class CustomerProperty < ActiveRecord::Base
belongs_to :customer
end
This enables usage like this:
customer = Customer.joins(:customer_properties)
.includes(:customer_properties)
.where(customer_properties: { key: "brand", value: "nike" })
.first
customer.customer_properties.each_with_object({}) do |prop, hsh|
hsh[prop.key] = prop.val
end
# => { "color" => "yellow",
# "brand" => "nike",
# "sales" => "33" }
customer.customer_properties.create(key: "email", value: "foo#bar.com")
# => #<CustomerProperty id: 7, customer_id: 1, key: "email", ...>
As database design goes this is pretty solid, but as you can see it has some limitations: In particular, it's cumbersome. Also, you're restricted to a single value type (:string/VARCHAR is common). If you go this route you'll likely want to define some convenience methods on Customer to make accessing and updating properties less cumbersome. I'm guessing there are probably gems specifically for making the EAV pattern work nicely with ActiveRecord, but I don't know them off the top of my head and I hope you'll forgive me for not googling, since I'm mobile.
As Brad Werth points out, if you just need to store arbitrary properties and not query by them, serialize is a great alternative, and if you use PostgreSQL even the querying problem is surmountable thanks to its great hstore feature.
Good luck!
You may want to look into the hydra_attribute gem, which is an implementation of the Entity-Attribute-Value (EAV) pattern for ActiveRecord models.
You should be able to use serialize for this, and assign your properties hash to your properties attribute, and retrieve them in the same way.

Will Paginate can order by certain columns but not by other columns on Model

I am doing some maintenance on a Rails 2 application using Ruby 1.8.7. I am using will_paginate.
I have the following problem:
When calling current_user.products.paginate(:page => 1, :order => "updated_at asc") it orders, as expected, based on the updated_at property in products.
However, when calling current_user.products.paginate(:page => 1, :order => "released_at asc") it does not order on released_at as expected. In fact it doesn't matter whether I specify ordering to happen according to "asc" or "desc", I get the same collection returned.
updated_at and released_at are both attributes defined on the model and exists in the database and all values are non-null. There is no default_scope defined on the models.
What could be causing this and how to correct this?
Adding Desc products below (for brevity I am only listing the relevant fields):
| Field | Type | Null | Key | Default | Extra |
| released_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
The Product model inherits from a model that also defines the released_at attribute. The method for released_at is overloaded in the Ruby on Rails code so that if one would call product.released_at on an instance product of Product, then it would actually call the released_at value on the parent model. As a result, from the rails console it always looks as if released_at is not null and contains a value.
However, looking at the database directly shows that released_at is null in the Product model and as such, even though the will_paginate request goes through correctly, no ordering is done because the values are all the same (null). In the case where the exact same query is performed on the updated_at attribute, sorting works because that attribute is not null. I missed this because I checked for empty values from the Rails Console rather than in MySQL directly.
My solution was to define a named_scope on the Product model that sorts based on the value in the parent model. I then call the paginate function on the named scope.
Sometimes it need to have an index defined for ordering fields to make it working.

Rails Models and unique combinations

I have a Rails app which has a table called friendrequests. It looks like this:
user1_id:integer user2_id:integer hasaccepted:boolean
I'm creating an option to add friends, but a friendrequest can only be send once. So you cannot have something like this in the database's data:
user1_id | user2_id | hasaccepted
1 | 2 | false
1 | 2 | false
or
user1_id | user2_id | hasaccepted
1 | 2 | false
2 | 1 | false
The user1_id/user2_id combination must be unique, not the columns themselves, so this would be possible:
user1_id | user2_id | hasaccepted
1 | 2 | false
1 | 3 | false
Is it possible to define this in a model? How can I do this?
For the first case, in your FriendRequest model, use validates_uniqueness_of(:user1_id, :scope => :user2_id). You might also need the reverse. For the second case I'd override #validate in FriendRequest and do a check in there (see the API docs for details on how the method should perform).
Try this:
validates_each :user1_id, :user2_id do |record, attr, value|
if FriendRequest.exists?( :user1_id => [record.user1_id, record.user2_id],
:user2_id => [record.user1_id, record.user2_id])
record.errors.add attr, 'Duplicate friend request'
end
end
I would also make sure to add an index to the database itself. Validation checks in the model can't be relied upon to preserve data integrity because there are edge cases in a concurrent system where the validation will not protect you.
eg, another record may be inserted after the validation check, but before the new record is created:
Model A: Validate (pass)
Model B: Validate (pass)
Model A: Insert (success)
Model B: Insert (success: bad data)

Resources