attr_accessor, not able to access property - ruby-on-rails

This is probably very stupid question but here we go
class Foo < ActiveRecord::Base
attr_accessor :group_id
end
From irb
# gets record which has group_id set to 106
foo = Foo.find(1)
foo.group_id
=> nil
If I go and remove
attr_accessor :group_id
All works as it should
foo = Foo.find(1)
foo.group_id
=> 106
My question is why? Shouldn't attr_accessor create accessor / mutator for property :group_id and that why all should be working. What am I missing?
Update
Good answers bellow, just as explanation for my motivation here is I want to use mass assignment of certain properties (you need this since Rails 3.2.x). For that you need attr_accessible , I find that code is much cleaner that way, of course if used responsibly :)

Looks like group_id is already a property on your Foo object (shown by the fact that it returns 106 when attr_accessor is omitted). By adding attr_accessor you are overriding what's already there and creating a method reader and writer called group_id. The return of the newly defined group_id is nil since you don't define anything.
Conceptually, you're ending up with something like this:
class Foo < ActiveRecord::Base
def group_id # overriding previous definition of 'group_id'
nil
end
end
Edit:
If your goal is expose properties then yes, use attr_accessible

This happens because ActiveRecord automatically maps the attributes in your database table to attributes in the actual model. When you add attr_accessor you overshadow the database attribute with you own, newly defined attribute.
Basically, rails automatically defines attr_accessor (or something similar) for all attributes in the corresponding database table.
If you're looking for some way on how to make only certain of your attributes accessible for multi-assignment, you should instead look at attr_accessible.

Without the accessor, for sure you have a 'belongs_to :group' in your Foo model, so if you call 'group_id' to an instance of 'Foo' you will get the value of this field.
For instance, if you set an accessor called like that attribute( group_id) it will overwrite the original field and it will return nil, if havenĀ“t assigned any value.
class Foo < ActiveRecord::Base
attr_accessor :group_id
end
# gets record which has group_id set to 106
foo = Foo.find(1)
foo.group_id
=> nil
foo.group_id = "wadus"
=> "wadus"
foo.group_id
=> "wadus"

If you have a column by the name group_id in the table, than ActiveRecord defines the accessor for you. When you overwrite it as you did, your calls simply searches for a regular instance variable named group_id, which returns nil.

Related

How should I set initialise instance variables on an ApplicationRecord object?

