Safest way to override the update method of a model - ruby-on-rails

I have the following model:
class TwitterEngagement < ApplicationRecord
end
And I would like to override create (and create!), update (and
update!) methods of it so no one can manually entry fake data. I would like the help of someone more experienced with active record and rails so I don't mess anything up. Right now what I have is:
class TwitterEngagement < ApplicationRecord
belongs_to :page
def create
super(metrics)
end
def update
super(metrics)
end
private
def metrics
client.get_engagements(page.url)
def client
TwitterClient.new
end
end
Thank you.

TL;DR:
class FacebookEngagement < ApplicationRecord
def create_or_update(*args, &block)
super(metrics)
end
Probably depends on your Rails version, but I traced the ActiveRecord::Persistence sometime before in Rails 5, and found out that both create and update eventually calls create_or_update.
Suggestion:
If ever possible, I'll just do a validation, because it kinda makes more sense because you are validating the inputs, and then probably set an optional readonly?, to prevent saving of records. This will also prevent "silent failing" code / behaviour as doing TL;DR above would not throw an exception / populate the validation errors, if say an unsuspecting developer does: facebook_engagement.update(someattr: 'somevalue') as the arguments are gonna basically be ignored because it's instead calling super(metrics), and would then break the principle of least surprise.
So, I'll probably do something like below:
class FacebookEngagement < ApplicationRecord
belongs_to :page
validate :attributes_should_not_be_set_manually
before_save :set_attributes_from_facebook_engagement
# optional
def readonly?
# allows `create`, prevents `update`
persisted?
end
private
def attributes_should_not_be_set_manually
changes.keys.except('page_id').each do |attribute|
errors.add(attribute, 'should not be set manually!')
end
end
def set_attributes_from_facebook_engagement
assign_attributes(metrics)
end
def metrics
# simple memoization to prevent wasteful duplicate requests (or remove if not needed)
#metrics ||= graph.get_object("#{page.url}?fields=engagement")
end
def graph
Koala::Facebook::API.new
end
end

Related

Is this Rails validation thread-safe

This is running in multiple Sidekiq instances and workers at the same time and it seems that is has generated a couple of issues, like instances getting assigned the "It was alerted recently" error when shouldn't and the opposite.
It is rare, but it is happening, is this the problem or maybe it is something else?
class BrokenModel < ActiveRecord::Base
validates_with BrokenValidator
end
class BrokenValidator < ActiveModel::Validator
def validate record
#record = record
check_alerted
end
private
def check_alerted
if AtomicGlobalAlerted.new(#record).valid?
#record.errors[:base] << "It was alerted recently"
end
p "check_alerted: #{#record.errors[:base]}"
end
end
class AtomicGlobalAlerted
include Redis::Objects
attr_accessor :id
def initialize id
#id = id
#fredis = nil
Sidekiq.redis do |redis|
#fredis = FreshRedis.new(redis, freshness: 7.days, granularity: 4.hours)
end
end
def valid?
#fredis.smembers.includes?(#id)
end
end
We were experiencing something similar at work and after A LOT of digging finally figured out what was happening.
The class method validates_with uses one instance of the validator (BrokenValidator) to validate all instances of the class you're trying to validate (BrokenModel). Normally this is fine but you are assigning a variable (#record) and accessing that variable in another method (check_alerted) so other threads are assigning #record while other threads are still trying to check_alerted.
There are two ways you can fix this:
1) Pass record to check_alerted:
class BrokenValidator < ActiveModel::Validator
def validate(record)
check_alerted(record)
end
private
def check_alerted(record)
if AtomicGlobalAlerted.new(record).valid?
record.errors[:base] << "It was alerted recently"
end
p "check_alerted: #{record.errors[:base]}"
end
end
2) Use the instance version of validates_with which makes a new validator instance for each model instance you want to validate:
class BrokenModel < ActiveRecord::Base
validate :instance_validators
def instance_validators
validates_with BrokenValidator
end
end
Either solution should work and solve the concurrency problem. Let me know if you experience any other issues.
I believe there are some thread safety issues in rails but we can overcome them by taking necessary precautions.
The local variables, such as your local var, are local to each particular invocation of the method block. If two threads are calling this block at the same time, then each call will get its own local context variable and those won't overlap unless there are shared resources involved: instance variables like (#global_var), static variables (##static_var), globals ($global_var) can cause concurrency problems.
You are using instance variable, just instantiate it every time you are coming to the validate_record method and hopefully your problem will go away like :
def validate record
#record.errors[:base] = []
#record = record
check_alerted
end
For more details you can visit this detailed link
Or try to study about rails configs here : link

