ActiveRecord how to set associated attributes from a hash? - ruby-on-rails

I have an ActiveRecord model instance where I want to modify attributes where some attributes exist on an association rather than directly on the model itself. All of the attributes come in as a hash.
This code is part of my test suite so I'm not concerned about issues of mass attribute assignment in this case.
If I try to update using instance.update_attributes(hash) this fails.
As an alternative I tried looping over the hash and setting the attributes like this.
hash.each do |key, val|
instance[key] = val
end
This method will set attributes that exist directly on the instance, but throws an exception on attributes that are tied to associated records ActiveModel::MissingAttributeError Exception: can't write unknown attribute 'foo'

Give the delegate approach a go. Something along the lines of:
class Foo
attr_accessor :bar, :foo_attribute
delegate :bar_attribute=, to: :bar
def initialize
#bar = Bar.new
end
end
class Bar
attr_accessor :bar_attribute
end
Then, in the console:
hsh = {foo_attribute: 'foo', bar_attribute: 'bar'}
f = Foo.new
hsh.each do |k,v|
f.send("#{k}=",v)
end
f.inspect
=> "#<Foo:0x00000004f1f0f0 #bar=#<Bar:0x00000004f1f0c8 #bar_attribute=\"bar\">, #foo_attribute=\"foo\">"
f.bar.inspect
=> "#<Bar:0x00000004f1f0c8 #bar_attribute=\"bar\">"

Related

How to convert an Object to array in ruby

I have a class with (as example) 3 attributes ,I want to convert the class's attribute to an array ,so I can store them into my csv file.
class Human
attr_accessor :name,:Lname,:id
....
end
when I create :
human1=Human.new("nice","human",1)
I need a function that return ["nice","human",1].
Is there a predefined one that I didn't find or I have to redefine to_a so it does the job.
note: the class has more than 3 attribute
is there a function to go through the object attribute or not.
I need a function that return ["nice","human",1]
Creating such method is trivial. If it is specifically for CSV, I would name it accordingly, e.g.:
class Human
attr_accessor :name, :lname, :id
# ...
def to_csv
[name, lname, id]
end
end
To generate a CSV:
require 'csv'
human1 = Human.new("nice", "human", 1)
csv_string = CSV.generate do |csv|
csv << ['name', 'lname', 'id']
csv << human1.to_csv
end
puts csv_string
# name,lname,id
# nice,human,1
Note that I've renamed Lname to lname in the above example. Uppercase is reserved for constants.
is there a function to go through the object attribute or not?
No, there is no built in way to actually do what you want and you might be falling for a common beginner trap.
attr_accessor does not "define attributes" since Ruby doesn't actually have properties/attributes/members like other langauges do. It defines a setter and getter method for an instance variable. Ruby doesn't keep track of which properties an object is presumed to have - only the actual instance variables which have been set.
But Ruby does provide the basic building blocks to make any kind of attributes system you want. This is very simplefied (and quite rubbish) example:
class Human
# this is a class instance variable
#attributes = []
# a class method that we use for "defining attributes"
def self.attribute(name)
attr_accessor name
#attributes << name
end
attribute(:name)
attribute(:l_name)
attribute(:id)
def initialize(**kwargs)
kwargs.each {|k,v| send("#{k}=", v) }
end
# the attributes that are defined for this class
def self.attributes
#attributes
end
# cast a human to an array
def to_a
self.class.attributes.map{ |attr| send(attr) }
end
# cast a human to an hash
def to_h
self.class.attributes.each_with_object({}) do |attr, hash|
hash[attr] = send(attr)
end
end
end
jd = Human.new(
name: 'John',
l_name: 'Doe',
id: 1
)
jd.to_a # ['John', Doe, 1]
jd.to_h # {:name=>"John", :l_name=>"Doe", :id=>1}
Here we are creating a class method attribute that adds the names of the "attributes" to a class instance variable as they are declared. Thus the class "knows" what attributes it has. It then uses attr_accessor to create the setter and getter as usual.
When we are "extracting" the attributes (to_a and to_h) we use the list we have defined in the class to call each corresponding setter.
Usually this kind functionality would go into a module or a base class and not the actual classes that represent your buisness logic. For example Rails provides this kind of functionality through ActiveModel::Attributes and ActiveRecord::Attributes.

