How to build ActiveRecord associations with STI - ruby-on-rails

I'm having problems with AR trying to build associations of models that inherit from others. The problem is the associated models are being saved to the database before the call do the save method.
I found more information in this page http://techspry.com/ruby_and_rails/active-records-or-push-or-concat-method/
That's really weird, why would AR automatically save models appended to the association (with << method) ? One would obviously expect that the save method must called, even if the parent already exists. We can prevent this calling
#user.reviews.build(good_params)
but this would be a problem in a context where the association have an hierarchy, for example: if a Hunter has_many :animals, and Dog and Cat inherit from Animal, we can't do
#hunter.dogs.build
#hunter.cats.build
instead we are stuck with
#hunter.animals << Cat.new
#hunter.animals << Dog.new
and if the Cat/Dog class has no validations, the object will be saved automatically to the database. How can I prevent this behaviour ?

I found out that Rails 3 doesn't fully support associations with STI, and usually hacks are needed. Read more on this post http://simple10.com/rails-3-sti/. As mentioned in one of the comments, this issue is referred in rails 4 https://github.com/rails/rails/commit/89b5b31cc4f8407f648a2447665ef23f9024e8a5
Rails sux so bad handling inheritance = (( Hope Rails 4 fixes this.
Meanwhile I'm using this ugly workaround:
animal = #hunter.animals.build type: 'Dog'
then replace the built object, this step may be necessary for reflection to workout (check Lucy's answer and comments)
hunter.animals[#hunter.animals.index(animal)] = animal.becomes(Dog)
this will behave correctly in this context, since
hunter.animals[#hunter.animals.index(animal)].is_a? Dog
will return true and no database calls will be made with the assignment

Based on Gus's answer I implemented a similar solution:
# instantiate a dog object
dog = Dog.new(name: 'fido')
# get the attributes from the dog, add the class (per Gus's answer)
dog_attributes = dog.attributes.merge(type: 'Dog')
# build a new dog using the correct attributes, including the type
hunter.animals.build(dog_attributes)
Note that the original dog object is just thrown away. Depending on how many attributes you need to set it might be easier to do:
hunter.animals.build(type: 'Dog', name: 'Fido')

Related

Force reload another model's methods in rails?

I have a model that defines methods based off of the entries in another model's table: eg Article and Type. An article habtm types and vice versa.
I define in Article.rb:
Type.all.each do |type|
define_method "#{type.name}?" do
is?(:"#{type.name}")
end
end
This works great! it allows me to ensure that any types in the type db result in the methods associated being created, such as:
article.type?
However, these methods only run when you load the Article model. This introduces certain caveats: for example, in Rails Console, if I create a new Type, its method article.type_name? won't be defined until I reload! everything.
Additionally, the same problem exists in test/rspec: if I create a certain number of types, their associated methods won't exist yet. And in rspec, I don't know how to reload the User model.
Does anyone know a solution here? Perhaps, is there some way to, on creation of a new Type, to reload the Article model's methods? This sounds unlikely.. Any advice or guidance would be great!
I think you'll be better off avoiding reloading the model and changing your api a bit. In Article, are you really opposed to a single point of access through a more generic method?
def type?(type)
return is? type if type.is_a? String # for when type is the Type name already
is? type.name # for when an instance of Type is passed
end
If you're set on having separate methods for each type, perhaps something like this would work in your Type class
after_insert do
block = eval <<-END.gsub(/^ {6}/, '')
Proc.new { is? :#{self.name} }
END
Article.send(:define_method, "#{self.name}?", block)
end

Does add_to_base in rails adds a message to Activerecord::Base?

The method add_to_base(msg) in given link :
http://rails.rubyonrails.org/classes/ActiveRecord/Errors.html#M001712
is it really adding the message to Activerecord::Base is it what the document refers to as base object ?
Although i know the method is deprecated in rails 3
ActiveRecord::Base is the class that all ActiveRecord classes inherit from and it's quite confusing to think of it as the thing that the base object is derived from even though they share the same name. Base in the context of add_to_base means an instance of Foo < ActiveRecord::Base (for example)
It adds it to the base object rather than attaching any notion of errors directly to an attribute, this may be because an error may not specifically mention any attributes that the person may be changing or the error is associated with multiple attributes.
For Rails 3 - its errors.add(:base, msg)
No, it's just adding an error that's not associated with a specific model attribute.

ActiveRecord saves object when adding to association collection

From the Rails docs:
Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object (the owner of the collection) is not yet stored in the database.
I would like to prevent this save from occurring (for better or worse). I've come up with three strategies and would be curious to hear which one is recommended.
Block access to the DB during "<<". I posed the question of how best to accomplish this in another question: How can I prevent ActiveRecord from writing to the DB?. The proposed solution seems quite feasible.
Leveraging the caveat of "except if the parent object is not yet stored in the db". Looking at the rails code, this condition is determined with the new_record? method. I wrote a little method that makes an object think it's new for the duration of a block.
# model.rb
def appear_as_new_record
instance_eval { alias :old_new_record? :new_record? }
instance_eval { alias :new_record? :present? }
yield
instance_eval { alias :new_record? :old_new_record? }
end
# used like this:
model.appear_as_new_record do
model.gadgets << gadget
end
Using the 'build' method, to attach the object to the collection. This would like something like this (I know this doesn't cover all the edge cases)
new_gadget = model.gadgets.build(gadget.attributes)
new_gadget.id = gadget.id
there are quites a few scary things about this approach... but namely, it won't do a copy by reference. So if gadget also had a collection of objects already loaded, they wouldn't be accessible via the model without having to hit the db.

Polymorphic models hackery

Even though I know that, at least to my knowledge, this is not the default way of doing associations with ActiveRecord I'm looking for help in order to implement an "hibernatish" Polymorphic model.
For instance, consider the following base model:
# Content only has one property named path
class Content < ActiveRecord::Base
self.abstract_class = true
end
And the concrete content:
# Audio only has one property on it's own (duration).
# However, it should also inherit Content's property (path)
class Audio < Content
end
Now, something relatively interesting happens when using ActiveRecord, more accurately Rails 3 beta 3 ActiveRecord. If you set the abstract_class to false on the Content model and you execute the following:
Audio.create!(:path => '/dev/null')
It kinda works from an Hibernate perspective. That is, a Content record is created with ID 1 and an Audio record is also created with the ID = 1.
However, the problem #1 is that in order for this to work, you obviously need to turnoff the abstract_class, which kinda breaks the whole point of this abstract content example.
Furthermore, the problem #2 is that if you turn on the abstract_class you lose the content's properties when creating instances of Audio. And, if you turn it off, you lose the Audio properties when creating an instance of Audio.
Ideally, when faced with an abstract class that is then subclassed, ActiveRecord would provide the abstract + concrete properties to the concrete class being instantiated, in this case Audio. Effectively, that way, when creating an instance of Audio we would have:
audio = Audio.new #=> <Audio id: nil, duration: nil, path: nil, created_at: nil, updated_at: nil>
And then [naturally] when you assign audio.path = '/dev/null' and performed a save operation, ActiveRecord would know that the path attribute has been inherited, thus needs to be persisted at the parent class level. Furthermore, on that same save operation, should you set a non inherited property of audio ActiveRecord would also persist those changes in the audios table.
My question, after this introduction, is how one could go around active record and enhance it that way?
Effectively, let's assume that we're developing a gem that aims to provide active record with this kind of functionality. How would you go about and do it?
PS: I'm actually considering to develop such gem, though such hackery shouldn't go by without prior thinking. So, your feedback is most welcomed.
Best regards,
DBA
I see what you're going for here, but unfortunately I don't think you can get this to work out of the box the way you'd like.
Basically ActiveRecord uses model inheritance for two things:
Abstract classes, from which subclasses can inherit code and behavior but NOT table structure. You could implement a #path attribute on Content using attr_accessor and it would be inherited by subclasses, but it would not be persisted to the database unless the table for your model subclass had a 'path' column.
Single table inheritance, where subclasses inherit both behavior and table persistence.
You can mix the two, but there is no scenario where an ActiveRecord model can inherit a persistable column from an abstract class. There's always a 1:1 mapping between persisted attributes and table columns.

What role do ActiveRecord model constructors have in Rails (if any)?

I've just been reading this question which is about giving an ActiveRecord model's date field a default value. The accepted answer shows how to set the default value from within the controller. To my mind, this sort of business logic really belongs in the model itself.
Then I got to thinking how if this were Java I'd probably set the initial field value when declaring the instance variable or within the constructor. Since database-backed fields don't have to be explicitly declared within ActiveRecord models, is this something that you could use the model's initialize method for? I'm curious because I've not really seen much use of constructors for ActiveRecord models within the Rails code that I've looked at. Do they have a role to play and if so, what is it?
I do this quite often actually for default values. It works well and still lets the user change it. Remember, the initialize method is called when you say MyObject.new. However, you may want to read this blog entry (albeit a bit outdated) about using initialize.
You should use after_initialize instead of initialize. The initialize method is required by ActiveRecord::Base to prepare many of the convenience methods. If an after_initialize method is defined in your model it gets called as a callback to new, create, find and any other methods that generate instances of your model.
Ideally you'd want to define it like this:
def after_initialize
#attribute ||= default_value
end
Also note, you cannot use this callback like the others, you must define a method named after_initialize (like above) for it to work. You can't do the following:
after_initialize :run_some_other_method
#TopherFangio's answer is correct. It seems that the ActiveRecord API changed some time between his answer (2009) and now (2015).
As of today (Rails 4 with ActiveRecord 4.2.0), here's how you add initializers according to the ActiveRecord docs:
class Widget < ActiveRecord::Base
after_initialize |new_widget|
new_widget.name ||= 'Unnamed Widget'
end
end
You can verify with puts statements or by inspecting the new object from rails console that it actually initializes correctly.
According to this blog, active record doesn't always use new, so initialize might not be called on your object.

Resources