ActiveModel::Dirty watch only specific fields

I'm not 100% sure about why ActiveModel::Dirty has its name. I'm guessing it is because it is considered as dirty to use it.
But in some cases, it is not possible to avoid watching on specific fields.
Ex:
if self.name_changed?
self.slug = self.name.parameterize
end
Without ActiveModel::Dirty, the code would look like:
if old_name != self.name
self.slug = self.name.parameterize
end
which implies having stored old_name before, and it is not readable, so IMHO, it is dirtier than using ActiveModel::Dirty. It becomes even worse if old_number is a number and equals params[:user]['old_number'] as it needs to be correctly formated (parsed as int), whereas ActiveRecord does this automagically.
So I would find clean to define watchable fields at Model level:
class User < ActiveRecord::Base
include ActiveModel::Dirty
watchable_fields :name
before_save :generate_slug, if: name_changed?
def generate_slug
self.slug = self.name.parameterize
end
end
Or (even better?) at controller level, before assigning new values:
def update
#user = current_user
#user.watch_fields(:name)
#user.assign_attributes(params[:user])
#user.generate_slug if #user.name_changed?
#user.save # etc.
end
The good thing here is that it removes the memory overload produced by using ActiveModel::Dirty.
So my question is:
Can I do that using ActiveRecord pre-built tools, or should I write a custom library to this?
Thanks
If ActiveModel::Dirty solves your problem, feel free to use it. The name comes from the term "dirty objects" and is not meant to imply that it's a dirty/hackish module.
See this answer for more details on dirty objects: What is meant by the term "dirty object"?
Here's what I have ended up doing. I like it:
class User < ActiveRecord::Base
attr_accessor :watched
def field_watch(field_name)
self.watched ||= {}
self.watched[field_name] = self.send(field_name)
return self
end
def field_changed?(field_name)
self.send(field_name) != self.watched(field_name)
end
end
And in the controller
def update
#user = current_user.field_watch(:name)
#user.assign_attributes(params[:user])
#user.generate_slug if #user.field_changed?(:name)
#user.save
end
I'll report here if I take the time to wrap this code in a gem or something.

rails callback before 'new' of a model?

I have a model base_table, and I have a extended_table which has extra properties to further extend my base_table. (I would have different extended_tables, to add different properties to my base_table, but that's non-related to the question I'm asking here).
The model definition for my base_table is like:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def self.included(base)
base.belongs_to :base_table, autosave:true, dependent: :destroy
# do something more when this module is included
end
end
end
And the model definition for my extended_table is like:
class TennisQuestionaire < ActiveRecord::Base
include BaseTable::BaseTableInclude
end
Now I what I want is the code below:
params = {base_table: {name:"Songyy",age:19},tennis_ball_num:3}
t = TennisQuestionaire.new(params)
When I created my t, I want the base_table to be instantiated as well.
One fix I can come up with, is to parse the params to create the base_table object, before TennisQuestionaire.new was called upon the params. It's something like having a "before_new" filter here. But I cannot find such kind of filter when I was reading the documentation.
Additionally, I think another way is to override the 'new' method. But this is not so clean.
NOTE: There's one method called accepts_nested_attributes_for, seems to do what I want, but it doesn't work upon a belongs_to relation.
Any suggestions would be appreciated. Thanks :)
After some trails&error, the solution is something like this:
class BaseTable < ActiveRecord::Base
module BaseTableInclude
def initialize(*args,&block)
handle_res = handle_param_args(args) { |params| params[:base_table] = BaseTable.new(params[:base_table]) }
super(*args,&block)
end
private
def handle_param_args(args)
return unless block_given?
if args.length > 0
params = args[0]
if (params.is_a? Hash) and params[:base_table].is_a? Hash
yield params
end
end
end
end
end

Object does not get loaded

