Metaprogramming question involving has_many & belongs_to association - ruby-on-rails

I have two classes:
class Activity < ActiveRecord::Base
belongs_to :activity_type
def belongs_to_cat_a?
self.activity_category == ActivityCategory.category_a
end
def belongs_to_cat_b?
self.activity_category == ActivityCategory.category_b
end
end
class ActivityCategory < ActiveRecord::Base
has_many :activities
def self.cat_a
ActivityCategory.find_by_name("CatA")
end
def self.cat_b
ActivityCategory.find_by_name("CatB")
end
end
Using metaprogramming, I changed ActivityCategory to the following:
class ActivityCategory < ActiveRecord::Base
has_many :activities
CATEGORIES = ['CatA', 'CatB']
class << self
CATEGORIES.each do |c|
define_method "#{c.underscore.downcase}" do # for ex: cat_a
find_by_name(c)
end
end
end
end
Ok. Now imagine in the class Activity that I have about 12 methods to check which category it belongs to.
Seems like a perfect candidate to be DRY'ed up a bit using MP.
How can I do so?

I'm not sure this is a good candidate for MP. First, you are hard coding you categories, which right away is writing code, instead of generating it. If you want to return a true/false statement when asked if it belongs to a certain category, you could just do the following:
class Activity < ActiveRecord::Base
...
def belongs_to? activity
activity_type.name == activity
end
end
and execute as so...
a = Activity.save(:activity_category => ActivityCategory.new(:name => "CatA")
a.belongs_to? "CatA" #=> true
or am i missing the point?

This isn't a recommended way as this code tends to be a little difficult to maintain (Jed's solution is nicer), but you can just apply your same style of MP you already have:
class Activity < ActiveRecord::Base
belongs_to :activity_type
CATEGORIES = ['CatA', 'CatB']
class << self
CATEGORIES.each do |c|
define_method "belongs_to_#{c.underscore.downcase}?" do # for ex: cat_a
self.activity_category == ActivityCategory.send( "category_#{c[-1]}".to_sym )
end
end
end
end

Firstly, I'd alter what you already have.
If you intercept it with method_missing, you can avoid hardcoding the category list.
class ActivityCategory < ActiveRecord::Base
has_many :activities
alias_method :old_method_missing, :method_missing
def self.method_missing(method, *args, &block)
if cat = self.class.find_by_name(method.to_s)
return cat
else
old_method_missing(method, *args, &block)
end
end
end
This works because if the method called isn't detected, it will pass it to the old method missing. Just don't name any categories "find" or anything similar if you want to pull any tricks like this!
In the same manner, in activity, you can do
class Activity < ActiveRecord::Base
belongs_to :activity_type
alias_method :old_method_missing, :method_missing
def method_missing(method, *args, &block)
if matchdata = /\Abelongs_to_category_(\w)\?/.match(method.to_s)
return ActivityCategory.find_by_name(matchdata[1]) == ActivityCategory.send(matchdata[1].to_sym)
else
old_method_missing(method, *args, &block)
end
end
end
I'm not sure the syntax is entirely right, but you could investigate that general approach

Related

Custom belongs_to attribute writer

An application I'm working on, is trying to use the concept of polymorphism without using polymorphism.
class User
has_many :notes
end
class Customer
has_many :notes
end
class Note
belongs_to :user
belongs_to :customer
end
Inherently we have two columns on notes: user_id and customer_id, now the bad thing here is it's possible for a note to now have a customer_id and a user_id at the same time, which I don't want.
I know a simple/better approach out of this is to make the notes table polymorphic, but there are some restrictions, preventing me from doing that right now.
I'd like to know if there are some custom ways of overriding these associations to ensure that when one is assigned, the other is unassigned.
Here are the ones I've tried:
def user_id=(id)
super
write_attribute('customer_id', nil)
end
def customer_id=(id)
super
write_attribute('user_id', nil)
end
This doesn't work when using:
note.customer=customer or
note.update(customer: customer)
but works when using:
note.update(customer_id: 12)
I basically need one that would work for both cases, without having to write 4 methods:
def user_id=(id)
end
def customer_id=(id)
end
def customer=(id)
end
def user=(id)
end
I would rather use ActiveRecord callbacks to achieve such results.
class Note
belongs_to :user
belongs_to :customer
before_save :correct_assignment
# ... your code ...
private
def correct_assignment
if user_changed?
self.customer = nil
elsif customer_changed?
self.user = nil
end
end
end

Iterate over STI list in rails 4

