Closed andyl closed 10 years ago
I found that AR supports a #becomes
method (eg new_obj = obj.becomes(klass)
). Could be used in a view, or in a controller like:
texting_messages = user.messages.map {|x| x.becomes(Message::AsTexting)}
Better yet:
class Array
def becomes(klass)
self.map {|x| x.becomes(klass)}
end
end
texting_messages = user.messages.all.becomes(Message::AsTexting)
This seems ok - is there a better approach I should consider?
Hi @andyl,
Your approach is more linked with the base User class and it's results. I'm more inclined to let them "over there". Another "problem" is that it makes one conversion operation for each member of the messages collection. What about to first convert the User instance to a User::AsTexting and only after that to call the messages method on it? Perhaps an "elegant" way (IMHO) is to use a capitalized conversion method (Like Integer(), String(), etc). (But it may be just a matter of 'taste')
I saw this approach in details on the "Confident Ruby Book" by Avid Grimn (@avdi).
I'm not able to test code right now, but it would be something like...
class User::AsTexting < ActiveType::Record[User]
has_many :texting_messages, class_name: 'Message::AsTexting', foreign_key: :user_id
end
def User::AsTexting(user)
user.becomes(User::AsTexting)
end
# It would be...
texting_messages = User::AsTexting(user).messages
# versus
texting_messages = user.messages.all.becomes(Message::AsTexting)
At the book, there's a more elegant way to define this conversion function inside a module.
Please let me know if you test this approach.
@kratob and @henning-koch are the creators and maintainers of this gem.
Hi @abinoam - yes it is more efficient to incur the conversion cost once on the 'topmost' object.
I've had good luck using the object.becomes(klass)
style, and it has the extra benefit of not having to write a conversion function for each sub-class. For example:
collection = user.becomes(User::AsTexting).texting_messages
The other benefit of converting the 'topmost' object is that you can refine the relation, like:
col = user.becomes(User::AsTexting).texting_messages.where(condition: 'valid')
obj = user.becomes(User::AsTexting).texting_messages.create # create Message::AsTexting
There are differences between the Capitalized conversion function and the post-fix conversion methods (like .to_s
or .becomes
). But I don't know enough about the differences to say which is best.
Internally at makandra we usually define a becomes
method on ActiveRecord relations and has_many
associations (which are also relations).
It it possible to implement such a becomes
method without loading any records, and without having to to iterate over every record and cast it to a new class:
ActiveRecord::Base.class_eval do
def self.becomes(other_class)
other_class.scoped.merge(scoped)
end
end
You can use it similarly to the examples in the previous comments:
user.messages.becomes(Message::AsTexting)
Chaining subsequent calls like find
, where
or build
will then use the Message::AsTexting
model.
@henning-koch thanks this is just what I wanted.
But - it looks like the 'scoped' method was removed in Rails4.1. Can you give an example that is compatible with Rails 4.1 ?
The equivalent method in Rails 4 is all
. Or you can install edge_rider to get scoped
in Rails 4.
When I run the example in IRB things work as expected:
> new = Message::AsTexting.all
> old = user.messages
> new.klass #=> Message::AsTexting
> old.klass #=> Message
> new.merge(old).first.class #=> Message::AsTexting
But within the context of the becomes
method it doesn't work for me:
ActiveRecord::Base.class_eval do
def self.becomes(other_class)
new = other_class.all
old = all
puts "1> #{other_class}"
puts "2> #{new.klass}"
puts "3> #{old.klass}"
new.merge(old)
end
end
user.messages.becomes(Message::AsTexting).first.class
1> Message::AsTexting
2> Message
3> Message
Message
In #11, we talked about creating hard-coded associations that return ActiveType objects. This approach works in situations where you know the expected type ahead of time.
The downside of this approach is that you are forced to create a chain of ActiveType classes, which can sometimes grow deep and hard to manage.
In many situations, I want to dynamically select the type of object to be returned from an association. It would be great if I could do something like this:
The idea I have is similar to single-table-inheritance. But instead of storing the class name in a type field, it would be passed as a method argument.
Has anyone done anything like this? Is this possible in Rails?