Rails Validations with Redis - ruby-on-rails

I have a bunch of Redis getters and setters like
def share_points= p
$redis.set("store:#{self.id}:points:share", p) if valid?
end
The thing is, ActiveRecord's validation doesn't stop the values from being inserted into redis. How do I go about doing this without adding if valid? on every setter? valid? calculates the validation every time it is called.

What about switching to an after_save callback approach, where you store all the fields that have been changed and just persist them all at once to redis.
Something like:
class Foo < ActiveRecord::Base
after_save :persist_to_redis
attr_accessor :redis_attributes
def share_points=(p)
#redis_attributes ||= {}
#redis_attributes[:share_points] = p
end
def something_else=(p)
#redis_attributes ||= {}
#redis_attributes[:something_else] = p
end
private
def redis_store_share_points(value)
$redis.set("store:#{self.id}:key", value)
end
def redis_store_something_else(value)
$redis.set("something_else:#{self.id}", value)
end
def persist_to_redis
$redis.multi do
#redis_attributes.each_pair do |key, value|
send("redis_store_#{key}".to_sym, value)
end
end
end
end
I think even this could be refactored and cleaned up but you get the idea.

If the model you're editing is derived from active_record, then you probably want to have a specific, wrapped call to redis that does the validation for you. e.g.
class Foo < ActiveRecord::Base
def rset(key,value)
$redis.set("store:#{self.id}:key", value) if valid?
end
def share_points=(p)
rset("points:share", p)
end
end
You could also put that in a module and include it.
If you're not deriving from AR:Base, you might want to come up with a more AR::Base-like structure using ActiveModel as described here: http://purevirtual.de/2010/04/url-shortener-with-redis-and-rails3/

Related

Ruby how to return instance evaled array

I have method in my rails model which returns anonymous class:
def today_earnings
Class.new do
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.instance_eval do
def to_f
reduce(:+)
end
end
end
def used
all.map(&:amount_used).instance_eval do
def to_f
reduce(:+)
end
end
end
end.new(self)
end
I want to achieve possibility to chain result in that way user.today_earning.unused.to_f and I have some problems with instance eval because when I call to_f on result it's undefined method, I guess it is due to ruby copying returned value so the instance gets changed, is it true? And if I'm correct how can I change the code to make it work. Also I'm wondering if making new model can be better solution than anomyous class thus I need advice if anonymous class is elegant in that case and if so how can I add to_f method to returned values
Yes, Anonymous class makes the code much complex. I would suggest a seperate class. It will solve 2 problems here.
defining some anonymous class again and again when we call the today_earnings method.
Readability of the code.
Now coming to actual question, you can try something similar to hash_with_indifferent_access. The code looks as follows.
class NumericArray < Array
def to_f
reduce(:+)
end
end
Array.class_eval do
def with_numeric_operations
NumericArray.new(self)
end
end
Usage will be:
Class Earnings
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.with_numeric_operations
end
def used
all.map(&:amount_used).with_numeric_operations
end
end
This looks like a "clever" but ridiculously over-complicated way to do something that can be simply and efficiently done in the database.
User.joins(:store_credits)
.select(
'users.*',
'SUM(store_credits.amount_used) AS amount_used',
'SUM(store_credits.amount) - amount_used AS unused',
)
.where(store_credits: { created_at: Time.current.beginning_of_day..Time.current.end_of_day })
.group(:id)

Safest way to override the update method of a model

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

How to add errors before updating attributes?

I'm trying to handle the situation where the user has entered info incorrectly, so I have a path that follows roughly:
class Thing < AR
before_validation :byebug_hook
def byebug_hook
byebug
end
end
thing = Thing.find x
thing.errors.add(:foo, "bad foo")
# Check byebug here, and errors added
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
byebug for byebug_hook> errors.messages #=> {}
Originally I thought that maybe the model was running its own validations and overwriting the ones I added, but as you can see even when I add the before hook the errors are missing, and I'm not sure what's causing it
ACTUAL SOLUTION
So, #SteveTurczyn was right that the errors needed to happen in a certain place, in this case a service object called in my controller
The change I made was
class Thing < AR
validate :includes_builder_added_errors
def builder_added_errors
#builder_added_errors ||= Hash.new { |hash, key| hash[key] = [] }
end
def includes_builder_added_errors
builder_added_errors.each {|k, v| errors.set(k, v) }
end
end
and in the builder object
thing = Thing.find x
# to my thinking this mirrors the `errors.add` syntax better
thing.builder_added_errors[:foo].push("bad foo") if unshown_code_does_stuff?
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
update_attributes will validate the model... this includes clearing all existing errors and then running any before_validation callbacks. Which is why there are never any errors at the pont of before_validation
If you want to add an error condition to the "normal" validation errors you would be better served to do it as a custom validation method in the model.
class Thing < ActiveRecord::Base
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo")
end
end
If you want some validations to occur only in certain controllers or conditions, you can do that by setting an attr_accessor value on the model, and setting a value before you run validations directly (:valid?) or indirectly (:update, :save).
class Thing < ActiveRecord::Base
attr_accessor :check_foo
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo") if check_foo
end
end
In the controller...
thing = Thing.find x
thing.check_foo = true
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end

Recommended practice for passing current user to model