I have two sub models, called: Service and Product that inherits from ProductBase. And I have another model to consume it. Acquire that have many AcquireBasket. Check out my code:
product_base.rb:
class ProductBase < ActiveRecord::Base
extend ::EnumerateIt
include Searchable
self.table_name = 'products'
end
product.rb:
class Product < ProductBase
default_scope { where(kind: ProductKind::PRODUCT) }
def initialize(attributes = {})
super(attributes)
self.kind = ProductKind::PRODUCT
self.status = ProductStatus::DRAFT
end
end
service.rb:
class Service < ProductBase
default_scope { where(kind: ProductKind::SERVICE) }
def initialize(attributes = {})
super(attributes)
self.kind = ProductKind::SERVICE
self.status = ProductStatus::DRAFT
end
end
acquire_basket.rb:
class AcquireBasket < ActiveRecord::Base
extend ::EnumerateIt
belongs_to :acquire
belongs_to :product
end
In some part of my project, I get a list (acquire baskets) of both models, Service and Product. And I need to check if I have services inside of it.
My code to check was:
def services_in?(acquire)
acquire.baskets.map(&:product).detect(&:service?)
end
The code works, ONLY if I pass services first, and products after!! Or if I have only one of them.
You should be able to utilize the descendents method to iterate over all of the subclasses
I can't find the answer in blog post around the world, so I will share with you:
class AcquireBasket < ActiveRecord::Base
extend ::EnumerateIt
belongs_to :acquire
belongs_to :product, class_name: 'ProductBase'
end
The problem was, when I try to find (lazily) in a ActiveRecord::Relation, Rails lookup (I think) to just Product model. And It can't find other type models inside of it. So using this typo I put it to work.

ActiveRecord Problems using callbacks and STI

Hey folks, following problem with Rails and STI:
I have following classes:
class Account < AC::Base
has_many :users
end
class User < AC::Base
extend STI
belongs_to :account
class Standard < User
before_save :some_callback
end
class Other < User
end
end
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
type.new(*args, &block)
end
end
end
And now the problem:
Without rewriting User.new (in module STI), the callback inside User::Standard gets never called, otherwise the account_id is always nil if I create users this way:
account.users.create([{ :type => 'User::Standard', :firstname => ... }, { :type => 'User::Other', :firstname => ... }])
If I'm using a different approach for the module like:
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
super(*args, &block).becomes(type)
end
end
end
Then instance variables are not shared, because it's creating a new object.
Is there any solution for this problem without moving the callbacks to the parent class and checking the type of class?
Greetz
Mario
Maybe there's something I don't know, but I've never seen Rails STI classes defined in that manner. Normally it looks like...
app/models/user.rb:
class User < AC::Base
belongs_to :account
end
app/models/users/standard.rb:
module Users
class Standard < User
before_save :some_callback
end
end
app/models/users/other.rb:
module Users
class Other < User
end
end
It looks as though you are conflating class scope (where a class "lives" in relation to other classes, modules, methods, etc.) with class inheritance (denoted by "class Standard < User"). Rails STI relationships involve inheritance but do not care about scope. Perhaps you are trying to accomplish something very specific by nesting inherited classes and I am just missing it. But if not, it's possible it's causing some of your issues.
Now moving on to the callbacks specifically. The callback in Standard isn't getting called because the "account.users" relationship is using the User class, not the Standard class (but I think you already know that). There are several ways to deal with this (I will be using my class structure in the examples):
One:
class Account
has_many :users, :class_name => Users::Standard.name
end
This will force all account.users to use the Standard class. If you need the possibility of Other users, then...
Two:
class Account
has_many :users # Use this to look up any user
has_many :standard_users, :class_name => Users::Standard.name # Use this to look up/create only Standards
has_many :other_users, :class_name => Users::Other.name # Use this to look up/create only Others
end
Three:
Just call Users::Standard.create() and Users::Other.create() manually in your code.
I'm sure there are lots of other ways to accomplish this, but there are probably the simplest.
So I solved my problems after moving my instance variables to #attributes and using my second approach for the module STI:
module STI
def new(*args, &block)
type = args.dup.extract_options!.with_indifferent_access.delete(:type)
if type.blank? or (type = type.constantize) == self
super(*args, &block)
else
super(*args, &block).becomes(type)
end
end
end
class User < AR:Base
extend STI
belongs_to :account
validates :password, :presence => true, :length => 8..40
validates :password_digest, :presence => true
def password=(password)
#attributes['password'] = password
self.password_digest = BCrypt::Password.create(password)
end
def password
#attributes['password']
end
class Standard < User
after_save :some_callback
end
end
Now my instance variable (the password) is copied to the new User::Standard object and callbacks and validations are working. Nice! But it's a workaround, not really a fix. ;)

How can I extend an object returned from an ActiveRecord association at runtime?

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

adding class methods to ActiveRecord::Base

I have created an instance method which is also a callback (if that makes sense) which does some stuff thats irrelevant. I would love to be able to just call:
class Model < ActiveRecord::Base
fix_camelcase_columns
end
Instead at the moment I have this:
def after_find
self.class.columns.each do |column|
self.instance_eval("def #{column.name.to_underscore}; self.#{column.name}; end;")
end
end
I would love to abstract this and use it on other classes. Any pointers?
Well, you can open up ActiveRecord::Base and throw a method there:
class ActiveRecord::Base
def self.fix_camelcase_columns
define_method :after_find do
...
end
end
end
For a cleaner way, create a module:
module CamelcaseFixer
def self.included(base)
base.extend(self)
end
def fix_camelcase_columns
define_method :after_find do
...
end
end
end
and then in your model do
class Model < ActiveRecord::Base
include CamelcaseFixer
fix_camelcase_columns
end
Didn't test the code, see if it works.

Resources