Is there a way to override one of the methods provided by an ActiveRecord association?
Say for example I have the following typical polymorphic has_many :through association:
class Story < ActiveRecord::Base
has_many :taggings, :as => :taggable
has_many :tags, :through => :taggings, :order => :name
end
class Tag < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :stories, :through => :taggings, :source => :taggable, :source_type => "Story"
end
As you probably know this adds a whole slew of associated methods to the Story model like tags, tags<<, tags=, tags.empty?, etc.
How do I go about overriding one of these methods? Specifically the tags<< method. It's pretty easy to override a normal class methods but I can't seem to find any information on how to override association methods. Doing something like
def tags<< *new_tags
#do stuff
end
produces a syntax error when it's called so it's obviously not that simple.
You can use block with has_many to extend your association with methods. See comment "Use a block to extend your associations" here.
Overriding existing methods also works, don't know whether it is a good idea however.
has_many :tags, :through => :taggings, :order => :name do
def << (value)
"overriden" #your code here
super value
end
end
If you want to access the model itself in Rails 3.2 you should use proxy_association.owner
Example:
class Author < ActiveRecord::Base
has_many :books do
def << (book)
proxy_association.owner.add_book(book)
end
end
def add_book (book)
# do your thing here.
end
end
See documentation
I think you wanted def tags.<<(*new_tags) for the signature, which should work, or the following which is equivalent and a bit cleaner if you need to override multiple methods.
class << tags
def <<(*new_tags)
# rawr!
end
end
You would have to define the tags method to return an object which has a << method.
You could do it like this, but I really wouldn't recommend it. You'd be much better off just adding a method to your model that does what you want than trying to replace something ActiveRecord uses.
This essentially runs the default tags method adds a << method to the resulting object and returns that object. This may be a bit resource intensive because it creates a new method every time you run it
def tags_with_append
collection = tags_without_append
def collection.<< (*arguments)
...
end
collection
end
# defines the method 'tags' by aliasing 'tags_with_append'
alias_method_chain :tags, :append
The method I use is to extend the association. You can see the way I handle 'quantity' attributes here: https://gist.github.com/1399762
It basically allows you to just do
has_many : tags, :through => : taggings, extend => QuantityAssociation
Without knowing exactly what your hoping to achieve by overriding the methods its difficult to know if you could do the same.
This may not be helpful in your case but could be useful for others looking into this.
Association Callbacks:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
Example from the docs:
class Project
has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
def evaluate_velocity(developer)
...
end
end
Also see Association Extensions:
class Account < ActiveRecord::Base
has_many :people do
def find_or_create_by_name(name)
first_name, last_name = name.split(" ", 2)
find_or_create_by_first_name_and_last_name(first_name, last_name)
end
end
end
person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name # => "Heinemeier Hansson"
Rails guides documents about overriding the added methods directly.
OP's issue with overriding << probably is the only exception to this, for which follow the top answer. But it wouldn't work for has_one's = assignment method or getter methods.
Related
models
class User
has_many :pictures
class Picture
belongs_to User
mount_uploader :picture, UserPictureUploader
def self.profile
find_by(is_profile: true)
end
controller
User.includes(:pictures).where(...
view
=user.pictures.profile.picture_url
This is causing the following Problem, that each picture will be queried (again).
if we use user.pictures.where(profile: true).picture_url it won't make any new sql-queries.
question:
How can we use scopes on the already included result?
With a little digging I found this question
What is the equivalent of the has_many 'conditions' option in Rails 4?
It looks like you can put conditions on your has_many relation to essentially be the scoped config you're looking for. It'd be something like:
has_many :old_pictures, :class_name => 'Picture',
:foreign_key => 'picture_id', -> { where('created_at < ?', Time.now - 7.days) }
Something like below will do
class User
has_many :pictures
scope :some_name, -> { includes(:pictures).where(your_query_here) }
end
Also, scopes are used only on a Class(in other words they are class methods), if you want to use it on the instance, then you need define an instance method something like below
class User
has_many :pictures
def some_name
self.includes(:pictures).where(your_query_here)
end
end
We can use ActiveRelation like this:
MyModel.where(:field => "test").create => #<Message ... field:"test">
But it doesnt work for joins with polymorphic has_one associations:
class RelatedModel < AR::Base
# has :some_field
belongs_to :subject, :polymorphic => true
end
class MyModel < AR::Base
# need some dirty magic here
# to build default related_model with params from active_relation
has_one :related_model, :as => :subject, :dependent => :destroy
end
describe MyModel do
it "should auto-create has_one association with joins" do
test = MyModel.joins(:related_model).where("related_models.subject_type" => "MyModel", "related_models.some_field" => "chachacha").create
test.related_model.should_not be_nil
test.related_model.some_field.should == "chachacha"
test.related_model.subject_type.should == "MyModel"
test.related_model.subject_id.should == test.id
# fails =)
end
end
Is it possible to extract active_relation params, pass them to MyModel for use in before_create and build RelatedModel with them?
Diving into ActiveRecord sources i found that
ActiveRecord::Relation covers 'create' with 'scoping' method.
ActiveRecord::Persistance 'create' calls 'initialize' from ActiveRecord::Core.
ActiveRecord::Core 'initialize' calls 'populate_with_current_scope_attributes'
This method declared in ActiveRecord::Scoping uses 'scope_attributes' declared in ActiveRecord::Scoping::Named.
scope_attributes creating relation 'all' and calls 'scope_for_create' on it.
'ActiveRecord::Relation's 'scope_for_create' uses only 'where_values_hash' from current_scope that does not contain rules like 'related_models.subject_type' (this values are contained in where_clauses). So we need to have simple key-value wheres to be used with 'create' on ActiveRecord::Relation. But ActiveRecord not clever enough to know that 'some_field' in where clause should be used with join table.
I found it can be implemented only by accessing where options with self.class.current_scope.where_clauses in 'before_create' on MyModel, parsing them and setting up attributes.
class MyModel < AR::Base
before_create :create_default_node
def create_default_node
clause = self.class.current_scope.where_clauses.detect{|clause| clause =~ /\`related_models\`.\`some_field\`/}
value = clause.scan(/\=.+\`([[:word:]]+)\`/).flatten.first
self.create_node(:some_field => value)
end
end
But it is so dirty, then i decided to find simpler solution and inverted dependency as described in Railscast Pro #394, moved RelatedModel functionality to MyModel with STI. Actually i needed such complicated relation creation because RelatedModel had some functionality common for all models (acts as tree). I decided to delegate 'ancestors' and 'children' to RelatedModel. Inverting dependency solved this problem.
class MyModel < AR::Base
acts_as_tree
belongs_to :subject, :polymorphic => true
end
class MyModel2 < MyModel
end
class RelatedModel < AR::Base
# has :some_field
has_one :my_model, :as => :subject, :dependent => :destroy
end
MyModel.create{|m| m.subject = RelatedModel.create(:some_field => "chachacha")}
MyModel.ancestors # no need to proxy relations
I’ve got what’s becoming a complex model, and am trying to DRY it out. In the case of my has_many options, instead of having them repeat, I’d like to simply load them from a method on the class.
class ExampleClass < ActiveRecord::Base
has_many :related_things, get_association_hash(arg1)
has_many :other_things, get_association_hash(arg2)
def get_association_hash(arg)
{ :class_name => 'SomeClass', :conditions => ['table.column = ?', arg] }
end
end
Unfortunately, this results in undefined method ‘get_association_hash’ for #<Class:0x007f9ae9efe6c0> when loading the class.
(As a sanity check, that method is fine if I just call it by itself, without including it in the has_many. Also, the actual class is considerably larger and so DRY is more helpful than in this small example.)
I do note that the error message mentions Class, and not my derived ExampleClass, so perhaps it has to do with how has_many is loaded, and where I define my method?
has_many is just a class method so this:
has_many :related_things, get_association_hash(arg1)
is just a method call like any other and the receiver in that context is your ExampleClass. That means that get_association_hash needs to be a class method. You'll also have to define it before your has_many calls or you won't be able to call it where you want to:
class ExampleClass < ActiveRecord::Base
def self.get_association_hash(arg)
{ :class_name => 'SomeClass', :conditions => ['table.column = ?', arg] }
end
has_many :related_things, get_association_hash(arg1)
has_many :other_things, get_association_hash(arg2)
end
That might be a bit ugly and make a mess of the usual definition order. If that's the case, then you can push your get_association_hash method into a module and then include that module at the top of your class:
module Pancakes
def self.included(base)
# There are various different ways to do this, use whichever one you like best
base.class_exec do
def self.get_association_hash(arg)
# ...
end
end
end
end
class ExampleClass < ActiveRecord::Base
include Pancakes
has_many :related_things, get_association_hash(arg1)
has_many :other_things, get_association_hash(arg2)
end
You'd probably call your module something more sensible than Pancakes, that's just my default name for things (because foo gets boring after awhile and I prefer Fargo over tradition).
I'm implementing a method, that will be used in many places of a project.
def do association
end
"association" is a symbol, like :articles, :tags, :users etc.
When the association is :articles, I need to work with the Article model.
When the association is :users, I need to work with the User model.
Etc.
I know, that I can write a helper method, that returns model class, depending on the provided symbol. But is there a ready to use method for that?
Rails provides a method called classify on the String class for such purpose.
:users.to_s.classify.constantize
#User
:line_items.to_s.classify.constantize
#LineItem
Edit:
If you are trying to retrieve the class associated with an association, use this approach:
Author.reflect_on_association(:books).klass
# => Book
This will address the scenario where the association name doesn't match the class name.
E.g:
class Order
has_many :line_items
has_many :active_line_items, :class_name => "LineItem",
:conditions => {:deleted => false}
end
In the example above, :active_line_items will result in ActiveLineItem and our original code will throw error.
Read more about this here.
This will work
(:users.to_s.singularize.capitalize.constantize).find :all, :conditions => ["name = ?", "john"]
And with your example
association.to_s.singularize.capitalize.constantize
Now I hava a problem,how can I make the callback#after_add receive a reference to the join model in a has_many :through association?
my code like this:
class Emergency
has_many :departments, :through => :eme_references, :after_add => Proc.new { |eme_reference| eme_reference.eme_flag = 1}
end
the attribute eme_flag is the model EmeReference's attribute! but in the block ,i get the eme_reference.class is Emergency.
I want to set the attribute eme_flag of the model EmeReference.
That is my question!
cheers!
Presumably Emergency also has_many :eme_references in order for the :through association to work?
In that case, you should be able to attach the callback there:
has_many :eme_references,
:after_add => Proc.new { |emergency, eme_ref| # code here }
The block accepts 2 parameters, the first will be the Emergency, the 2nd will be the EmeReference being added.
Perhaps a before_save callback on EmeReference can also do what you want in this instance?
I think what you want to do can't be done there.
You could create an after_create hook on departments (I'm assuming Emergency has_many eme_references has_many departments):
class Emergency
has_many :departments, :through => :eme_references
def flag!
eme_flag=1
save
end
end
class Department
after_create :check_emergency
# this allows you to call department.emergency. Will return nil if anything is nil
delegate :emergency, :to =>:eme_reference, :allow_nil => true
def check_emergency
self.emergency.flag! if self.emergency.present?
end
end