How can I extend ActiveRecord relations with new method? - ruby-on-rails

Let's imagine that I have simple ActiveRecord models like this:
class Post < ActiveRecord::Base
belongs_to :category
end
class Category < ActiveRecord::Base
has_many :posts
has_many :published_posts, -> { where(:published => true) }
end
And I want to create module Reindexable that will add a method called reindex into a base class. I want to be able to call this method in the 3 following ways:
Place.reindex
Place.reindex(Place.where(:published => true))
Place.where(:published => true).reindex
Category.first.places.reindex
Inside of this method I should be able to do something like this:
Reindexer.new(relation).reindex # how can I get relation here?
What is a correct way to do it in Ruby-on-Rails? How can I access current relation in all this situation?

I have tried to include code into ActiveRecord_Relation class that is dynamically added into any AR classes.
In case you need to add .to_csv to all calls like MyModel.all.to_csv:
Model:
class MyModel < ActiveRecord::Base
include ToCsv
end
You module that you want to include
module ToCsv
extend ActiveSupport::Concern
# Dynamically including our Relation and extending current class
#
def self.included(child_class)
child_class.const_get('ActiveRecord_Relation').include(RelationMethods)
child_class.extend(ExtendMethods)
end
# Instance Method
#
def to_csv(opts = {})
end
# Module containing methods to be extended into the target
#
module ExtendMethods
# Allowing all child classes to extend ActiveRecord_Relation
#
def inherited(child_class)
super + (child_class.const_get('ActiveRecord_Relation').include(RelationMethods) ? 1 : 0 )
end
end
# Holds methods to be added to relation class
#
module RelationMethods
extend ActiveSupport::Concern
# Relation Method
#
def to_csv(opts = {})
end
end
end

Related

gem module method cant be included to ActiveRecord::Base

