Here is the structure I'm working with:
app/models/model.rb
class Model < ActiveRecord::Base
attr_accessor :some_var
end
app/models/model_controller.rb
class ModelsController < ApplicationController
def show
#model = Model.find(params[:id])
#other_var
if #model.some_var.nil?
#model.some_var = "some value"
#other_var = "some value"
else
#other_var = #model.some_var
end
end
end
Whenever I run this code (e.g. the show method), the if clause is evaluated to be true (e.g. #model.some_var == nil). How do I get around this? Is there something wrong in my assumption of how attr_accessor works?
attr_accessor is a built-in Ruby macro which will define a setter and a getter for an instance variable of an object, and doesn't have anything to do with database columns with ActiveRecord instances. For example:
class Animal
attr_accessor :legs
end
a = Animal.new
a.legs = 4
a.legs #=> 4
If you want it to be saved to the database, you need to define a column in a migration. Then ActiveRecord will create the accessor methods automatically, and you can (should) remove your attr_accessor declaration.
Related
I have a model Customer that accepts a virtual attribute opening_balance. Code looks something like this:
model Customer < ApplicationRecord
attr_accessor :opening_balance
has_one :ledger_account
after_create :open_ledger_account
def opening_balance
ledger_account.opening_balance if ledger_account.present?
end
private
def open_ledger_account
create_ledger_account!(opening_balance: self.opening_balance)
end
But the issue here is self.opening_balance is calling the method defined in the class not the value stored in attr_accessor's opening_balance attribute.
I tried one more solution:
def open_ledger_account
create_ledger_account!(opening_balance: self.read_attribute(:opening_balance))
end
But this also doesn't work.
How to read the value stored in the actual attribute? Any help would be appreciated. I am using rails 5.1.
Thanks!
attr_accessor defines a instance variable and a method to access it (read/write).
So the easy way is to write:
def open_ledger_account
create_ledger_account!(opening_balance: #opening_balance)
end
The read_attribute would only work if opening_balance was an attribute in the database on Customer.
First you have to understand that attr_accessor does not define instance variables. It just creates setter and getter methods. What attr_accessor :name does is:
class Person
def name
#name
end
def name=(value)
#name = value
end
end
Now you can access the instance variable from the outside:
p = Person.new
p.name = 'Jane'
puts p.name
And you can also access the instance variable from the inside by using the getter method instead of #name:
class Person
attr_accessor :name
def hello
"hello my name is: #{name}"
end
end
attr_accessor does not "define" a instance variable. There is no declaration of members/attributes in Ruby like in for example Java. An instance variables is declared when it is first set. Accessing an instance variable that has not been assigned a value returns nil.
So whats happing here:
class Customer < ApplicationRecord
attr_accessor :opening_balance
# ...
def opening_balance
ledger_account.opening_balance if ledger_account.present?
end
end
Is that you are overwriting the getter created by attr_accessor. If you want to access the instance variable itself just use #opening_balance.
However...
You should just use delegate instead:
class Customer < ApplicationRecord
has_one :ledger_account
delegate :opening_balance, to: :ledger_account
end
In PHP, I can set an attribute (that is not a column in database) to a model. E.g.(PHP code),
$user = new User;
$user->flag = true;
But in rails, when I set any attribute that doesn't exist in database, it will throw an error undefined method flag. There is attr_accessor method, but what will happen if I need about ten temp attributes?
but what will happen if I need about ten temp attributes?
#app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :flag, :other_attribute, :other_attribute2, :etc...
end
attr_accessor creates "virtual" attributes in Rails -- they don't exist in the database, but are present in the model.
As with attributes from the db, attr_accessor just creates a set of setter & getter (instance) methods in your class, which you can call & interact with when the class is initialized:
#app/models/user.rb
class User < ActiveRecord::Base
attr_accessor :flag
# getter
def flag
#flag
end
# setter
def flag=(val)
#flag = val
end
end
This is expected because it's how ActiveRecord works by design. If you need to set arbitrary attributes, then you have to use a different kind of objects.
For example, Ruby provides a library called OpenStruct that allows you to create objects where you can assign arbitrary key/values. You may want to use such library and then convert the object into a corresponding ActiveRecord instance only if/when you need to save to the database.
Don't try to model ActiveRecord to behave as you just described because it was simply not designed to behave in that way. That would be a cargo culting error from your current PHP knowledge.
As the guys explained, attr_accessor is just a quick setter and getter.
We can set our Model attr_accessor on record initializing to be a Ruby#Hash for example using ActiveRecord#After_initilize method so we get more flexibility on temporarily storing values (idea credit to this answer).
Something like:
class User < ActiveRecord::Base
attr_accessor :vars
after_initialize do |user|
self.vars = Hash.new
end
end
Now you could do:
user = User.new
#set
user.vars['flag'] = true
#get
user.vars['flag']
#> true
All that attr_accessor does is add getter and setter methods which use an instance variable, eg this
attr_accessor :flag
will add these methods:
def flag
#flag
end
def flag=(val)
#flag = val
end
You can write these methods yourself if you want, and have them do something more interesting than just storing the value in an instance var, if you want.
If you need temp attributes you can add them to the singleton object.
instance = Model.new
class << instance
attr_accessor :name
end
Is it possible / advisable to have a member of a class that is not persisted to the database for a rails model?
I want to store the last type the user selects in a session variable. Since I cant set the session variable from my model, I want to store the value in a "dummy" class member that just passes the value back to the controller.
Can you have such a class member?
Adding non-persisted attributes to a Rails model is just like any other Ruby class:
class User < ActiveRecord::Base
attr_accessor :someattr
end
me = User.new(name: 'Max', someattr: 'bar')
me.someattr # "bar"
me.someattr = 'foo'
The extended explanation:
In Ruby all instance variables are private and do not need to be defined before assignment.
attr_accessor creates a setter and getter method:
class User < ActiveRecord::Base
def someattr
#someattr
end
def someattr=(value)
#someattr = value
end
end
There is one special thing going on here; Rails takes the hash you pass to User.new and maps the values to attributes. You could simulate this behavior in a plain ruby class with something like:
class Foo
attr_accessor :bar
def initialize(hash)
hash.keys.each do |key|
setter = "#{key}=".intern
self.send(setter, hash[key]) if self.respond_to? setter
end
end
end
> Foo.new(bar: 'baz')
=> <Foo:0x0000010112aa50 #bar="baz">
Classes in Ruby can also be re-opened at any point, ActiveRecord uses this ability to "auto-magically" add getters and setters to your models based on its database columns (ActiveRecord figures out which attributes to add based on the database schema).
Yes you can, the code below allows you to set my_class_variable and inside the model reference it as #my_class_variable
class MyCLass < ActiveRecord::Base
attr_accessor :my_class_variable
def do_something_with_it
#my_class_variable + 10
end
I have a model with some attributes and a virtual attribute.
This virtual attribute is used to make a checkbox in the creation form.
class Thing < ActiveRecord::Base
attr_accessor :foo
attr_accessible :foo
end
Since the field is a checkbox in the form, the foo attribute will receive '0' or '1' as value. I would like it to be a boolean because of the following code:
class Thing < ActiveRecord::Base
attr_accessor :foo
attr_accessible :foo
before_validation :set_default_bar
private
def set_default_bar
self.bar = 'Hello' if foo
end
end
The problem here is that the condition will be true even when foo is '0'. I would like to use the ActiveRecord type casting mechanism but the only I found to do it is the following:
class Thing < ActiveRecord::Base
attr_reader :foo
attr_accessible :foo
before_validation :set_default_bar
def foo=(value)
#foo = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
private
def set_default_bar
self.bar = 'Hello' if foo
end
end
But I feel dirty doing it that way. Is there a better alternative without re-writing the conversion method ?
Thanks
Your solution from the original post looks like the best solution to me.
class Thing < ActiveRecord::Base
attr_reader :foo
def foo=(value)
#foo = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(value)
end
end
If you wanted to clean things up a bit, you could always create a helper method that defines your foo= writer method for you using value_to_boolean.
I would probably create a module with a method called bool_attr_accessor so you could simplify your model to look like this:
class Thing < ActiveRecord::Base
bool_attr_accessor :foo
end
It seems like ActiveModel ought provide something like this for us, so that virtual attributes act more like "real" (ActiveRecord-persisted) attributes. This type cast is essential whenever you have a boolean virtual attribute that gets submitted from a form.
Maybe we should submit a patch to Rails...
In Rails 5 you can use attribute method. This method defines an attribute with a type on this model. It will override the type of existing attributes if needed.
class Thing < ActiveRecord::Base
attribute :foo, :boolean
end
Caution: there is incorrect behaviour of this attribute feature in rails 5.0.0 on models loaded from the db. Therefore use rails 5.0.1 or higher.
Look at validates_acceptance_of code (click Show source).
They implemented it with comparing to "0".
I'm using it in registrations forms in this way:
class User < ActiveRecord::Base
validates_acceptance_of :terms_of_service
attr_accessible :terms_of_service
end
If you really want cast from string etc you can use this:
def foo=(value)
self.foo=(value == true || value==1 || value =~ (/(true|t|yes|y|1)$/i)) ? true:false
end
Or add typecast method for String class and use it in model:
class String
def to_bool
return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
end
end
Why you don't do this :
def foo=(value)
#foo = value
#bar = 'Hello' if value == "1"
end
Im trying set the single table inheritance model type in a form. So i have a select menu for attribute :type and the values are the names of the STI subclasses. The problem is the error log keeps printing:
WARNING: Can't mass-assign these protected attributes: type
So i added "attr_accessible :type" to the model:
class ContentItem < ActiveRecord::Base
# needed so we can set/update :type in mass
attr_accessible :position, :description, :type, :url, :youtube_id, :start_time, :end_time
validates_presence_of :position
belongs_to :chapter
has_many :user_content_items
end
Doesn't change anything, the ContentItem still has :type=nil after .update_attributes() is called in the controller. Any idea how to mass update the :type from a form?
we can override attributes_protected_by_default
class Example < ActiveRecord::Base
def self.attributes_protected_by_default
# default is ["id","type"]
["id"]
end
end
e = Example.new(:type=>"my_type")
You should use the proper constructor based on the subclass you want to create, instead of calling the superclass constructor and assigning type manually. Let ActiveRecord do this for you:
# in controller
def create
# assuming your select has a name of 'content_item_type'
params[:content_item_type].constantize.new(params[:content_item])
end
This gives you the benefits of defining different behavior in your subclasses initialize() method or callbacks. If you don't need these sorts of benefits or are planning to change the class of an object frequently, you may want to reconsider using inheritance and just stick with an attribute.
Duplex at railsforum.com found a workaround:
use a virtual attribute in the forms
and in the model instead of type
dirtectly:
def type_helper
self.type
end
def type_helper=(type)
self.type = type
end
Worked like a charm.
"type" sometimes causes troubles... I usually use "kind" instead.
See also: http://wiki.rubyonrails.org/rails/pages/ReservedWords
I followed http://coderrr.wordpress.com/2008/04/22/building-the-right-class-with-sti-in-rails/ for solving the same problem I had. I'm fairly new to Rails world so am not so sure if this approach is good or bad, but it works very well. I've copied the code below.
class GenericClass < ActiveRecord::Base
class << self
def new_with_cast(*a, &b)
if (h = a.first).is_a? Hash and (type = h[:type] || h['type']) and (klass = type.constantize) != self
raise "wtF hax!!" unless klass < self # klass should be a descendant of us
return klass.new(*a, &b)
end
new_without_cast(*a, &b)
end
alias_method_chain :new, :cast
end
class X < GenericClass; end
GenericClass.new(:type => 'X') # => #<X:0xb79e89d4 #attrs={:type=>"X"}>