How to assign Rails model column by symbol - ruby-on-rails

I have a method like this:
class Foo < ActiveRecord::Base
def load_data(data)
self.foo = data[:foo] if data.has_key?(:foo)
self.bar = data[:bar] if data.has_key?(:bar)
self.moo = data[:moo] if data.has_key?(:moo)
self.save
end
end
I want to write the method like this:
[:foo, :bar, :moo].each do |sym|
# need some trick here
self.sym = data[sym] if data.has_key?(sym)
end
Of course this method doesn't work, how can I assign a value to a Model column by using a symbol?

vee's answer is correct for the general case, but since this is Rails and ActiveRecord, you can take some nice shortcuts:
def load_data(data)
update_attributes data.slice(:foo, :bar:, :moo)
end
This works because data.slice filters your data hash to just the given keys, and then update_attributes will set those values in your model and invoke #save. When the keys aren't present, they aren't written, so you don't need to check and assign each key separately.
If you don't care about filtering the inbound data and simply assigning the keys given to the model, then just update_attributes data and you're done.

You can use send:
[:foo, :bar, :moo].each do |sym|
# need some trick here
send "#{sym}=", data[sym] if data.has_key?(sym)
end

Related

Is it possible to reliably execute a callback on an ActiveRecord jsonb column assignment or change?

I'm trying to execute some additional functionality in an ActiveRecord model when a jsonb column value is assigned to or when it changes.
My model is simple, it just contains a single jsonb column called payload. My current thinking is to override the column setter, but this doesn't seem to work as expected:
class Entity < ActiveRecord::Base
def payload=(jsonb)
pp 'Setter called...'
super(jsonb)
end
end
With the above model the following works:
ent = Entity.new
ent.payload = {}
#>> Setter called...
ent.save!
but subsequently the following syntax does not:
ent.payload["attribute"] = "value"
#>> ...
Is there any way to trigger code or fire a callback that works for both methods of assignment (inline and explict)?
I am aware that it is possible to hook into the ActiveModel dirty attributes to check if the jsonb value has changed at other points such as before_save, but these are reliant on active model callbacks.
I am specifically looking for a way to react to the change when it occurs, on assignment, not on subsequent events in the model life-cycle.
When you assign attribute / value, you call other method under the hood -- Hash#[]=
So if you decide to change its behavior you need to override like this
class Hash
def []=(key, val)
pp 'Setter called...'
super(key, val)
end
end
But keep in mind that such monkeypatch will affect not only this model, but everywhere when you will assign hash key
According to the comment you can define your own class
class TalkingHash < Hash
def []=(key, val)
pp 'Setter called...'
super(key, val)
end
end
ent = Entity.new
ent.payload = TalkingHash.new
# Setter called...
ent.payload[:nested] = TalkingHash.new
# Setter called...
ent.payload[:nested][:attribute] = :value
# Setter called...

How to perform automatic typecasting for non-ActiveRecord backed classes?

I have an ActiveRecrod model User and a separate class UsersFilter, which is solely used for filtering the model. Say UsersFilter accepts a parameter hash params = {min_age: '18', max_age: '30', admin: 'true'}. All the values are strings. If I pass these values directly to ActiveRecord, the queries will work. However, I also want to be able to use these values in my code, so that I can build some logic around it. So in order to do that, I need to manually type cast these values. So UsersFilter might look like this:
class UsersFilter
include ActiveRecord::AttributeAssignment
attr_accessor :min_age, :max_age, :admin
def initialize(params)
params[:min_age] = params[:min_age].to_i
params[:max_age] = params[:min_age].to_i
params[:admin] = ActiveRecord::Type::Boolean.new.type_cast_from_user(params[:admin])
assign_attributes(params)
end
# some query methods
end
So my question is - is there a rails module I can mix in in order to have this typecasting occur automatically?
P.S. I suppose, I will need to add a mapping between each attribute and its type.
Use custom setters:
class UsersFilter
include ActiveModel::Model
def min_age=(age)
self[:min_age] = age.to_i
end
def max_age=(age)
self[:max_age] = age.to_i
end
def admin(val)
self[:admin] = ActiveRecord::Type::Boolean.new.type_cast_from_user(val)
end
end
If you really need to dry it out to a generic typecasting facility you could use define_method to do it with metaprogramming. But YAGNI.

Looping over attributes of object of a Non-Active-Record-Model

