As I understand Ruby inheritance and method lookup, when a child instance calls a parent's instance method, which in turn calls a method that's named in both parent and child, the scope is still at the child instance. So this will happen:
class Foo
def method1
"foo"
end
def method2
puts method1
end
end
class Bar < Foo
def method1
"bar"
end
end
Bar.new.method2
=> "bar"
However, when I do what I think is a similar thing with ActiveRecord associations, I don't get what I'd expect:
class Foo < ApplicationRecord
has_many :orders
has_many :order_items, through: :orders
end
class Bar < Foo
has_many :orders, -> { where(attribute1: 1) }
end
When I call bar.orders I get what I expect. But when I call bar.order_items I get the same result as if I had called foo.order_items (the query scope is not used). If I include has_many :orders_items, through: :orders in bar.rb it behaves as I expect. Why do ApplicationRecords behave this way? Am I comparing apples to oranges?
Like Max states in the comment, you're not defining methods, you're calling them, when you'r using the meta programming, so there is no inheritance involved.
Related
I'm trying to call a class method (currently a scope) that uses an attribute from its parent (or belongs_to) model, but can't seem to get it working right.
My models:
class Venue < ActiveRecord::Base
attr_accessible :address
has_many :events, :dependent => :destroy
end
class Event < ActiveRecord::Base
belongs_to :venue
scope :is_near, lambda {|city| self(Venue.address).near(city, 20, :units => :km)}
end
I know the syntax is wrong, but I think that illustrates what I'm intending to do. I want to get the address of the venue and call another method on it. I need the scope in the Event class so I can chain other scopes together.
Appreciate any ideas.
Since #address is not a class method but an instance method, you won't be able to do what you want by using a scope.
If you want to get all the events within a 20km range of a venue, create these class methods in Venue instead:
class Venue < ActiveRecord::Base
def self.events_near_city(city)
venues_near_city(city).map(&:events).flatten
end
private
def self.venues_near_city(city)
near(city, 20, :units => :km)
end
end
Then call it by using Venue.events_near_city(session[:city]) since, as you told me in chat, you're storing the city in the session.
As you've defined it above, address is not a class method - it's an instance method. You would have to have an instance of venue (like you do in your view) to call it.
Searching a bit more I found this page that answered the question in another way. This works better for me because it's simpler to call, and I can use it on various relations. In rails how can I delegate to a class method
class Venue < ActiveRecord::Base
attr_accessible :address
def self.is_near(city)
venues_near_city(city).map(&:events).flatten
end
private
def self.venues_near_city(city)
self.near(city, 20, :units => :km)
end
end
class Event < ActiveRecord::Base
belongs_to :venue
class << self
def is_near(*args, &block)
Venue.is_near(*args, &block)
end
end
end
And I call it with event.is_near(session[:city])
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).
Take the following for example:
class Foo < AR::Base
has_many :bars, :as => :barable, :dependent=> :destroy
accepts_nested_attributes_for :bars, :allow_destroy => true
end
class Bar < AR::Base
belongs_to :barable, :polymorphic => true
end
class Baz < Bar
before_save do
raise "Hi"
end
end
In the form for 'Foo' - I have fields_for :bars_attributes where a hidden field sets type to 'Baz'. The 'Baz' is succesfully created but the callback never fires. (It does, however, fire when manually creating a 'Baz' in the console.)
Any advice appreciated!
Baz's callbacks will only be triggered if you create it as a Baz object, i.e Baz.new(...).
However, you're not creating a Baz record, but rather a Bar record: Bar.new(type: 'Baz').
This will only trigger Bar's callbacks, even though that later on it will be treated as a Baz.
you need to specify additonal association in your Foo.rb
has_many :bazs
# or
# has_many :bazs class_name: 'ModuleName::Baz' # if you scoped your child classed within some module
If you do that your
before_save do
raise "Hi"
end
will fire on for example #current_user.bazs.build
I am writing an ActiveRecord extension that needs to know when an association is modified. I know that generally I can use the :after_add and :after_remove callbacks but what if the association was already declared?
You could simply overwrite the setter for the association. That would also give you more freedom to find out about the changes, e.g. have the assoc object before and after the change E.g.
class User < ActiveRecord::Base
has_many :articles
def articles= new_array
old_array = self.articles
super new_array
# here you also could compare both arrays to find out about what changed
# e.g. old_array - new_array would yield articles which have been removed
# or new_array - old_array would give you the articles added
end
end
This also works with mass-assignment.
As you say, you can use after_add and after_remove callbacks. Additionally set after_commit filter for association models and notify "parent" about change.
class User < ActiveRecord::Base
has_many :articles, :after_add => :read, :after_remove => :read
def read(article)
# ;-)
end
end
class Article < ActiveRecord::Base
belongs_to :user
after_commit { user.read(self) }
end
I have a model as follows:
class Property < ActiveRecord::Base
has_and_belongs_to_many :property_values
end
What I would like to do is to extend any value returned by a find on the property_values extension with a module that is determined by an attribute of the Property object. I've attempted something like this:
class Property < ActiveRecord::Base
has_and_belongs_to_many :property_values, :extend => PropertyUtil::Extensible
def enrich(to_extend)
modules.split(/\s*,\s*/).each do |mod|
to_extend.extend(Properties.const_get(mod.to_sym))
end
end
end
module PropertyUtil
module Extensible
def self.extended(mod)
mod.module_eval do
alias old_find find
end
end
def find(*args)
old_find(*args).map{|prop| proxy_owner.enrich(prop)}
end
end
end
Where all modules that may be selected are defined in the Properties module. In attempting to run with this code, though, there are a couple of problems; first, to my surprise, none of the dynamic finders (property_values.find_by_name, etc.) appear to delegate to find; second, something with how I've done the aliasing leads to a stack overflow when I try to run the find directly.
Is there a way to do what I'm attempting? What method can I alias and override such that all results returned by the association extension, irrespective of how they are retrieved, are extended with the appropriate modules?
Thanks, Kris
I never tried to do this but you may want to try the following (I just changed how the aliases are done):
class Property < ActiveRecord::Base
has_and_belongs_to_many :property_values, :extend => PropertyUtil::Extensible
def enrich(to_extend)
modules.split(/\s*,\s*/).each do |mod|
to_extend.extend(Properties.const_get(mod.to_sym))
end
end
end
module PropertyUtil
module Extensible
def self.extended(mod)
mod.module_eval do
alias_method :old_find, :find
alias_method :find, :new_find
end
end
def new_find(*args)
old_find(*args).map{|prop| proxy_owner.enrich(prop)}
end
end
end
If it does not work here is another idea you may wanna try:
class Value < ActiveRecord::Base
self.abstract_class = true
end
class ExtendedValue < Value
end
class ExtendedValue2 < Value
end
class Property < ActiveRecord::Base
has_and_belongs_to_many :property_values, :class_name => 'ExtendedValue'
has_and_belongs_to_many :property_values_extended, :class_name => 'ExtendedValue'
has_and_belongs_to_many :property_values_extended2, :class_name => 'ExtendedValue2'
end
The idea is to have one hatbm association per "type" (if you can group your extensions that way) and use the one you want at a given time, if you can do what you want that way I am also pretty sure it will have a smaller impact performance than patching every returned object after activerecord returned them.
I am kinda curious at what you are trying to achieve with this :)
It is much easier to simply use classes to change the functionality. You can have classes of PropertyValues with the appropriate behavior and use either STI (Single Table Inheritance) to instantiate the appropriate instance or you can over-ride the 'instantiate' ActiveRecord class method to set the class using the #becomes instance method:
class PropertyValue < AR:Base
def self.instantiate(record)
property_value = super
case property_value.sub # criteria for sub_class
when 'type1' then property_value.becomes(Type1)
when 'type2' then property_value.becomes(Type2)
end
end
end
class Type1 < PropertyValue
def some_method
# do Type1 behavior
end
end
class Type2 < PropertyValue
def some_method
# do Type2 behavior
end
end
I have found that using classes and inheritance provides much cleaner, simpler code and is easier to test.
I ended up using an after_find call on the value class to resolve this problem. This is a pretty suboptimal solution, because it means that the module information ends up needing to be duplicated between the property referent and the value, but it's workable, if less than exactly performant. The performance hit ended up being large enough that I had to cache a bunch of data in the database with the results of computations over large numbers of properties, but this turned out not to be all bad, in that it simplified the process for extraction of report data considerably.
In the end, here are some bits of what I ended up with:
module Properties::NamedModules
def modules
(module_names || '').split(/\s*,\s*/).map do |mod_name|
Property.const_get(mod_name.demodulize.to_sym)
end
end
end
module Properties::ModularProperty
def value_structure
modules.inject([]){|m, mod| m + mod.value_structure}.uniq
end
end
module Properties::Polymorphic
include NamedModules, ModularProperty
def morph
modules.each {|mod| self.extend(mod) unless self.kind_of?(mod)}
end
end
class Property < ActiveRecord::Base
include Properties::NamedModules, Properties::ModularProperty
has_and_belongs_to_many :property_values, :join_table => 'property_value_selection'
def create_value(name, value_data = {})
property_values.create(
:name => name,
:module_names => module_names,
:value_str => JSON.generate(value_data)
)
end
end
class PropertyValue < ActiveRecord::Base
include Properties::Polymorphic
has_and_belongs_to_many :properties, :join_table => 'property_value_selection'
after_find :morph
end