Given a model Orderstatus with attributes private_status:string, and private_status_history:json(I'm using Postgresql's json). I would like to record each status transition, together with the user who made the change.
Ideally it would be something like:
class Orderstatus < ActiveRecord::Base
after_save :track_changes
def track_changes
changes = self.changes
if self.private_status_changed?
self.private_status_history_will_change!
self.private_status_history.append({
type: changes[:private_status],
user: current_user.id
})
end
end
end
class OrderstatusController <ApplicationController
def update
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
#Desired behaviour (process not run with console)
status = Orderstatus.new(private_status:'one')
status.private_status #=> 'one'
status.private_status_history #=> []
status.update_attributes({:private_status=>'two'}) #=>true
status.private_status #=> 'two'
status.private_status_history #=> [{type:['one','two'],user:32]
What would be the recommended practice to achieve this? Apart from the usual one using Thread. Or maybe, any suggestion to refactor the structure of the app?
So, I finally settled for this option ( I hope it's not alarming to anyone :S)
class Orderstatus < ActiveRecord::Base
after_save :track_changes
attr_accessor :modifying_user
def track_changes
changes = self.changes
if self.private_status_changed?
newchange = {type:changes[:private_status],user: modifying_user.id}
self.update_column(:private_status_history,
self.private_status_history.append(newchange))
end
end
end
class OrderstatusController <ApplicationController
def update
#status.modifying_user = current_user # <---- HERE!
if #status.update_attributes(white_params)
# Good response
else
# Bad response
end
end
end
Notes:
- I pass the from the Controller to the Model through an instance attribute modifying_user of the class Orderstatus. That attribute is ofc not saved to the db.
- Change of method to append new changes to the history field. I.e. attr_will_change! + save to update_column + append

Refactor this Ruby and Rails code

I have this model:
class Event < Registration
serialize :fields, Hash
Activities=['Annonce', 'Butiksaktivitet', 'Salgskonkurrence']
CUSTOM_FIELDS=[:activity, :description, :date_from, :date_to, :budget_pieces, :budget_amount, :actual_pieces, :actual_amount]
attr_accessor *CUSTOM_FIELDS
before_save :gather_fields
after_find :distribute_fields
private
def gather_fields
self.fields={}
CUSTOM_FIELDS.each do |cf|
self.fields[cf]=eval("self.#{cf.to_s}")
end
end
def distribute_fields
unless self.fields.nil?
self.fields.each do |k,v|
eval("self.#{k.to_s}=v")
end
end
end
end
I have a feeling that this can be done shorter and more elegant. Does anyone have an idea?
Jacob
BTW. Can anyone tell me what the asterisk in front of CUSTOM_FIELDS does? I know what it does in a method definition (def foo(*args)) but not here...
Alright first off: never 10000000000.times { puts "ever" } use eval when you don't know what you're doing. It is the nuclear bomb of the Ruby world in the way that it can wreak devestation across a wide area, causing similar symptoms to radiation poisoning throughout your code. Just don't.
With that in mind:
class Event < Registration
serialize :fields, Hash
Activities = ['Annonce', 'Butiksaktivitet', 'Salgskonkurrence']
CUSTOM_FIELDS = [:activity,
:description,
:date_from,
:date_to,
:budget_pieces,
:budget_amount,
:actual_pieces,
:actual_amount] #1
attr_accessor *CUSTOM_FIELDS #2
before_save :gather_fields
after_find :distribute_fields
private
def gather_fields
CUSTOM_FIELDS.each do |cf|
self.fields[cf] = send(cf) #3
end
end
def distribute_fields
unless self.fields.empty?
self.fields.each do |k,v|
send("#{k.to_s}=", v) #3
end
end
end
end
Now for some notes:
By putting each custom field on its own line, you increase code readability. I don't want to have to scroll to the end of the line to read all the possible custom fields or to add my own.
The *CUSTOM_FIELDS passed into attr_accessor uses what is referred to as the "splat operator". By calling it in this way, the elements of the CUSTOM_FIELDS array will be passed as individual arguments to the attr_accessor method rather than as one (the array itself)
Finally, we use the send method to call methods we don't know the names of during programming, rather than the evil eval.
Other than that, I cannot find anything else to refactor about this code.
I agree with previous posters. In addition I would probably move the gather_fields and distribute_fields methods to the parent model to avoid having to repeat the code in every child model.
class Registration < ActiveRecord::Base
...
protected
def gather_fields(custom_fields)
custom_fields.each do |cf|
self.fields[cf] = send(cf)
end
end
def distribute_fields
unless self.fields.empty?
self.fields.each do |k,v|
send("#{k.to_s}=", v)
end
end
end
end
class Event < Registration
...
before_save :gather_fields
after_find :distribute_fields
private
def gather_fields(custom_fields = CUSTOM_FIELDS)
super
end
end
You can replace the two evals with send calls:
self.fields[cf] = self.send(cf.to_s)
self.send("#{k}=", v)
"#{}" does a to_s, so you don't need k.to_s
Activities, being a constant, should probably be ACTIVITIES.
For that asterisk *, check out this post: What is the splat/unary/asterisk operator useful for?
Activities=['Annonce', 'Butiksaktivitet', 'Salgskonkurrence']
can be written: ACTIVITIES = %w(Annonce, Butiksaktivitet, Salgskonkurrence).freeze since you are defining a constant.
def distribute_fields
unless self.fields.empty?
self.fields.each do |k,v|
send("#{k.to_s}=", v) #3
end
end
end
can be written as a one liner:
def distribute_fields
self.fields.each { |k,v| send("#{k.to_s}=", v) } unless self.fields.empty?
end
Ryan Bigg, gave a good answer.

Resources