Extend model in plugin with "has_many" using a module - ruby-on-rails

I have a some code in an engine style plugin which includes some models. In my app I want to extend one of these models. I have managed to add both instance and class methods to the model in question by including a module from within an initializer.
However I cannot seem to add associations, callbacks etc. I get a 'method not found' error.
/libs/qwerty/core.rb
module Qwerty
module Core
module Extensions
module User
# Instance Methods Go Here
# Class Methods
module ClassMethods
has_many :hits, :uniq => true # no method found
before_validation_on_create :generate_code # no method found
def something # works!
"something"
end
end
def self.included(base)
base.extend(ClassMethods)
end
end
end
end
end
/initializers/qwerty.rb
require 'qwerty/core/user'
User.send :include, Qwerty::Core::Extensions::User

You should be able to do this. A little more concise IMHO.
module Qwerty::Core::Extensions::User
def self.included(base)
base.class_eval do
has_many :hits, :uniq => true
before_validation_on_create :generate_code
end
end
end

I think this should work
module Qwerty
module Core
module Extensions
module User
# Instance Methods Go Here
# Class Methods
module ClassMethods
def relate
has_many :hits, :uniq => true # no method found
before_validation_on_create :generate_code # no method found
end
def something # works!
"something"
end
end
def self.included(base)
base.extend(ClassMethods).relate
end
end
end
end
end
The old code is wrong cause the validation and the association are called upon module loading, and this module knows nothing about ActiveRecord. That's a general aspect of Ruby, code inside class or module bodies is called directly when loaded. You don't want that. To get around that you can use the above solution.

In Rails 3, this sounds like a good use case for ActiveSupport::Concern:
module Qwerty::Core::Extensions::User
extend ActiveSupport::Concern
included do
has_many :hits, :uniq => true
before_validation_on_create :generate_code
end
end
class User
include Querty::Core::Extensions::User
# ...
end
Here are the ActiveSupport::Concern docs and the most helpful blog article on it I've found.

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

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

What is the preferred way to add some method in active record base?

I want to create a module which provides some common methods to the classes which are inherited from active record base.
Following is the two-way we can achieve it.
1)
module Commentable
def self.extended(base)
base.class_eval do
include InstanceMethods
extend ClassMethods
end
end
module ClassMethods
def test_commentable_classmethod
puts 'test class method'
end
end
module InstanceMethods
def test_commentable_instance_method
puts 'test instance method'
end
end
end
ActiveRecord::Base.extend(Commentable)
2)
module Commentable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def test_commentable_classmethod
puts 'test class method'
end
end
def test_commentable_instance_method
puts 'test instance methods'
end
end
ActiveRecord::Base.send(:include, Commentable)
Which one is the preferred way to handle this?
And
What to use when?
As of Rails 5, the recommended way is to make a module and include it in the models where it is needed, or everywhere using ApplicationRecord, which all models inherit from. (You can easily implement this pattern from scratch in older versions of Rails.)
# app/models/concerns/my_module.rb
module MyModule
extend ActiveSupport::Concern
module ClassMethods
def has_some_new_fancy_feature(options = {})
...
end
end
end
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
include MyModule
end
Modules are a form of multiple-inheritance, and sometimes add unnecessary complexity. Check if a decorator, service, or other kind of object makes more sense first. Not everything needs be a fancy macro that adds 50 callbacks to your model. You will hate your life if you do this too much.
If you want to monkey-patch (DON'T DO THIS), here is my old answer:
# config/initializers/activerecord_extensions.rb
ActiveRecord::Base.send(:include, MyModule)
Or without monkey-patching (see Mori's response):
# app/models/base_model.rb
class BaseModel < ActiveRecord::Base
self.abstract_class = true
include MyModule
end
Edit: Several months down the road in a large project, I have realized its better to have every model inherit from a new base model class, as Mori explains. The problem with including modules directly into ActiveRecord::Base is this can interfere with third-party code that also relies on ActiveRecord. It is just better not to monkey-patch when you don't have to. In this case, creating a new base class can end up being simpler in the long run.
Another way is make your own base class by inheriting from ActiveRecord::Base and then letting your models inherit from that base class. This has the advantage of making it clear that your models aren't running on vanilla ActiveRecord:
class MyBase < ActiveRecord::Base
self.abstract_class = true
def self.a_class_method
end
def an_instance_method
end
end
class Foo < MyBase
end
Foo.a_class_method
Foo.new.an_instance_method
reffering with Mori's answer...you can do something like:-
Module ActiveRecordUtilities
class MyBase < ActiveRecord::Base
self.abstract_class = true
def self.a_class_method
end
def an_instance_method
end
end
end##class ends
end##module ends
and can use it ...suppose in user.rb
include ActiveRecordUtilities::MyBase
User.a_class_method
#user.instance_method
============================OR====================
module MyUtils
def do_something_funky
# Some exciting code
end
end
class Account < ActiveRecord::Base
belongs_to :person, :extend => MyUtils
end
And then call it like this:
#account = Account.first
#account.person.do_something_funky

Using ActiveRecord:Relation for specific classes, not all relations across ActiveRecord

I'm building a gem in which part of its purpose is to extend associations on a target Class. Although I can easily extend all associations by using something like :
ActiveRecord::Relation.send(:include, MyGem::ActiveRecord::RelationMethods)
This is too broad, and for a Rails App that may use this Gem, I don't want to extend associations for all Classes.
For better granularity, I want to provide the equivalent of :
class User < ActiveRecord::Base
has_many :messages, :extend => MyGem::ActiveRecord::RelationMethods
has_many :comments, :extend => MyGem::ActiveRecord::RelationMethods
end
By using
class User < ActiveRecord::Base
acts_as_my_fancy_gem
has_many :messages
has_many :comments
end
The problem I have is trying to conditionally extend associations within the Gem, when acts_as_my_fancy_gem is added to a class. This is the bare bones of it.
module MyGem
extend ActiveSupport::Concern
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_as_my_fancy_gem
include MyGem::InstanceMethods
end
end
module InstanceMethods
...
end
end
I've looked into reflections, but at this point can find a clear path, and have simply taking stabs in the dark to experiment.
UPDATE:
Currently, I can achieve this with each association by providing a class method like
class User < ActiveRecord::Base
has_many :messages
has_many :comments
fancy_extend :messages
end
module MyGem
extend ActiveSupport::Concern
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def acts_as_my_fancy_gem
include MyGem::InstanceMethods
end
def fancy_extend *associations
class_eval do
associations.each do |association|
reflections[association].options[:extend] = MyGem::ActiveRecord::RelationMethods
end
end
end
end
module InstanceMethods
...
end
end
Adding this approach into the act_as_my_fancy method (which is where I would like to have it) gives me :
# NoMethodError: undefined method `options' for nil:NilClass
Is this rail4? I did not find the :extend option documented. It looks like rails 4 uses blocks to do that nowadays.
It could be as simple as this:
module Fancy
def has_many(name, scope = nil, options = {})
super(name, scope, options) do
def doit
"i did"
end
end
end
end
# in your model
extend Fancy
YourModel.your_relation.doit # => 'i did'

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

Resources