My app creates a QuoteRequest object and upon save of that object to the db I want it to create a Quote.new object (QuoteRequest has_one Quote). In a new Quote instance I want to set some instance variables, which various methods related to a Watir crawler will use, then the result of that Watir crawl & scrape will #save thus persist the Quote object to the db.
I would like to set the Quote instance variables in an initialize method as soon as the object is created, but this is creating issues with the fact that it inherits from ApplicationRecord this there's an initialization conflict.
What is the correct way to instantiate an ApplicationRecord object with variables, without conflicting with the Rails library initialization code?
If you just want to set one instance variable without any extra logic at initialize then Vijay's answer is a good one. However you can have more flexibility than just being restricted to attr methods.
As with all Ruby, you can always take advantage of super if you're maintaining the right interface to the super method. ActiveRecord::Base#initialize only takes one optional argument, a hash of attributes and values. It also yields itself to a block if a block is given. https://github.com/rails/rails/blob/master/activerecord/lib/active_record/core.rb#L313
So if you wanted to set an instance variable at initialization without also defining any public access methods, you can:
class Quote < ApplicationRecord
def initialize(attributes = nil, &block)
#my_var = attributes.delete(:my_var) if attributes
super
end
end
quote = Quote.new(my_var: 'value')
quote.instance_variable_get(:#my_var) # => "value"
You can also perform more complex operations. Assume you have User has_many :posts and Post belongs_to :user, User has a :name and Post has a :title.
class User < ApplicationRecord
def initialize(attributes = nil, &block)
title = attributes.delete(:first_post_title) if attributes
super
posts.build(title: "#{name}'s first post: #{title}") if title
end
end
user = User.new(first_post_title: 'Hello, world!', name: 'Matz')
user.posts.first # => #<Post:xxxxxxxxxxx title: "Matz's first post: Hello, world!">
Hopefully that demonstrates how flexible you can be even with ActiveRecord objects, and sometimes callbacks are a little too complicated in implementation and restrictive in use when super can work just fine and even better when used appropriately.
For the properties of Quote you don't want to save in DB, create attr_reader (or attr_accessor). Suppose your Watir method needs var1 and var2 objects, then your Quote should look like this:
class Quote < ApplicationRecord
attr_reader :var1, :var2
end
And then in after_create callback of QuoteRequest you should simply pass these variables as key-value pair like any other property.
obj1 = Var1.new
obj2 = Var2.new
quote = Quote.new(var1: obj1, var2: obj2, ....)`
Now you can access these by calling quote.var1 and quote.var2. Calling save on quote would persist all the properties which have corresponding columns in db except var1, var2.

Rails model attributes that are sometimes virtual and sometimes real

I have a very complicated Rails (v 3.2) model that is doing a manual select over multiple tables. This works great for getting all the data I need to display my Model. However, when my data doesn't exist and I have to create a virtual object, these columns don't exist on my model. For the life of me I can't figure out a way to support both virtual and actual columns on a model.
It is a lot more complicated than this, but this is the general select I currently have:
class MyObject < ActiveRecord::Base
attr_accessible :apples, :bananas, :oranges
def self.get(id)
select("my_objects.*, table1.apples, table2.bananas, table3.oranges")
.joins("left outer join table1 on something
left outer join table2 on something
left outer join table2 on something")
.where(:my_object => id)
end
end
This works great for what I need it to display when id exists. However, in some cases id doesn't exist and I have to display a virtual (or default) object.
I thought I could simply do something like this for that virtual object:
#my_object = MyObject.new({:apples => 1,
:bananas => 50,
:oranges => 10})
But of course, in the view when I do #my_object.apples I get an error because MyObject doesn't actually have those columns:
ActionView::Template::Error (unknown attribute: apples)
The next step I took was to add attr_accessor to the MyObject model:
attr_accessor :apples, :bananas, :oranges
That works perfect for the virtual version of MyObject. But now, when trying to display a real version of the object, all my apples, bananas, and oranges are nil! I assume this is because the attr_accessor getters and setters are overridding what the select is returning.
How can I support both virtual and actual attributes on this model?
p.s. I've tried multiple ways of using method_missing along with define_method, but have been unable to get anything that is successful.
Interesting problem.
One way is to define a setter method for virtual attribs like so
# MyObject.rb
def set_virtual_attribs=(hsh)
hsh.map{|key,value| self[key] = value}
end
then you can create a virtual object like so
#my_object = MyObject.new(:set_virtual_attribs => {
:apples => 1,
:bananas => 50,
:oranges => 10
})
The virtual attributes should now be available as #my_object.apples and this will not override any attribute methods for actual objects unless the set_virtual_attribs is called.
I would recommend looking into the Null Object Pattern; I think ideas along those lines will help in your case.
The general idea is that instead of having nil checks everywhere, you create an object that can stand in for the original object with some sensible defaults.
I'm assuming that something outside your class MyObject definition is deciding when id doesn't exist and therefore you need to do the #my_object = MyObject.new({:apples => 1,... call. Instead of creating an instance of MyObject there, you could have a different class like:
class MyNilObject
def apples
1
end
def bananas
50
end
def oranges
10
end
end
and then instead do #my_object = MyNilObject.new. If it's a requirement that you be able to set these default values dynamically, you can have a def initialize that takes a hash and assigns attributes and has attr_readers.
This class would be a Plain Old Ruby Object that wouldn't have any database persistence. It would need to implement any other methods that your view calls on an instance of MyObject.
I'm not sure what you do in this case after rendering the object, so you may need to implement a method like create on this object that would convert it into an instance of MyObject and save it in the database, potentially.
You could override the accessor methods in the models:
class MyObject < ActiveRecord::Base
def apples
read_attribute(:apples) || 1
end
# etc ....
end

STI, delegate and becomes

I have some STI setup like this:
class Document < ActiveRecord::Base
attr_accessible :name, description
# Basic stuff omitted
end
class OriginalDocument < Document
has_many :linked_documents, foreign_key: :original_document_id, dependent: :destroy
end
class LinkedDocument < Document
belongs_to :original_document
# Delegation, because it has the same attributes, except the name
delegate :description, to: :original_document
end
Now I want to dup the LinkedDocument and store it as an OriginalDocument, with its own name and keep the attribute values on duplication. However, my approachs are failing because somewhere, the duplicate still wants to access its delegated methods in the after_* callbacks.
class LinkedDocument < Document
def unlink_from_parent
original = self.original_document
copy = self.becomes OriginalDocument
copy.original_document_id = nil
copy.description = original.description
copy.save
end
end
This throws a RuntimeError: LinkedDocument#description delegated to original_document.description, but original_document is nil.
Doing an additional copy.type = 'OriginalDocument' to enforce things won't work, since the save query involves the type; UPDATE documents SET [...] WHERE documents.type IN('OriginalDocument') [...]. This fails, because at the time of the transaction, the object still is of type LinkedDocument.
What would be a clean way to copy an object and let it become another one? I thought of calling update_column for type and every attribute I want to copy over, but before doing it that inelegant way, I wanted to ask here.
I am going to add my solution here, in case no one has a better one. Hopefully, it will help someone.
To let the object become another without having wrong queries because the where clause is checking for the wrong type, I manually updated the type column without invoking any callbacks before calling become.
# This is for rails3, where +update_column+ does not trigger
# validations or callbacks. For rails4, use
#
# self.update_columns {type: 'OriginalDocument'}
#
self.update_column :type, 'OriginalDocument'
document = self.becomes OriginalDocument
Now for the assignments, there were two problems: First, the attribute setters somehow may trigger an exception because of the delegations. Second, the attributes I wanted to mass-assign were not listed in e.g. attr_accessible intentionally because they were internal attributes. So I resorted to a loop with an ugly update_column statement producing way too much queries (since rails3 has no update_columns).
original.attributes.except('id', 'name', 'original_document_id').each do |k,v|
document.update_column k.to_sym, v
end

Non persistent ActiveRecord model attributes

I want to add to an existing model some attributes that need not be persisted, or even mapped to a database column.
Is there a solution to specify such thing ?
Of course use good old ruby's attr_accessor. In your model:
attr_accessor :foo, :bar
You'll be able to do:
object.foo = 'baz'
object.foo #=> 'baz'
I was having the same problem but I needed to bootstrap the model, so the attribute had to persist after to_json was called. You need to do one extra thing for this.
As stated by apneadiving, the easiest way to start is to go to your model and add:
attr_accessor :foo
Then you can assign the attributes you want. But to make the attribute stick you need to change the attributes method. In your model file add this method:
def attributes
super.merge('foo' => self.foo)
end
In case anyone is wondering how to render this to the view, use the method arguments for the render method, like so:
render json: {results: results}, methods: [:my_attribute]
Please know that this only works if you set the attr_accessor on your model and set the attribute in the controller action, as the selected answer explained.
From Rails 5.0 onwards you could use attribute:
class StoreListing < ActiveRecord::Base
attribute :non_persisted
attribute :non_persisted_complex, :integer, default: -1
end
With attribute the attribute will be created just like the ones being persisted, i.e. you can define the type and other options, use it with the create method, etc.
If your DB table contains a matching column it will be persisted because attribute is also used to affect conversion to/from SQL for existing columns.
see: https://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html#method-i-attribute
In my case I wanted to use a left join to populate custom attribute. It works if I don't add anything but I also want to be able to set the attribute on a new object and of course it doesn't exist. If I add attr_accessor then it always returns nil after a select. Here's the approach I've ended up with that works for setting on new object and retrieving from left join.
after_initialize do
self.foo = nil unless #attributes.key?("foo")
end
def foo
#attributes["foo"]
end
def foo=(value)
#attributes["foo"] = value
end

Ruby on Rails - Overriding the association id creation process

I'm trying to override the way rails apply and id to an associated object, for example:
There are 2 simple models:
class Album < ActiveRecord::Base
has_many :photos
end
class Photo < ActiveRecord::Base
belongs_to :album
end
And then I want to do this:
album = Album.new :title => 'First Album'
album.photos.build
album.save #=> true
On this case I've created a plugin that overrides the id property and replaces it to a hashed string, so what I want to do is find the methods where this album_id is being replaced for my custom method instead of the int and be able to converted before it's saved.
But I want to act globally inside Rails structure because since it will be a sort of plugin I want to make this action work on dynamic models, that's why I can't create an before_save validation on the model.
I'm not sure if it's easy to understand, but I hope someone could help me on that..
Here's a screenshot of my current table so you can see what is happening:
SQLite3 DB http://cl.ly/1j3U/content
So as you can see the album_id it's being replaced for my custom ruby object when its saved...I've disabled the plugin and then it saved normally with records 11 and 12...
I want just act on a rails action and converted with my custom methods, something like
def rails_association_replaced_method(record)
#take the record associations and apply a to_i custom method before save
super(record)
end
something like this :)
Well I hope this didn't get too complicated
Cheers
It seems if I only override theActiveRecord::Base save method do the job if handled properly
define_method 'save' do
int_fields = self.class.columns.find_all { |column| column.type == :integer }
int_fields.each do |field|
if self.attributes[field.name]
self.attributes[field.name] = self.attributes[field.name].to_i
end
end
super
end
And this shall replace all the integer fields from the Current Model applying a to_i method over the result.
Rails is unfriendly to that kind of change to the defaults. What's your end goal here?

Resources