Rails validations and initializations with interdependent fields - ruby-on-rails

After coding in Rails for a couple of years, I still don't understand the Rails Way for more advanced concepts. The Rails Way is so specific about convention over configuration, however when you get into Enterprise coding, all rules go out the window and everyone writes blogs with non-standard way of doing things. Here's another one of those situations: contextual validations where the context is a little more complex (fields depend on each other). Basically in a shipping application, I need to initialize an AR object with request params and with some calculated values. The calculated values are dependent on the request params, and I'm not sure how to initialize and validate my member variables.
table mailpieces
mail_class
weight
sort_code
t_indicator_id
end
class Mailpiece
validates_presence_of: :mail_class
validates_presence_of: :weight
validates_presence_of: :sort_code
validates_presence_of: :t_indicator_id
def some_kind_of_initializer
if mail_class == 'Priority'
sort_code = '123'
elsif mail_class == 'Express'
if weight < 1
sort_code = '456'
else
sort_code = '789'
end
end
t_indicator = ndicator.find_by(name: 'blah')
if sort_code = '456'
t_indicator = Indicator.find_by(name: 'foobar')
end
end
end
mailpiece = Mailpiece.new(
mail_class: params[:mail_class],
weight: params[:weight])
#mailpiece.some_kind_of_initializer ?!
raise 'SomeError' if !mailpiece.valid?
What should some_kind_of_initializer be?
Override of ActiveRecord initialize? That's not good practice.
after_initialize. More Rails Way-sy.
Custom method called after
Mailpiece.new (e.g. mailpiece.some_kind_of_initializer)
Whichever of the above choices, the problem is that the initialization of sort_code and t_indicator depends on mail_class and weight being valid. Given that mail_class and weight should be not null before I enter some_kind_of_initializer, how should I write my validations?
Extract all validations into a json schema validation. More complex business rules around mail_class and weight are difficult to write in a json schema.
Extract all validations into some type of Data Transfer Object validation class. Moves away from the Rails Way of doing things. Feels like I'm writing in .NET/Java and I'm afraid that Rails will kick my azz later (in validations, testing, etc.).
Assign sort_code only if mail_class and weight have been initialized. This seems to be most Rails Way to write things, but it's tough. So many if/else. This is just a simple example, but my mailpiece has references that have references and they all do these type of validations. If this is the right answer, then I'm getting a gut feeling that it might be easier to move ALL validations and ALL initializations to an external class/module - perhaps getting close to option #2.
Option 3 code rewrite
def some_kind_of_initializer
if mail_class && weight
if (mail_class == 'Priority')
sort_code = '123'
elsif (mail_class == 'Express')
if weight < 1
sort_code = '456'
else
sort_code = '789'
end
end
end
if sort_code
t_indicator = Indicator.find_by(name: 'blah')
if sort_code = '456'
t_indicator = Indicator.find_by(name: 'foobar')
end
end
end
I'd love to hear your opinion on this. It seems to me this is such a popular use case of AR and I'm not sure what to do. Again, this is a just a simple case. My Mailpiece model has many other references that have dependencies on the mailpiece object properties and their own interdependencies in the same style as above.

I'm not sure if it helps or I understood your question correctly, but maybe using "case" instead of if/elsif/else, the ternary operator, some lazy assignment and some extra validations can give your code a better "rails" feeling.
With lazy assignment I mean that you could wait to initialize sort_code and t_indicator until you actually need to use it or save it.
def sort_code
return self[:sort_code] if self[:sort_code].present?
sort_code = case mail_class
when 'Priority' then '123'
when 'Express'
weight < 1 ? '456' : '789' if weight
end
self[:sort_code] = sort_code
end
That way, sort_code gets initialized right before you need to use it the first time so you can do Mailpiece.new(mail_class: 'Something') and forget about initializing sort_code right away.
The problem would come when you need to save the object and you never called mailpiece.sort_code. You could have a before_validation callback that justs calls sort_code to initialize it in case it wasn't already.
The same can be done for t_indicator.
I would also add some context in your validations
validates_presence_of :weight, if: Proc.new{|record| 'Express' == record.mail_class} #you could add some helper method "is_express?" so you can just do "if: :is_express?"
validates_presence_of :sort_code, if: Proc.new{|record| 'Priority' == record.mail_class or 'Express' == record.mail_class && record.weight.present?}
Sorry if I missunderstood your question, I didn't even use your options haha.

