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
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
What is the correct way to do the following in Rails 4? I have a form that takes in a string and some other parameters for a particular model, let's say Foo. This model has an association to another model,
class Foo < ActiveRecord::Base
belongs_to :bar
end
I want to create a new Bar from the given string, assuming a Bar with that name doesn't already exist in the database.
My initial approach was to do this at the controller level, essentially grabbing the name from the params, and then attempting to create a object from it and mutate the original params to include the new object instead of the original string. But I'm realizing that that's not very idiomatic Rails and that I should instead be doing this entirely at the model level.
So how would I accomplish what I want to do at the model level? I am thinking I need a transient attribute and some kind of before_validation filter, but I am not very familiar with Rails. Any ideas?
Not sure to understand correctly what are you trying to do but you might want to take a look at rails nested attributes.
You would have a Foo class like the following:
class Foo < ActiveRecord::Base
has_one :bar
accepts_nested_attributes_for :bar
end
class Bar < ActiveRecord::Base
belongs_to :foo
end
And you could create a new Bar instance directly associated to Foo like so:
foo = Foo.new(bar_attributes: { name: "Bar's name" })
foo.bar.name
#=> "Bar's name"
Why do you need to mutate the params? IMO this should be done at the controller level but without changing any params at all.
class FoosController
before_filter :set_foo
before_filter :set bar
def update
#foo.bar = #bar
#foo.update(foo_params)
respond_with #foo
end
private
def set_foo
#foo = Foo.find(params[:id])
end
def set_bar
#bar = # create/find your Bar from the params here
end
def foo_params
params.require(:foo).permit(...)
end
end
Supose that I have the following class:
class Foo < ActiveRecord::Base
belongs_to :bar
end
In rails console I can do this:
foo = Foo.new
foo.bar_id = 3
But this can violates the encapsulation principle. I think that is better idea do:
foo = Foo.new
foo.bar = Bar.find(3);
And bar_id should be private/protected.
This has nothing to do with the mass assignment and strong parameters but it is an security issue too.
Is there any way to set to private some attributes?
Is there a way to make Rails ActiveRecord attributes private?
class MyModel < ActiveRecord::Base
private
def my_private_attribute
self[:my_private_attribute]
end
def my_private_attribute=(val)
write_attribute :my_private_attribute, val
end
end
I don't think just making the write accessor private or protected will reliably prevent change via update_attribute or mass assignment.
While it's not actually "private" per se, but you could get the desired effect by setting the attribute read_only, e.g.
attr_readonly :bar_id
and if you do need to update the value "private-ly," access it as #bar_id. Per the docs, "Attributes listed as readonly will be used to create a new record but update operations will ignore these fields."
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.