1

I am using Ruby on Rails 3.2.2 and the Squeel gem. I have following statements and I am trying to refactoring the my_squeel_query method in a Mixin module (since it is used by many of my models):

# Note: 'article_comment_associations' and 'model_as_like_article_comment_associations'
# refer to database table names.

class Article < ActiveRecord::Base
  def my_squeel_query
    commenters.
      .where{
        article_comment_associations.article_id.eq(my{self.id}) & ...
      }
  end
end

class ModelAsLikeArticle < ActiveRecord::Base
  def my_squeel_query
    commenters.
      .where{
        model_as_like_article_comment_associations.article_id.eq(my{self.id}) & ...
      }
  end
end

My problem is that I can not refactoring article_comment_associations and model_as_like_article_comment_associations statements by generating a dynamic name in the Mixin module. That is, if that was a String I could dynamically generate the related name by using something like "#{self.class.to_s.singularize}_comment_associations" as the following:

class Article < ActiveRecord::Base
  include MyModule
end

class ModelAsLikeArticle < ActiveRecord::Base
  include MyModule
end

module MyModule
  def my_squeel_query
    commenters.
      .where{
        # Note: This code doesn't work. It is just an sample.
        "#{self.class.to_s.singularize}_comment_associations".article_id.eq(my{self.id}) & ...
      }
  end
end

But, since it is not my case, I cannot "build" the name and make the my_squeel_query to be "shared" across models.

How can I dynamically generate association names related to the Squeel gem? Should I think to refactoring in another way? What do you advice about?

Backo
  • 18,291
  • 27
  • 103
  • 170

3 Answers3

1

You can do this if you generate the methods dynamically. The Module.included method is provided for this purpose:

module ModuleAsLikeArticle
  def self.included(base)
    base.send(:define_method, "#{base.to_s.singularize}_comment_associations") do
      # ...
    end
  end
end

This gets triggered when the module is imported with include and allows you to create methods specifically tailored for that.

As a note you might want to use base.name.underscore.singularize for a more readable method name. By convention, method names should not have upper-case in them, especially not as the first character.

Conventional Rails type applications use a different approach, though, instead defining a class method that can be used to create these on-demand:

module ModuleAsLikeArticle
  def has_comments
    base.send(:define_method, "#{base.to_s.singularize}_comment_associations") do
      # ...
    end
  end
end

This would be used like this:

class ModelAsLikeArticle < ActiveRecord::Base
  extend MyModule

  has_comments
end

Since the method is not created until has_comments is called, you can safely extend ActiveRecord::Base and then insert the appropriate call in all the classes which require that functionality.

tadman
  • 208,517
  • 23
  • 234
  • 262
  • **You**: "[...] you can safely extend `ActiveRecord::Base` and then insert the appropriate call in all the classes which require that functionality". **I**: "How to make and to use that (in general and in *my case*)? I really don't understand the useful *reason* for making that (probably, it is because I am a newbie in this specific field)." – Backo Sep 26 '12 at 16:49
  • Instead of calling `include` on specific classes that require it, just `include` in `ActiveRecord::Base` and call a custom method that adds the required relationships. This is how `has_many` and related methods work. They're always there, but they don't do anything useful until called. – tadman Sep 26 '12 at 17:27
  • Thank you, maybe I am understanding some more of your approach... however, I'd be glad if you provide some links to useful resources (at least one, please!) where I can learn more on this matter and how to make it "powerful" (especially with `ActiveRecord::Base` inclusions). – Backo Sep 26 '12 at 17:35
  • I'm not sure what the *method-that-generates-methods-dynamically* pattern is called, but it's used throughout ActiveRecord for many things. `belongs_to`, `has_many` and all those related functions work precisely this way, creating methods where none existed, all depending on options passed to the method. The best way to learn more is to experiment with this approach. – tadman Sep 26 '12 at 19:42
1

Since the DSL is instance_evaled, you can actually say something like:

def my_squeel_query
  base = self
  commenters.
    .where{
      # Note: This code does work. Because it's awesome.
      __send__("#{base.class.to_s.singularize}_comment_associations").
        article_id.eq(my{self.id})
    }
end
Ernie
  • 896
  • 5
  • 7
  • Can I use something else than `__send__`? The `__send__` statement seems less expressive (to me). – Backo Sep 26 '12 at 23:25
  • Use `__send__`. You could use `send`, but that is prone to name collisions -- which is the reason for having `__send__` in the first place. – zetetic Sep 27 '12 at 00:07
0

I think you might find what you need in the Rails Reflection class (http://api.rubyonrails.org/classes/ActiveRecord/Reflection/ClassMethods.html), which, as the page says, allows you to interrogate ActiveRecord classes about their associations and aggregations.

MrTheWalrus
  • 9,670
  • 2
  • 42
  • 66
  • I seen that, but it is my first time that I'd try to use it. Can you show me an example on how to use the `Reflection` in my case? – Backo Sep 26 '12 at 16:11
  • I'm not terribly familiar with actually using Reflection either - I remembered that it existed and sounded relevant to your situation. Perhaps someone else will know more. – MrTheWalrus Sep 26 '12 at 16:13