I have a module in my gem
module joinSelect
def self.with
puts 'with called'
end
ActiveRecord::Base.send :include, self
end
but I am unable to access method with in any of model classes
irb(main):015:0> User.with
NoMethodError: undefined method `with' for User (call 'User.connection' to establish a connection):Class
I have tried putting
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include JoinSelect
#or
extend JoinSelect
end
``
doesen't work. How can I get "with" accessible on ApplicationRecord ?
Thanks in advance.
I'll recommend to include the module with your code only in the classes that will need that functionality. Including your code in ActiveRecord::Base is really not recommended, other gems you may use may conflict with it.
If you need your code to be available on all your ActiveRecord models, then define it in your ApplicationRecord. Since all your models will inherit from it, all will gain the functionality.
If you want to add a class method in your AR class, create a module with the function and extend it from your class:
module A
def foo
"Hi"
end
end
class User < ApplicationRecord
extend A
end
User.foo # => "Hi"
If you need to do more things, like declaring scopes, using ActiveRecord hooks, etc. then you'll need to use concerns, see here
define it without self in module
module JoinSelect
def with
puts 'with called'
end
end
and in ApplicationRecord use extend to include it as a class method
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
extend JoinSelect
end

Rails: How to retrieve the polymorphic '_type' column name for a polymorphic model dynamically?

I basically want to create a concern which will be included in all the polymorphic models. This concern needs to have a dynamic setter method which which sets the value for the '_type' column.
module StiPolymorphable
extend ActiveSupport::Concern
included do
define_method "#{magic_method_to_get_type_column}=" do |type_field|
super(type_field.to_s.classify.constantize.base_class.to_s)
end
end
end
I basically want to access all the addresses of a Parent instance instead of a Person instance.
Example -
Suppose I have the following classes
class Person < ActiveRecord::Base
end
class Parent < Person end
class Teacher < Person end
class Address < ActiveRecord::Base
include StiPolymorphable
belongs_to :addressable, polymorphic: true
end
Right now if I try to access the addresses of a Parent it gives me zero records since the addressable_type field contains the value 'Person'.
Parent.first.addresses => #<ActiveRecord::Associations::CollectionProxy []>
Person.first.addresses => #<ActiveRecord::Associations::CollectionProxy [#<Address id: .....>]>
You might be interested on looking at Modularity gem so you could pass variables when you're including the Module. Haven't really tried it though. Hope it helps.
We do something like this:
module Shared::PolymorphicAnnotator
extend ActiveSupport::Concern
class_methods do
# #return [String]
# the polymorphic _id column
def annotator_id
reflections[annotator_reflection].foreign_key.to_s
end
# #return [String]
# the polymorphic _type column
def annotator_type
reflections[annotator_reflection].foreign_type
end
end
included do
# Concern implementation macro
def self.polymorphic_annotates(polymorphic_belongs, foreign_key = nil)
belongs_to polymorphic_belongs.to_sym, polymorphic: true, foreign_key: (foreign_key.nil? ? (polymorphic_belongs.to_s + '_id').to_s : polymorphic_belongs.to_s)
alias_attribute :annotated_object, polymorphic_belongs.to_sym
define_singleton_method(:annotator_reflection){polymorphic_belongs.to_s}
end
attr_accessor :annotated_global_entity
# #return [String]
# the global_id of the annotated object
def annotated_global_entity
annotated_object.to_global_id if annotated_object.present?
end
# #return [True]
# set the object when passed a global_id String
def annotated_global_entity=(entity)
o = GlobalID::Locator.locate entity
write_attribute(self.class.annotator_id, o.id)
write_attribute(self.class.annotator_type, o.class.base_class)
true
end
end
end
In your model:
class Foo
include Shared::PolymorphicAnnotator
polymorphic_annotates('belongs_to_name', 'foreign_key')
end

How do I add global Rails helpers like has_many and devise_for?

In Rails, I'd like to apply functionality to many models at the same time, having the ability to add it to other models in the future.
I'm looking for something like...
class Stuff < ActiveRecord::Base
some_tag
end
class Thing < ActiveRecord::Base
some_tag
end
to give those and any other models with
some_tag
the functionality of
class Functionality
has_many :other_things, polymorphic: true
def does_something
end
end
First step: Create a module with the method you want to add to your classes:
module FooFunction
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def does_something(options = {})
...
end
end
end
Next step: Include that module into the class
# in config/initializers/foo_function.rb
class Object
include FooFunction
end

Rails extending ActiveRecord::Base

I've done some reading about how to extend ActiveRecord:Base class so my models would have some special methods. What is the easy way to extend it (step by step tutorial)?
There are several approaches :
Using ActiveSupport::Concern (Preferred)
Read the ActiveSupport::Concern documentation for more details.
Create a file called active_record_extension.rb in the lib directory.
require 'active_support/concern'
module ActiveRecordExtension
extend ActiveSupport::Concern
# add your instance methods here
def foo
"foo"
end
# add your static(class) methods here
class_methods do
#E.g: Order.top_ten
def top_ten
limit(10)
end
end
end
# include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtension)
Create a file in the config/initializers directory called extensions.rb and add the following line to the file:
require "active_record_extension"
Inheritance (Preferred)
Refer to Toby's answer.
Monkey patching (Should be avoided)
Create a file in the config/initializers directory called active_record_monkey_patch.rb.
class ActiveRecord::Base
#instance method, E.g: Order.new.foo
def foo
"foo"
end
#class method, E.g: Order.top_ten
def self.top_ten
limit(10)
end
end
The famous quote about Regular expressions by Jamie Zawinski can be re-purposed to illustrate the problems associated with monkey-patching.
Some people, when confronted with a problem, think “I know, I'll use
monkey patching.” Now they have two problems.
Monkey patching is easy and quick. But, the time and effort saved is always extracted back
sometime in the future; with compound interest. These days I limit monkey patching to quickly prototype a solution in the rails console.
You can just extend the class and simply use inheritance.
class AbstractModel < ActiveRecord::Base
self.abstract_class = true
end
class Foo < AbstractModel
end
class Bar < AbstractModel
end
You can also use ActiveSupport::Concern and be more Rails core idiomatic like:
module MyExtension
extend ActiveSupport::Concern
def foo
end
module ClassMethods
def bar
end
end
end
ActiveRecord::Base.send(:include, MyExtension)
[Edit] following the comment from #daniel
Then all your models will have the method foo included as an instance method and the methods in ClassMethods included as class methods. E.g. on a FooBar < ActiveRecord::Base you will have: FooBar.bar and FooBar#foo
http://api.rubyonrails.org/classes/ActiveSupport/Concern.html
With Rails 4, the concept of using concerns to modularize and DRY up your models has been in highlights.
Concerns basically allow you to group similar code of a model or across multiple models in a single module and then use this module in the models. Here is a example:
Consider a Article model, a Event model and a Comment Model. A article or A event has many comments. A comment belongs to either article or event.
Traditionally, the models may look like this:
Comment Model:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
Article Model:
class Article < ActiveRecord::Base
has_many :comments, as: :commentable
def find_first_comment
comments.first(created_at DESC)
end
def self.least_commented
#return the article with least number of comments
end
end
Event Model
class Event < ActiveRecord::Base
has_many :comments, as: :commentable
def find_first_comment
comments.first(created_at DESC)
end
def self.least_commented
#returns the event with least number of comments
end
end
As we can notice, there is a significant piece of code common to both Event and Article Model. Using concerns we can extract this common code in a separate module Commentable.
For this create a commentable.rb file in app/model/concerns.
module Commentable
extend ActiveSupport::Concern
included do
has_many :comments, as: :commentable
end
# for the given article/event returns the first comment
def find_first_comment
comments.first(created_at DESC)
end
module ClassMethods
def least_commented
#returns the article/event which has the least number of comments
end
end
end
And Now your models look like this :
Comment Model:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
Article Model:
class Article < ActiveRecord::Base
include Commentable
end
Event Model
class Event < ActiveRecord::Base
include Commentable
end
One point I will like to highlight while using Concerns is that Concerns should be used for 'domain based' grouping rather than 'technical' grouping. For example, a domain grouping is like 'Commentable', 'Taggable' etc. A technical based grouping will be like 'FinderMethods', 'ValidationMethods'.
Here is a link to a post that I found very useful for understanding concerns in Models.
Hope the writeup helps :)
Step 1
module FooExtension
def foo
puts "bar :)"
end
end
ActiveRecord::Base.send :include, FooExtension
Step 2
# Require the above file in an initializer (in config/initializers)
require 'lib/foo_extension.rb'
Step 3
There is no step 3 :)
Rails 5 provides a built-in mechanism for extending ActiveRecord::Base.
This is achieved by providing additional layer:
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
# put your extensions here
end
and all models inherit from that one:
class Post < ApplicationRecord
end
See e.g. this blogpost.
With Rails 5, all models are inherited from ApplicationRecord & it gives nice way to include or extend other extension libraries.
# app/models/concerns/special_methods.rb
module SpecialMethods
extend ActiveSupport::Concern
scope :this_month, -> {
where("date_trunc('month',created_at) = date_trunc('month',now())")
}
def foo
# Code
end
end
Suppose the special methods module needs to be available across all models, include it in application_record.rb file. If we wants to apply this for a particular set of models, then include it in the respective model classes.
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include SpecialMethods
end
# app/models/user.rb
class User < ApplicationRecord
include SpecialMethods
# Code
end
If you want to have the methods defined in the module as class methods, extend the module to ApplicationRecord.
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
extend SpecialMethods
end
Hope it help others !
Just to add to this topic, I spent a while working out how to test such extensions (I went down the ActiveSupport::Concern route.)
Here's how I set up a model for testing my extensions.
describe ModelExtensions do
describe :some_method do
it 'should return the value of foo' do
ActiveRecord::Migration.create_table :test_models do |t|
t.string :foo
end
test_model_class = Class.new(ActiveRecord::Base) do
def self.name
'TestModel'
end
attr_accessible :foo
end
model = test_model_class.new(:foo => 'bar')
model.some_method.should == 'bar'
end
end
end
I have
ActiveRecord::Base.extend Foo::Bar
in an initializer
For a module like below
module Foo
module Bar
end
end

Where to put common code found in multiple models?

I have two models that contain the same method:
def foo
# do something
end
Where should I put this?
I know common code goes in the lib directory in a Rails app.
But if I put it in a new class in lib called 'Foo', and I need to add its functionality to both of my ActiveRecord models, do I do that like this:
class A < ActiveRecord::Base
includes Foo
class B < ActiveRecord::Base
includes Foo
and then both A and B will contain the foo method just as if I had defined it in each?
Create a module, which you can put in the lib directory:
module Foo
def foo
# do something
end
end
You can then include the module in each of your model classes:
class A < ActiveRecord::Base
include Foo
end
class B < ActiveRecord::Base
include Foo
end
The A and B models will now have a foo method defined.
If you follow Rails naming conventions with the name of the module and the name of the file (e.g. Foo in foo.rb and FooBar in foo_bar.rb), then Rails will automatically load the file for you. Otherwise, you will need to use require_dependency 'file_name' to load your lib file.
You really have two choices:
Use a module for common logic and include it in A & B
Use a common class C that extends ActiveRecord and have A & B extend C.
Use #1 if the shared functionality is not core to each class, but applicable to each class. For example:
(app/lib/serializable.rb)
module Serializable
def serialize
# do something to serialize this object
end
end
Use #2 if the shared functionality is common to each class and A & B share a natural relationship:
(app/models/letter.rb)
class Letter < ActiveRecord::Base
def cyrilic_equivilent
# return somethign similar
end
end
class A < Letter
end
class B < Letter
end
Here's how I did it... First create the mixin:
module Slugged
extend ActiveSupport::Concern
included do
has_many :slugs, :as => :target
has_one :slug, :as => :target, :order => :created_at
end
end
Then mix it into every model that needs it:
class Sector < ActiveRecord::Base
include Slugged
validates_uniqueness_of :name
etc
end
It's almost pretty!
To complete the example, though it's irrelevant to the question, here's my slug model:
class Slug < ActiveRecord::Base
belongs_to :target, :polymorphic => true
end
One option is to put them in a new directory, for example app/models/modules/. Then, you can add this to config/environment.rb:
Dir["#{RAILS_ROOT}/app/models/modules/*.rb"].each do |filename|
require filename
end
This will require every file in in that directory, so if you put a file like the following in your modules directory:
module SharedMethods
def foo
#...
end
end
Then you can just use it in your models because it will be automatically loaded:
class User < ActiveRecord::Base
include SharedMethods
end
This approach is more organized than putting these mixins in the lib directory because they stay near the classes that use them.
If you need ActiveRecord::Base code as a part of your common functionalities, using an abstract class could be useful too. Something like:
class Foo < ActiveRecord::Base
self.abstract_class = true
#Here ActiveRecord specific code, for example establish_connection to a different DB.
end
class A < Foo; end
class B < Foo; end
As simple as that. Also, if the code is not ActiveRecord related, please find ActiveSupport::Concerns as a better approach.
As others have mentioned include Foo is the way to do things... However it doesn't seem to get you the functionality you want with a basic module. The following is the form a lot of Rails plugins take to add class methods and new callbacks in addition to new instance methods.
module Foo #:nodoc:
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
include Foo::InstanceMethods
before_create :before_method
end
module InstanceMethods
def foo
...
end
def before_method
...
end
end
end

Resources