Rails扩展ActiveRecord::Base

161

我阅读了一些关于如何扩展ActiveRecord:Base类的文章,以便我的模型可以拥有一些特殊方法。有什么简单的方法可以扩展它吗(逐步教程)?


什么样的扩展名?我们需要更多的信息。 - jonnii
9个回答

339

有几种方法:

使用ActiveSupport::Concern(首选)

阅读ActiveSupport::Concern文档以获取更多细节。

lib目录下创建名为active_record_extension.rb的文件。

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)

config/initializers 目录下创建一个名为 extensions.rb 的文件,然后在该文件中添加以下一行代码:

require "active_record_extension"

继承(首选)

请参考Toby的答案

猴子补丁(应该避免使用)

config/initializers目录中创建一个名为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

著名的正则表达式引用语,来自Jamie Zawinski,可以被重新运用来说明与猴子补丁相关的问题。

有些人面对问题时想:“我知道了,我会使用猴子补丁。”现在他们有两个问题了。

猴子补丁很容易而且快速。但是,所节省的时间和精力在未来总会被以复利的形式提取出来。如今,我仅将猴子补丁用于在Rails控制台中快速原型设计解决方案。


3
environment.rb 的末尾需要使用 require 命令引入文件。我已经在我的回答中添加了这一额外步骤。 - Harish Shetty
1
@HartleyBrody 这只是个人偏好问题。如果使用继承,你必须引入一个新的“ImprovedActiveRecord”并从中继承,当你使用“module”时,你正在更新相关类的定义。我过去常常使用继承(因为有多年的Java/C++经验),但现在我大多数时候使用模块。 - Harish Shetty
2
有点讽刺的是,你的链接实际上是在说明人们如何错误地使用和过度使用引用。但说真的,我很难理解为什么在这种情况下“猴子补丁”不是最好的方法。如果你想要添加到多个类中,那么模块显然是最好的选择。但是,如果你的目标是扩展一个类,那么这就是为什么Ruby以这种方式扩展类变得如此容易的原因了。 - MCB
1
@MCB,每个大项目都有一些关于由于猴子补丁引入而难以定位的错误故事。这里有一篇Avdi关于补丁恶习的文章:http://devblog.avdi.org/2008/02/23/why-monkeypatching-is-destroying-ruby/。Ruby 2.0引入了一个名为“Refinements”的新功能,解决了大部分猴子补丁问题(http://yehudakatz.com/2010/11/30/ruby-2-0-refinements-in-practice/)。有时候,一个功能只是为了让你诱惑命运。而有时候,你真的会去尝试。 - Harish Shetty
1
@TrantorLiu 是的。我已经更新了答案以反映最新的文档(看起来class_methods是在2014年引入的https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad) - Harish Shetty
显示剩余19条评论

69

你可以直接扩展类并使用继承。

class AbstractModel < ActiveRecord::Base  
  self.abstract_class = true
end

class Foo < AbstractModel
end

class Bar < AbstractModel
end

我喜欢这个想法,因为这是一种标准的做法,但是...我遇到了一个错误:表'moboolo_development.abstract_models'不存在:SHOW FIELDS FROM 'abstract_models'。我应该把它放在哪里? - xpepermint
23
在你的 AbstractModel 中添加 self.abstract_class = true。Rails将会把该模型识别为抽象模型。 - Harish Shetty
哇!没想到这是可能的。之前尝试过,但当ActiveRecord在数据库中寻找“AbstractModel”时出现问题而放弃了。谁知道一个简单的setter会帮助我DRY事情!(我开始感到不安...太糟糕了)。谢谢Toby和Harish! - dooleyo
在我的情况下,这绝对是最好的方法:我不会在此处使用外部方法扩展我的模型能力,而是重构我的应用程序中类似行为对象的常见方法。继承在这里更有意义。没有首选方式,但有两种解决方案,取决于您想要实现什么! - Augustin Riedinger
在Rails4中,这对我没有用。我创建了abstract_model.rb并将其放置在我的models目录中。在模型内部,它具有self.abstract_class = true。然后我使得我的另一个模型继承... User < AbstractModel。在控制台上,我得到了: User (call 'User.connection' to establish a connection) - Joel Grannas
继承会降低性能,因为你最终会得到一个具有许多祖先的类,并且这会增加方法查找的时间。扩展ActiveRecord的最佳方式是使用模块。 - Lev Lukomsky