How about:
class Mailpiece
validates_presence_of: :mail_class
validates_presence_of: :weight
validates_presence_of: :sort_code
validates_presence_of: :t_indicator_id
def default_mail_class
'Priority'
end
def default_sort_code
mail_class = mail_class || default_mail_class
if mail_class == 'Priority'
'123'
elsif mail_class == 'Express'
weight < 1 ? '456' : '789'
end
end
end
Then, when you needed to figure out t_indicator, just do it as-needed:
def is_foobar_indicator?
sort_code || default_sort_code == '456'
end
def t_indicator
indicator_params = {name: is_foobar_indicator? ? 'foobar' : 'blah'}
Indicator.find_by(indicator_params)
end
So you can still have the validation on sort_code (assuming that is user provided) but still use defaults when looking at t_indicator. I don't know how complicated the rest of your model is, but I would suggest not looking up values until you need them. Also, after_initialize is risky because it runs your code exactly where it claims - after every initialize, so if you run a query for N items but do nothing but find on them, or wind up not using the defaults you're setting, after_initialize runs N times for nothing.

Related

Getting "original" object during a before_add callback in ActiveRecord (Rails 7)

I'm in the process of updating a project to use Ruby 3 and Rails 7. I'm running into a problem with some code that was working before, but isn't now. Here's (I think) the relevant parts of the code.
class Dataset < ActiveRecord::Base
has_and_belongs_to_many :tags, :autosave => true,
:before_add => ->(owner, change){ owner.send(:on_flag_changes, :before_add, change) }
before_save :summarize_changes
def on_flag_changes(method, tag)
before = tags.map(&:id)
after = before + [tag.id]
record_change('tags', before, after)
end
def record_change(field, before_val, after_val)
reset_changes
before_val = #change_hash[field][0] if #change_hash[field]
if before_val.class_method_defined? :sort
before_val = before_val.sort unless before_val.blank?
after_val = after_val.sort unless after_val.blank?
end
#change_hash[field] = [before_val, after_val]
end
reset_changes
if #change_hash.nil?
#change_notes = {}
#change_hash = {
tags: [tags.map(&:id), :undefined]
}
end
end
def has_changes_to_save?
super || !change_hash.reject { |_, v| v[1] == :undefined }.blank?
end
def changes_to_save
super.merge(change_hash.reject { |_, v| v[0] == v[1] || v[1] == :undefined })
end
def summarize_changes
critical_fields = %w[ tags ]
#change_notes = changes_to_save.keep_if { |key, _value| critical_fields.include? key } if has_changes_to_save?
self.critical_change = true unless #change_notes.blank?
end
There are more fields for this class, and some attr_accessors but the reason I'm doing it this way is because the tags list can change, which may not necessarily trigger a change in the default "changes_to_save" list. This will allow us to track if the tags have changed, and set the "critical_change" flag (also part of Dataset) if they do.
In previous Rails instances, this worked fine. But since the upgrade, it's failing. What I'm finding is that the owner passed into the :before_add callback is NOT the same object as the one being passed into the before_save callback. This means that in the summarize_changes method, it's not seeing the changes to the #change_hash, so it's never setting the critical_change flag like it should.
I'm not sure what changed between Rails 6 and 7 to cause this, but I'm trying to find a way to get this to work properly; IE, if something says dataset.tags = [tag1, tag2], when tag1 was previously the only association, then dataset.save should result in the critical_change flag being set.
I hope that makes sense. I'm hoping this is something that is an easy fix, but so far my looking through the Rails 7 documentations has not given me the information I need. (it may go without saying that #change_notes and #change_hash are NOT persisted in the database; they are there just to track changes prior to saving to know if the critical_change flag should be set.
Thanks!
Turns out in my case there was some weird caching going on; I'd forgotten to mention an "after_initialize" callback that was calling the reset method, but for some reason at the time it makes this call, it wasn't the same object as actually got loaded, but some association caching was going on with tags (it was loading the tags association with the "initialized" record, and it was being cached with the "final" record, so it was confusing some of the code).
Removing the tags bit from the reset method, and having it initialize the tag state the first time it tries to modify tags solved the problem. Not particularly fond of the solution, but it works, and that's what I needed for now.