This is the weirdest thing ever happened to me with ruby/rails.
I have a model, Store, which has_many Balances. And I have a method that gives me the default balance based on the store's currency.
Store model.
class Store < ActiveRecord::Base
has_many :balances, as: :balanceable, dependent: :destroy
def default_balance
#puts self.inspect <- weird part.
balances.where(currency: self.currency)[0]
end
...
end
Balance model.
class Balance < ActiveRecord::Base
belongs_to :balanceable, :polymorphic => true
...
end
Ok, so then in the Balance controller I have the show action, that will give me a specific balance or the default one.
Balance controller.
class Api::Stores::BalancesController < Api::Stores::BaseController
before_filter :load_store
# Returns a specific alert
# +URL+:: GET /api/stores/:store_id/balances/:id
def show
#puts #store.inspect <- weird part.
#balance = (params[:id] == "default") ? #store.default_balance : Balance.find(params[:id])
respond_with #balance, :api_template => :default
end
...
private
# Provides a shortcut to access the current store
def load_store
#store = Store.find(params[:store_id])
authorize! :manage, #store
end
end
Now here is where the weird part comes...
If I make a call to the show action; for example:
GET /api/stores/148/balances/default
It returns null (because the currency was set as null, and there is no Balance with null currency), and the SQL query generated is:
SELECT `balances`.* FROM `balances` WHERE `balances`.`balanceable_id` = 148 AND `balances`.`balanceable_type` = 'Store' AND `balances`.`currency` IS NULL
So I DON'T know why... it is setting the currency as NULL. BUT if in any where in that process I put
puts #store.inspect
or inside the default_balance method:
puts self.inspect
it magically works!!!.
So I don't know why is that happening?... It seems like the store object is not getting loaded until I "inspect" it or something like that.
Thanks
Sam and Adrien are on the right path.
ActiveRecord overrides method_missing to add a whole bunch of dynamic methods including the accessors for the column-backed attributes like Store#currency. While I'm glossing over a lot, suffice it to say that when the logic is invoked then the dynamic class/instance methods are added to the Store class/instances so that subsequent calls no longer require the method_missing hook.
When YOU overrode method_missing without calling super, you effectively disabled this functionality. Fortunately, this functionality can be invoked by other means, one of which you tripped upon when you called store#inspect.
By adding the call to super, you simply assured that ActiveRecord's dynamic methods are always added to the class when they're needed.
OK finally after a lot of debugging, I found the reason...
In the Store model I have a method_missing method and I had it like this:
def method_missing method_name, *args
if method_name =~ /^(\w+)_togo$/
send($1, *args).where(togo: true)
elsif method_name =~ /^(\w+)_tostay$/
send($1, *args).where(tostay: true)
end
end
So when I was calling self.currency it went first to the method_missing and then returned null. What I was missing here was the super call.
def method_missing method_name, *args
if method_name =~ /^(\w+)_togo$/
send($1, *args).where(togo: true)
elsif method_name =~ /^(\w+)_tostay$/
send($1, *args).where(tostay: true)
else
super
end
end
But I continue wondering why after I had called puts #store.inspect or puts self.inspect it worked well?. I mean, why in that case that super call wasn't needed?

Adding callbacks for model classes in separate file (RoR)

I have a Message model class (which inherits from ActiveRecord::Base). For a particular deployment, I would like to have a separate file which modifies Message by adding a callback. So, instead of doing:
# app/models/message.rb
class Message < ActiveRecord::Base
before_save :foo
def foo
puts 'foo!'
end
end
I would like to be able to do:
# app/models/message.rb
class Message < ActiveRecord::Base
end
# config/initializers/fixes.rb
Message
class Message
before_save :foo
def foo
puts 'foo!'
end
end
Problem is, it works when I start the script/console, but when I start it using script/server it usually doesn't. That is the worst part, it isn't that it never works. Sometimes I start the server and it works, sometimes it doesn't, and that is without making any changes to the source.
I am restarting the server itself as (as far as I know) the initializers are run only once and don't get reloaded if modified.
I know the 'sometimes' works is very vague, but I have spent hours here without any luck. Perhaps someone has had a similar issue, or can come up with a different idea to add the callback.
Why not put those into a module and import it?
class Message < ActiveRecord::Base
include Message::Callbacks
end
In another file you can define whatever you like, such as message/callbacks.rb:
module Message::Callbacks
def self.included(base)
base.class_eval do
before_save :foo
end
end
def foo
# ...
end
end
The downside to this is it's more work to make the methods protected.
Why not use observers? (http://api.rubyonrails.org/classes/ActiveRecord/Observer.html)
For example, you'd do something like this:
class MessageObserver < ActiveRecord::Observer
def before_save(message)
puts 'you win at ruby!'
end
end

Resources