21

你也可以使用ActiveSupport::Concern,这样更符合Rails核心的惯用法:

module MyExtension
  extend ActiveSupport::Concern

  def foo
  end

  module ClassMethods
    def bar
    end
  end
end

ActiveRecord::Base.send(:include, MyExtension)
然后,所有您的模型都将具有名为foo的方法,作为实例方法包含,并且在ClassMethods中包含的方法作为类方法。例如,在FooBar < ActiveRecord::Base上,您将拥有:FooBar.barFooBar#foohttp://api.rubyonrails.org/classes/ActiveSupport/Concern.html

5
请注意,自Rails 3.2起,InstanceMethods已被废弃,请将您的方法放入模块主体中。 - Daniel Rikowski
我在初始化程序中放置了 ActiveRecord::Base.send(:include, MyExtension),然后这对我起作用了。Rails 4.1.9 - 6ft Dan

21

在Rails 4中,使用concerns来模块化和DRY化您的模型的概念备受关注。

Concerns基本上允许您将一个模型或多个模型中类似的代码分组到一个单独的模块中,然后在模型中使用此模块。以下是一个示例:

考虑一个Article模型、一个Event模型和一个Comment模型。一篇文章或一个事件有很多评论。评论属于文章或事件之一。

传统上,这些模型可能看起来像这样:

Comment模型:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

文章模型:

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

事件模型

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

正如我们所注意到的,Event和Article模型都有一个显著的代码片段是相同的。使用concerns,我们可以将这个共同的代码提取到一个单独的模块Commentable中。
为此,在app/model/concerns中创建一个commentable.rb文件。
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

文章模型:

class Article < ActiveRecord::Base
    include Commentable
end

事件模型
class Event < ActiveRecord::Base    
    include Commentable
end

使用Concerns时需要注意的一点是应该根据领域而非技术来进行分组。例如,领域分组可以是“可评论”,“可标记”等,而基于技术的分组将是“FinderMethods”,“ValidationMethods”等。
这里有一篇link to a post我发现非常有用,可以帮助理解模型中的Concerns。
希望这篇文章能够帮到您 :)

8

步骤1

module FooExtension
  def foo
    puts "bar :)"
  end
end
ActiveRecord::Base.send :include, FooExtension

第二步

# Require the above file in an initializer (in config/initializers)
require 'lib/foo_extension.rb'

第三步

There is no step 3 :)

1
我猜步骤2必须放置在config/environment.rb中。对我来说它不起作用:(。你能写更多的帮助吗?谢谢。 - xpepermint

5

在Rails 5中,所有的模型都继承自ApplicationRecord,这为包含或扩展其他扩展库提供了良好的方式。

# 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

假设特殊方法模块需要在所有模型中使用,则将其包含在application_record.rb文件中。如果我们只想将其应用于特定一组模型,则将其包含在相应的模型类中。
# 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

如果您想将模块中定义的方法作为类方法使用,就需要将该模块扩展到ApplicationRecord中。

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  extend SpecialMethods
end

希望这能帮助其他人!

5

Rails 5 提供了一种内置机制来扩展ActiveRecord::Base

它通过提供额外的层次结构来实现:

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  # put your extensions here
end

所有模型都从该模型继承:

class Post < ApplicationRecord
end

请查看例如这篇博客文章,其中涉及IT技术相关内容。

4

补充一下这个话题,我花了一些时间来研究如何测试这样的扩展(我采用了ActiveSupport::Concern方法)。

以下是我为测试我的扩展设置模型的步骤。

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

0

我有

ActiveRecord::Base.extend Foo::Bar

在初始化器中

对于下面这样的模块

module Foo
  module Bar
  end
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接