How to control non-fixed cardinality on a relation in Rails? - ruby-on-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.

Related

How can I sort two different models with different attributes by created_at with postgres? [duplicate]

I want to show a time link mixing comments and post so I have this objects
#posts = Post::all()
#comments = Comment::all()
If I do this
#post.each ...
... end
#comments.each ...
... end
I will get first posts and after this, the comments. But I want a timeline, how i can create this?
I need to combine both object to create just one ordered list, example:
In post:
id | name | date
1 | post1 | 2015-01-01
2 | post2 | 2013-01-01
In comments:
id | name | date
1 | comment1 | 2014-01-01
2 | comment2 | 2016-01-01
if I do this
post.each ...
comments.each ...
the result will that:
-post1
-post2
-comment1
-comment2
But i need order by date to get
-post2
-comment1
-post1
-comment2
Thanks, and sorry for my ugly english.
Posts and comments are different models (and different tables), so we can't write SQL to get sorted collection, with pagination etc.
Usually I use next approach when I need mixed timeline.
I have TimelineItem model with source_id, source_type and timeline_at fields.
class TimelineItem < ApplicationRecord
belongs_to :source, polymorphic: true
end
Then I add in models logic to create timeline_item instance when needed:
has_many :timeline_items, as: :source
after_create :add_to_timeline
def add_to_timeline
timeline_items.create timeline_at: created_at
end
Then search and output as simple as
TimelineItem.includes(:source).order(:timeline_at).each { |t| pp t.source }
Solution#1
You can achieve this using UNION query.
sql= 'SELECT id, name, date FROM posts UNION ALL SELECT id, name, date FROM comments ORDER BY date'
ActiveRecord::Base.connection.execute(sql)
but union query will only work when you have same column names in both tables.
Solution#2 Add ordering logic in your view.
If you are simply displaying these records on html page then let them load on page without any specific order i.e.(first posts and then comments). Write javascript code to sort DOM elements which will run after page load.
for ref: Jquery - sort DIV's by innerHTML of children
Solution#3 Refactor your DB schema and put posts and comments in same database table. Then you will be able to query on single table.
Something like this,
class Text < ActiveRecord::Base
end
class Post < Text
end
class Comment < Text
end
Query will be Text.order(:date)
Refactoring your DB schema is too much to solve this problem. Do it if it makes sense for your application.

Create multiple records in the database with same :id

I am trying to create several instances of the same record on my rails app. I have tests and questions. tests can have many questions.
I also have testsessions. My testsessions table looks like this:
id | user_id | test_id | question_id | answer
I am trying to get the controller to create several instances of the same testsession, with the only thing differing being the question_id, which will be pulled from the questions that have the same test_id as the testsession.
At the moment I have this in my controller:
def new
#test = Test.find(params[:test_id])
#testsession = #test.testsessions.new
#testsession.user_id = current_user.id
#testsession.save
#questions = #test.questions
redirect_to action: :index
end
But I don't know how to make it create several instances based on the question_id.
I have an array of the question_id that I want to use:
#questions = Question.where(test_id: #test.id).pluck(:id)
But I don't know how to put this into the controller...in the end I would want the table to look something like this:
id | user_id | test_id | question_id | answer
1 | 1 | 1 | 1 |
1 | 1 | 1 | 2 |
1 | 1 | 1 | 3 |
1 | 1 | 1 | 4 |
Answer is always empty because this will be input by the user later using update
Any ideas on how to do this would be greatly appreciated!
I think I understand what you're trying to achieve: you have a test that has_many questions. When someone does the test, you want to create a test session for every question in advance.
Try this:
def new
#test = Test.find(params[:test_id])
#test.questions.each do |question|
#test.testsessions.create user: current_user, question: question
end
redirect_to action: :index
end
As a side note: the new controller action isn't really a good place to create things, because new is (normally) accessible through HTTP GET requests.
Assume for a moment that the app you write is accessible to Google. If Google finds a link to this controller action, it will visit it and accidentally create new test session objects. Possibly many times.
This is why controller actions that create or change something in your app should only be accessible through other methods, like a POST — search engines and other robots won't touch those.
Even if your app is not accessible in public, it's a good habit to get into.

How to create a special order_number that is based off the table id column in PostgreSQL and Rails

I have an Orders table in a SQL database (PostgreSQL if it matters, and Rails 4.04) and I want the column "number" in the orders table to kind of shadow the id column. In other words I want an automatic sequential field but I want it to be in the form:
Order::PRODUCT_NU_PREFIX + ((Order::STARTING_PRODUCT_NO + order.id).to_s)
So that if I set
PRODUCT_NU_PREFIX = 'PE' and
STARTING_PRODUCT_NO = '11681'
Then the first order I create will have a product number:
KB11682 and then
KB11683
SHould I do this purely in PostgreSQL or is there a good way to do it in Rails? Keep in mind I'll need to know what the latest Order.id is when an order comes in because when I save that new record I want that field to get saved correctly.
You're looking at uuid's (Universally Unique Identifiers)
These basically allow you to assign a special id to a record, giving you an instant reference throughout your app. Such use cases for this include the likes of order numbers, API keys and message ID's
We actually implement what you're asking quite regularly:
#app/models/order.rb
Class Order < ActiveRecord::Base
before_create :set_num
private
def set_num
unless self.num
loop do
token = SecureRandom.hex(10)
break token unless self.class.exists?(num: token)
end
end
end
end
orders
id | num | other | attrs | created_at | updated_at
Rails has a series of built-in UUID-generators:
SecureRandom.uuid #=> "1ca71cd6-08c4-4855-9381-2f41aeffe59c"
SecureRandom.hex(10) # => "52750b30ffbc7de3b362"
SecureRandom.base64(10) # => "EcmTPZwWRAozdA=="

Rails custom validation checking for duplicates

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.

Approach to limit the visibility of data

Ok, suppose to have this db schema (relation):
|User | (1-->n) |Customer | (1-->n) |Car | (1-->n) |Support |
|--------| |---------| |-----| |-----------|
|id | | user_id | |Brand| |Description|
|username| |lastname | |PS | |Cost |
|password| |firstname| |seats| |hours |
|... | |.. | |... | |... |
The table User is generated by Authlogic.
I have 2 registred users, each one has his customers, etc. . With Authlogic I'm able to allow only authenticated users to reach controllers/views. That's fine, that's what Authlogic is made for.
Now I need to be sure that the user#1 will never reach informations belonging to customers of user#2.
In other words:
if the user#1 goes to http://myapp.com/cars he will see the list of cars belonging to customers of user#1
if the car with the id=131 belongs to the customer of user#1, only user#1 have to be able to reach this information (http://myapp.com/car/1). If the user#2 insert in the browser the same link he doesn't have to be able to see this information.
Some people suggested me to create a relation between the user and each db table in order to check if a record is associated to the current_user.
What do you think? What is the best approach/solution?
So you have 2 things:
In index page of cars controller only cars which belong to the current user should be shown.
You want to restrict show pages to the owner.
As for the index i suggest something like:
def index
# find logic
#cars = Car.find(:all,:conditions=>{:customer_id => current_user.customers.collect(&:id)})
# whatever else
# ...
end
And the show page:
def show
# .....
# after the find logic
redirect_to :index unless current_user.customers.collect(&:id).include? #car.customer_id
# whatever else
# ...
end
This approach is ok for most of the cases, however a better approach for performance is to add a user_id column to the costumers table, this is called denormalization but it's acceptable for performance wise.

Resources