What is the best way to reuse a scope in Rails?

I'm confused to reuse or writing a new scope.
for example,
one of my methods will return future subscription or current subscription or sidekiq created subscriptions.
as scopes will look like:
scope :current_subscription, lambda {
where('(? between from_date and to_date) and (? between from_time and to_time)', Time.now, Time.now)
}
scope :sidekiq_created_subscription, lambda {
where.not(id: current_subscription).where("(meta->'special_sub_enqueued_at') is not null")
}
scope :future_subscription, lambda {
where.not(id: current_subscription).where("(meta->'special_sub_enqueued_at') is null")
}
so these were used for separate purposes in different methods, so for me what I tried is to check whether a particular account record will come under which of three subscriptions.
so I tried like:
def find_account_status
accounts = User.accounts
name = 'future' if accounts.future_subscription.where(id: #account.id).any?
name = 'ongoing' if accounts.current_subscription.where(id: #account.id).any?
name = 'sidekiq' if accounts.sidekiq_enqued_subscription.where(id: #account.id).any?
return name
end
so here what my doubt is, whether using like this is a good way, as here we will be fetching the records based on the particular subscriptions and then we are checking whether ours is there or not.
can anyone suggest any better way to achieve this?
Firstly, you are over using the scopes here.
The method #find_account_status will execute around 4 Queries as below:
Q1 => accounts = User.accounts
Q2 => accounts.future_subscription
Q3 => accounts.current_subscription
Q4 => accounts.sidekiq_enqued_subscription
Your functionality can be achived by simply using the #account object which is already present in memory as below:
Add below instance methods in the model:
def current_subscription?
# Here I think just from_time and to_time will do the work
# but I've added from_date and to_date as well based on the logic in the question
Time.now.between?(from_date, to_date) && Time.now.between?(from_time, to_time)
end
def future_subscription?
!current_subscription? && meta["special_sub_enqueued_at"].blank?
end
def sidekiq_future_subscription?
!current_subscription? && meta["special_sub_enqueued_at"].present?
end
#find_account_status can be refactored as below:
def find_account_status
if #account.current_subscription?
'ongoing'
elsif #account.future_subscription?
'future'
elsif #account.sidekiq_future_subscription?
'sidekiq'
end
end
Additionally, as far as I've understood the code, I think you should also handle a case wherein the from_date and to_date are past dates because if that is not handled, the status can be set based on the field meta["special_sub_enqueued_at"] which can provide incorrect status.
e.g. Let's say that the from_date in the account is set as 31st Dec 2021 and meta["special_sub_enqueued_at"] is false or nil.
In this case, #current_subscription? will return false but #future_subscription? will return true which is incorrect, and hence the case for past dates should be handled.

Access to old association inside Rails/ActiveRecord callback

In my Rails callback I have something like this:
private
def update_points
if user_id_changed?
user_was = User.find(user_id_was)
user_was.points -= points_was
user_was.save
user.points += points
user.save
end
end
Is that the proper way to do this with user_was? I initially just assumed user_was was already defined (or could be defined on the spot) because user_id_was existed.
It's not clear to me from the context what exactly you're doing (maybe the 2nd points is supposed to be points_was?). For mildly improved clarity, depending on who you ask, and with fewer lines:
...
user_was = User.find(user_id_was)
user_was.update_column :points, user_was.points - points_was
user.update_column :points, user.points + points
...

How to test the number of database calls in Rails

I am creating a REST API in rails. I'm using RSpec. I'd like to minimize the number of database calls, so I would like to add an automatic test that verifies the number of database calls being executed as part of a certain action.
Is there a simple way to add that to my test?
What I'm looking for is some way to monitor/record the calls that are being made to the database as a result of a single API call.
If this can't be done with RSpec but can be done with some other testing tool, that's also great.
The easiest thing in Rails 3 is probably to hook into the notifications api.
This subscriber
class SqlCounter< ActiveSupport::LogSubscriber
def self.count= value
Thread.current['query_count'] = value
end
def self.count
Thread.current['query_count'] || 0
end
def self.reset_count
result, self.count = self.count, 0
result
end
def sql(event)
self.class.count += 1
puts "logged #{event.payload[:sql]}"
end
end
SqlCounter.attach_to :active_record
will print every executed sql statement to the console and count them. You could then write specs such as
expect do
# do stuff
end.to change(SqlCounter, :count).by(2)
You'll probably want to filter out some statements, such as ones starting/committing transactions or the ones active record emits to determine the structures of tables.
You may be interested in using explain. But that won't be automatic. You will need to analyse each action manually. But maybe that is a good thing, since the important thing is not the number of db calls, but their nature. For example: Are they using indexes?
Check this:
http://weblog.rubyonrails.org/2011/12/6/what-s-new-in-edge-rails-explain/
Use the db-query-matchers gem.
expect { subject.make_one_query }.to make_database_queries(count: 1)
Fredrick's answer worked great for me, but in my case, I also wanted to know the number of calls for each ActiveRecord class individually. I made some modifications and ended up with this in case it's useful for others.
class SqlCounter< ActiveSupport::LogSubscriber
# Returns the number of database "Loads" for a given ActiveRecord class.
def self.count(clazz)
name = clazz.name + ' Load'
Thread.current['log'] ||= {}
Thread.current['log'][name] || 0
end
# Returns a list of ActiveRecord classes that were counted.
def self.counted_classes
log = Thread.current['log']
loads = log.keys.select {|key| key =~ /Load$/ }
loads.map { |key| Object.const_get(key.split.first) }
end
def self.reset_count
Thread.current['log'] = {}
end
def sql(event)
name = event.payload[:name]
Thread.current['log'] ||= {}
Thread.current['log'][name] ||= 0
Thread.current['log'][name] += 1
end
end
SqlCounter.attach_to :active_record
expect do
# do stuff
end.to change(SqlCounter, :count).by(2)

Clarifying a custom Rails 3.0 Validation with methods

I've created a custom validator in Rails 3.0 which validates whether a combination of columns is unique within a table. The entire code of the validation is:
class UniqueInProjectValidator < ActiveModel::EachValidator
def validate_each(object, attribute, value)
unless object.class.where("project_id = ? AND #{attribute} = ?", object.project_id, value).empty?
if object.new_record?
object.errors[attribute] << (options[:message] || "must be unique in each project")
else
orig_rec = object.class.find(object.id)
if value != orig_rec.method(attribute).call || object.project_id != orig_rec.project_id
object.errors[attribute] << (options[:message] || "must be unique in each project")
end
end
end
end
Note that it is not easy to recognize what the if statements do, so I was hoping to be able to replace the unless conditional with a def attribute_and_project_exist? method and the second if statement with a def attribute_or_project_changed? method. However when creating those methods, the arguments from validates_each do not pass because of encapsulation.
Now the question: Is there a way to somehow cleanly allow those variables to be accessed by my two newly created methods as one can do with column names in a model, or am I stuck with the options of either passing each argument again or leaving the hard to read conditional statements?
Thanks in advance!
I suppose you could clean it up a bit with one variable, one lambda, and one "return as soon as possible":
def validate_each(object, attribute, value)
# If there is no duplication then bail out right away as
# there is nothing to check. This reduces your nesting by
# one level. Using a variable here helps to make your
# intention clear.
attribute_and_project_exists = object.class.where("project_id = ? AND #{attribute} = ?", object.project_id, value).empty?
return unless attribute_and_project_exists
# This lambda wraps up your second chunk of ugly if-ness and saves
# you from computing the result unless you have to.
attribute_or_project_changed = lambda do
orig_rec = object.class.find(object.id)
value != orig_rec.method(attribute).call || object.project_id != orig_rec.project_id
end
# Note that || short-circuits so the lambda will only be
# called if you have an existing record.
if object.new_record? || attribute_or_project_changed.call
object.errors[attribute] << (options[:message] || "must be unique in each project")
end
end
I don't know how much better that is than your original but the logic and control flow is a lot clearer to me due to the nicer chunking.

Resources