How to assign multiple attributes at once to an object in Ruby

I have an object Foo and want to assign multiple attributes to it at once, similar to assign_attributes in Rails:
class Foo
attr_accessor :a, :b, :c
end
f = Foo.new
my_hash = {a: "foo", b: "bar", c: "baz"}
f.assign_attributes(my_hash)
The above does not work except if the class is an ActiveRecord Model in Rails. Is there any way to do it in Ruby?
You can implement the mass-assignment method yourself.
One option is to set the corresponding instance variables via instance_variable_set:
class Foo
attr_accessor :a, :b, :c
def assign_attributes(attrs)
attrs.each_pair do |attr, value|
instance_variable_set("##{attr}", value)
end
end
end
Note that this will by-pass any custom setters. As said in the docs:
This may circumvent the encapsulation intended by the author of the class, so it should be used with care.
Another way is to dynamically invoke the setters via public_send:
def assign_attributes(attrs)
attrs.each_pair do |attr, value|
public_send("#{attr}=", value)
end
end
This is equivalent to setting each single attribute sequentially. If a setter has been (re)defined to include constraints and controls on the value being set, the latter approach respects that.
It also raises an exception if you try to set undefined attributes: (because the corresponding setter doesn't exist)
f = Foo.new
f.assign_attributes(d: 'qux')
#=> NoMehodError: undefined method `d=' for #<Foo:0x00007fbb76038430>
In addition, you might want to ensure that the passed argument is indeed a hash and maybe raise a custom exception if the provided attributes are invalid / unknown.
The assign_attributes is an instance method of ActiveRecord
You have to define your assign_attributes method if not using ActiveRecord
def assign_attributes(attrs_hash)
attrs_hash.each do |k, v|
self.instance_variable_set("##{k}", v)
end
end

Is is possible to return virtual attributes automatically in Rails models?

Given:
class Foo
has_one :bar
def bar_name
bar.name
end
end
class Bar
belongs_to :foo
end
In the console or in a view, I can #foo.bar_name to get 'baz'.
I'm aware that I can #foo.as_json(methods: :bar_name) to get {"id"=>"abc123", "bar_name"=>"baz"}.
I could also denormalize the attribute and make it non-virtual, but I would rather not do that in this case.
Is it possible to automatically return the model with the virtual attribute included?
#<Foo id: "abc123", bar_name: "baz">
I want to do this because I am constructing a large object with nested collections of models, and the as_json call is abstracted away from me.
Not 100% sure I understand if your concern is related to as_json but if so this will work
class Foo
has_one :bar
def bar_name
bar.name
end
def as_json(options={})
super(options.merge!(methods: :bar_name))
end
end
Now a call to #foo.as_json will by default include the bar_name like your explicit example does.
Ugly would not recommend but you could change the inspection of foo e.g. #<Foo id: "abc123", bar_name: "baz"> as follows
class Foo
def inspect
base_string = "#<#{self.class.name}:#{self.object_id} "
fields = self.attributes.map {|k,v| "#{k}: #{v.inspect}"}
fields << "bar_name: #{self.bar_name.inspect}"
base_string << fields.join(", ") << ">"
end
end
Then the "inspection notation" would show that information although I am still unclear if this is your intention and if so why you would want this.
You could use attr_accessor - per the Rails docs:
Defines a named attribute for this module, where the name is symbol.id2name, creating an instance variable (#name) and a corresponding access method to read it. Also creates a method called name= to set the attribute.

How to assign Rails model column by symbol

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

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"}]

Resources