The simple way to loop over all attributes of an object when using Active Record is
order_item_object.attributes.each do |key,value|
....
end
But this doesn't work, when we are not using Active Record. How can I iterate over all the attributes of an object then?
For eg -:
I have a model in Rails which does not use active record.
The object from model order_item can be used in the controller like order_item_object.product_id, order_item_object.quantity, order_item_object.quoted_price. But when I try to order_item_object.attributes.each do |k,v| ...., I get undefined method "attributes" for #<Order:0x00000005aa81b0>
How should I go about this?
try this:
class Parent
def self.attr_accessor(*vars)
#attributes ||= []
#attributes.concat vars
super(*vars)
end
def self.attributes
#attributes
end
def attributes
self.class.attributes
end
end
class ChildClass < Parent
attr_accessor :id, :title, :body
end
p ChildClass.new.attributes.inspect #=> [:id, :title, :body]
attr_accessor is simply a macro that creates some methods to set an instance variable. SO perhaps what you want is the instance_variables method, which returns an array of instance variables that you can iterate through
class Foo
attr_accessor :bar
attr_accessor :baz
end
foo = Foo.new
foo.bar = 123
foo.baz
foo.instance_variables.each do |ivar_name|
ivar_value = foo.instance_variable_get ivar_name
# do something with ivar_name and ivar_value
end
But I wouldn't really recommend this. ActiveRecord keeps model data separate for a reason. Your instance may have lots of uses for instance variables. For instance, perhaps you want to track if the record is saved yet or not. This may be kept in #saved variable, which reflects the state of the instance but not the data of the model.
Perhaps you want to keep a hash of attributes instead?
This is the best way I found
item=self.instance_values.symbolize_keys
item.each do |k,v|
...
..
end
There is a code to show it's usage here (look at the update in the question itself) - Grouping via an element in hash
If you want the string representations of your instance variables, you can do:
self.instance_values.keys.each do |k|
..
end
That should loop over all the keys (instance variable names as strings) that are defined for your class.
The attributes you are referencing is an instance variable on ActiveRecord models which returns a hash of the models db attributes.
A non-ActiveRecord model won't have the attributes method you are calling unless you defined it explicitly. All objects have a method called instance_variables which you can use to get a list of attribute names and then reflectively find the attribute values.
A not very elegant way of doing it:
class Test
attr_accessor :firstname,:lastname
def initialize(fn,ln); #firstname = fn; #lastname = ln; end
end
o = Test.new("Fernando", "Mendez")
o.instance_variables.each {|e| p o.send e.to_s.sub("#","").to_sym}
output:
"Fernando"
"Mendez"
If you want to keep a track of the key-value pair:
r = o.instance_variables.map {|e| Hash[e,(o.send e.to_s.sub("#","").to_sym)]}
#[{:#firstname=>"Fernando"}, {:#lastname=>"Mendez"}]

Order of params stops *_attributes= from working

So I've implemented a hack and I want to know what the "proper" way is to do it.
The issue is that I have an *_attributes=() method that uses an instance variable. The reason this is a problem is that at the time the method is called, that instance variable hasn't been set. Here is the method in question:
def proposed_times_attributes=(attributes)
attributes.each do |key,value|
value[:timezone] = timezone
end
assign_nested_attributes_for_collection_association(:proposed_times, attributes)
end
The timezone is in the params hash after proposed_times_attributes. Therefore my hack is to delete it from params, then add it back, thus moving it to the end of the line.
def create
p = params[:consultation]
a = p.delete(:proposed_times_attributes)
p[:proposed_times_attributes] = a
#consultation = current_user.advised_consultations.new(p)
...
end
What is the proper way that I should be doing this?
new() calls load() where the loop is that goes through each key/value pair.
Thankfully I'm using Ruby 1.9.2 which keeps the order, but it would be nice to know how to do this so that it wouldn't depend on this fact.
If the next operation after new will always be a save operation, you can store the attributes in an accessor, and use a before_validation callback to operate on them as you wish.
class Consultations < ActiveRecord::Base
attr_accessor :proposed_times_attributes
before_validation :assign_proposed_times
def assign_proposed_times
proposed_times_attributes.each do |key,value|
value[:timezone] = timezone
end
assign_nested_attributes_for_collection_association(:proposed_times, attributes)
end
end
Now in your controller you simply have:
def create
#consultation = current_user.advised_consultations.new(params[:consultation])
...
end
If you wish to do other operations before calling save, then pulling out the param as you did in your example, then passing it to an appropriate method after calling new would be the way to go.

custom attr_reader in rails

Mostly in rails if you write my_obj.attr it looks up attr in the database and reports it back. How do you create a custom def attr method that internally queries the database for attr, modifies it and returns? In other words, what's the missing piece here:
# Within a model. Basic attr_reader method, will later modify the body.
def attr
get_from_database :attr # <-- how do I get the value of attr from the db?
end
Something like that:
def attr
value = read_attribute :attr
value = modify(value)
write_attribute :attr, value
save
value
end
Neutrino's method is good if you want to save a modified value back to the database each time you get the attribute. This not recommended since it will execute an extra database query every time you try to read the attribute even if it has not changed.
If you simply want to modify the attribute (such as capitalizing it for example) you can just do the following:
def attr
return read_attribute(:attr).capitalize #(or whatever method you wish to apply to the value)
end

Resources