I'm building a system that has some records in tables that are template records, which are viewable by all accounts and can be later copied to create live records for an individual account.
The reasoning behind this design decision is that template records and live records share 95%+ of the same code, so I didn't want to create a separate table to track mostly the same fields.
For instance, I have a workflows table:
id:integer
account_id:integer
name:string (required)
is_a_template:boolean (default: false)
is_in_template_library:boolean (default: false)
In this table, I have some records that are templates. When I go to create a new live record, I can use a template record:
# workflows_controller.rb (pseudo-code, not fully tested)
def create
#workflow_template = Workflow.where(is_a_template: true).find_by(id: params[:workflow_template_id])
#workflow = current_account.workflows.new(workflow_params.merge(#workflow_template.dup))
if #workflow.save
...
else
...
end
end
As I build more functionality, I find that I really need 2 different models that operate differently on the table. There are several more differences, but those listed below are enough to show the differences:
class Workflow < ApplicationRecord
default_scope -> { where(is_a_template: false) }
belongs_to :account
validates :account, presence: true
validates :name, presence: true
end
class WorkflowTemplate < ApplicationRecord
default_scope -> { where(is_a_template: true) }
validates :name, presence: true
end
class WorkflowLibraryTemplate < ApplicationRecord
default_scope -> { where(is_a_template: true, is_in_template_library: true) }
validates :name, presence: true
end
As you can see, the workflows table has 3 different "types" of records:
"live" workflows that belong to an account
template workflows that also belong to an account and are copied to create "live" workflows
library template workflows that do NOT belong to an account and can be viewed by any account, so they can copy them into their own list of templates
Question
What I'm trying to figure out, is at what point do I break up this single table into multiple tables, versus keeping the same table and having multiple models, or what solution is there to a problem like this?
The frustrating part is that there are 5+ other tables that are "children" associations of the workflows table. So if I decide that I need separate tables for each, I would end up going from 6 tables to something like 18, and everytime I add a field, I have to do it to all 3 "versions" of the table.
Thus I'm very reluctant to go down the multiple tables route.
If I keep a single table and multiple models, I then end up with different version of data in the table, which isn't the end of the world. I only interact with the data through my application (or a future API I control).
Another solution I'm thinking about is adding a role:string field to the table, which operates very much like the type field in Rails. I didn't want to use STI, however, because there are too many baked-in requirements with Rails that I don't want to conflict with.
What I'm envisioning is:
class Workflow < ApplicationRecord
scope :templates, -> { where(role: "template") }
scope :library_templates, -> { where(role: "library_template") }
validates :account, presence: true, if: :account_required?
validates :name, presence: true
# If record.role matches one of these, account is required
def account_required
["live", "template"].include?(role.to_s.downcase)
end
end
This seems to address several of the issues, keeps me with 1 table and 1 model, but begins to have conditional logic in the model, which seems like a bad idea to me as well.
Is there a cleaner way to implement a templating system within a table?
So what you are looking at here is called Single Table Inheritance. The models are called polymorphic.
As far as when to break up the STI into distinct tables, the answer is: when you have enough divergence that you start having specialized columns. The problem with STI is that let's say WorkFlows and WorkFlowTemplate start to diverge. Maybe the template starts getting a lot of extra attributes as columns that do not correspond to plain old workflows. Now you have lots of data that is empty for one class (or not needed) and useful and necessary for the other. In this case, I'd probably break the tables apart. The real question you should ask is:
How far will these models diverge from each other in terms of requirements?
How soon will this happen?
If it happens very late in the life of my app:
Will it be difficult/impossible to migrate these tables due to how many rows/how much data I have?
Edit:
Is there a cleaner way? In this specific case, I don't think so given a template and a copy of that template, are likely to be tightly coupled to each other.
The approach I took is decomposition by responsibility.
Decomposition by responsibility:
Right now, you have 3 different sources of data and 2 different ways to create/validate a workflow.
In order to achieve that, you can introduce the concept of Repositories and FormObjects.
Repositories are wrapper objects that will abstract the way you query your model. It doesn't care if it is the same table or multiple. It just knows how to get the data.
For example:
class Workflow < ApplicationRecord
belongs_to :account
end
class WorkflowRepository
def self.all
Workflow.where(is_a_template: false)
end
end
class WorkflowTemplateRepository
def self.all
Workflow.where(is_a_template: true)
end
end
class WorkflowLibraryTemplateRepository
def self.all
Workflow.where(is_a_template: true, is_in_template_library: true)
end
end
This makes sure that no matter what you decide in the future to do, you will not change other parts of the code.
So now let's discuss FormObject
FormObject will abstract the way you validate and build your objects. It might not be a great addition right now but usually, pays off in the long run.
For example
class WorkFlowForm
include ActiveModel::Model
attr_accessor(
:name,
:another_attribute,
:extra_attribute,
:account
)
validates :account, presence: true
validates :name, presence: true
def create
if valid?
account.workflows.create(
name: name, is_a_template: false,
is_in_template_library: false, extra_attribute: extra_attribute)
end
end
end
class WorkflowTemplateForm
include ActiveModel::Model
attr_accessor(
:name,
:another_attribute,
:extra_attribute
)
validates :name, presence: true
def create
if valid?
Workflow.create(
name: name, is_a_template: true,
is_in_template_library: false, extra_attribute: extra_attribute)
end
end
end
class WorkflowLibraryTemplateForm
include ActiveModel::Model
attr_accessor(
:name,
:another_attribute,
:extra_attribute
)
validates :name, presence: true
def create
if valid?
Workflow.create(
name: name, is_a_template: true,
is_in_template_library: true, extra_attribute: extra_attribute)
end
end
end
This approach helps with extendability as everything is a separate object.
The only drawback of that is that In my humble opinion, WorkflowTemplate and WorkflowLibraryTemplate are semantical the same thing with an extra boolean but that's an optional thing you can take or leave.
Related
I'm trying to build a reservation system where a customer can reserve a minibus. I've been able to get the all the data so a booking can be made.
I'm trying to avoid another user to reserve the minibus for the same day. I'm not to sure how to go about it as in new to ruby on rails.
In my reservation.rb I've got
belongs_to :vehicle
belongs_to :user
In my user.rb and vehicle.rb I've got
has_many :reservation
In my reservation controller I've got
def new
#vehicle = Vehicle.find(params[:vehicle_id])
#reservation = Reservation.new(user_id: User.find(session[:user_id]).id)
#reservation.vehicle_id = #vehicle.id
end
would I use validation to stop double reservations?
would it be something like in my reservation.rb
validates :vehicle_id, :startDate, :uniqueness => { :message => " minibus already reserved"}
Although the above will only allow the vehicle to be reserved.
Any help will be much appreciated!
As you already figured out you cannot use Rails' built-in uniqueness validator to validate that two ranges do not overlap.
You will have to build a custom validation to check this. A condition that checks if two time or date ranges A and B overlap is quite simple. Have a look at this image.
A: |-----|
B1: |-----|
B2: |---------|
B3: |-----|
C1: |-|
C2: |-|
A and B overlap if B.start < A.end && B.end > A.start
Add the following to your model:
# app/models/reservation.rb
validate :reservations_must_not_overlap
private
def reservations_must_not_overlap
return if self
.class
.where.not(id: id)
.where(vehicle_id: vehicle_id)
.where('start_date < ? AND end_date > ?', end_date, start_date)
.none?
errors.add(:base, 'Overlapping reservation exists')
end
Some notes:
You might need to adjust the naming of the database columns and the attributes names because I wasn't sure if it was just a typo or if you use names not following Ruby conventions.
Furthermore, you might need <= and >= (instead of < and >), depending on your definition of start and end.
Moving the condition into a named scope is a good idea and will improve readability
You're gonna want to use the uniqueness validator lie you're already doing but use the scope option.
The example they give on that page is pretty similar to your use case:
class Holiday < ApplicationRecord
validates :name, uniqueness: { scope: :year,
message: "should happen once per year" }
end
As to which column you should validate, it doesn't really matter. Since the uniqueness scope is going to be all three columns, it can be any of them:
validates :vehicle_id, uniqueness, { scope: [:startDate, user_id], message: "your message" }
You should also add indexes to the database as described here (this question is very similar to yours by the way).
I have MyModel which has attribute category. There are only two possible categories Category 1 and Category 2. The attributes are assigned using a form as follows:
<%= f.input :category, collection: category_options %>
What is considered "good practice" in Rails. Should I save the attributes as a string in the db, or should I create a new table / reference for the collection?
Storing the category as a string has the benefit that it keeps the db clean, but will have to store the collections seperately in the controller. Also, since I'm using i18n, I would expect that storing the category as a string will lead to translation issues.
If according to business logic you are planning to have some attributes, methods or other things for each of your categories, you should create another model for Category. It would be less hard to implement your future needs in terms of managing complexity. If you are 100% sure that there will be no more extras about categories, you should make it an attribute with addition of some validations in your code(inclusion in category1, category2, for example).
For simple cases, I usually use something like this. You can i18n the UI choices, using the internal string as the key.
# View Code:
# <%= form.select :role, MyModel.categories, prompt: '' -%>
class MyModel < ActiveRecord::Base
validates :category,
presence: true,
inclusion: {
in: :categories,
allow_blank: true }
class << self
def categories
%w[hot warm cold]
end
end
def categories
self.class.categories
end
end
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.
I have a Rails 3.2.18 app where I'm trying to do some conditional validation on a model.
In the call model there are two fields :location_id (which is an association to a list of pre-defined locations) and :location_other (which is a text field where someone could type in a string or in this case an address).
What I want to be able to do is use validations when creating a call to where either the :location_id or :location_other is validated to be present.
I've read through the Rails validations guide and am a little confused. Was hoping someone could shed some light on how to do this easily with a conditional.
I believe this is what you're looking for:
class Call < ActiveRecord::Base
validate :location_id_or_other
def location_id_or_other
if location_id.blank? && location_other.blank?
errors.add(:location_other, 'needs to be present if location_id is not present')
end
end
end
location_id_or_other is a custom validation method that checks if location_id and location_other are blank. If they both are, then it adds a validation error. If the presence of location_id and location_other is an exclusive or, i.e. only one of the two can be present, not either, and not both, then you can make the following change to the if block in the method.
if location_id.blank? == location_other.blank?
errors.add(:location_other, "must be present if location_id isn't, but can't be present if location_id is")
end
Alternate Solution
class Call < ActiveRecord::Base
validates :location_id, presence: true, unless: :location_other
validates :location_other, presence: true, unless: :location_id
end
This solution (only) works if the presence of location_id and location_other is an exclusive or.
Check out the Rails Validation Guide for more information.
A Course has many Lessons, and they are chosen by the user with a JS drag-n-drop widget which is working fine.
Here's the relevant part of the params when I choose two lessons:
Parameters: {
"course_lessons_attributes"=>[
{"lesson_id"=>"43", "episode"=>"1"},
{"lesson_id"=>"44", "episode"=>"2"}
]
}
I want to perform some validations on the #course and it's new set of lessons, including how many there are, the sum of the lessons' prices and other stuff. Here's a sample:
Course Model
validate :contains_lessons
def contains_lessons
errors[:course] << 'must have at least one lesson' unless lessons.any?
end
My problem is that the associations between the course and the lessons are not yet built before the course is saved, and that's when I want to call upon them for my validations (using course.lessons).
What's the correct way to be performing custom validations that rely on associations?
Thanks.
looks like you don't need a custom validation here, consider using this one:
validates :lessons, :presence => true
or
validates :lessons, :presence => {:on => :create}
You can't access the course.lessons, but the course_lessons are there, so I ended up doing something like this in the validation method to get access to the array of lessons.
def custom validation
val_lessons = Lesson.find(course_lessons.map(&:lesson_id))
# ...
# check some things about the associated lessons and add errors
# ...
end
I'm still open to there being a better way to do this.