Railsy rozszerzające ActiveRecord :: Base

160

Czytałem trochę o tym, jak rozszerzyć klasę ActiveRecord: Base, aby moje modele miały specjalne metody. Jak łatwo go rozszerzyć (samouczek krok po kroku)?

xpepermint
źródło
Jakie rozszerzenia? Naprawdę potrzebujemy więcej.
jonnii

Odpowiedzi:

336

Istnieje kilka podejść:

Korzystanie z ActiveSupport :: Concern (preferowane)

Przeczytaj dokumentację ActiveSupport :: Concern, aby uzyskać więcej informacji.

Utwórz plik o nazwie active_record_extension.rbw libkatalogu.

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)

Utwórz plik w config/initializerskatalogu o nazwie extensions.rbi dodaj następujący wiersz do pliku:

require "active_record_extension"

Dziedziczenie (preferowane)

Zapoznaj się z odpowiedzią Toby'ego .

Łatanie małp (należy unikać)

Utwórz plik w config/initializerskatalogu o nazwie 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

Słynny cytat Jamiego Zawinskiego o wyrażeniach regularnych może zostać wykorzystany ponownie, aby zilustrować problemy związane z małpami łatania.

Niektórzy ludzie, gdy stają przed problemem, myślą: „Wiem, użyję małpiego łatania”. Teraz mają dwa problemy.

Łatanie małp jest łatwe i szybkie. Jednak zaoszczędzony czas i wysiłek są zawsze wycofywane w przyszłości; z odsetkami składanymi. Obecnie ograniczam patchowanie małp do szybkiego prototypowania rozwiązania w konsoli rails.

Harish Shetty
źródło
3
Musisz do requirepliku na końcu environment.rb. Dodałem ten dodatkowy krok do mojej odpowiedzi.
Harish Shetty
1
@HartleyBrody to tylko kwestia preferencji. Jeśli używasz dziedziczenia, musisz wprowadzić nową ImprovedActiveRecordi dziedziczyć po tym, kiedy używasz module, aktualizujesz definicję danej klasy. Kiedyś korzystałem z dziedziczenia (przyczyna wieloletniego doświadczenia w Javie / C ++). Obecnie najczęściej używam modułów.
Harish Shetty
1
To trochę ironiczne, że Twój link faktycznie kontekstualizował i wskazywał, jak ludzie nadużywają cytatu. Ale z całą powagą mam problem ze zrozumieniem, dlaczego „małpie łatanie” nie byłoby najlepszym sposobem w tym przypadku. Jeśli chcesz dodać do wielu klas, moduł jest oczywiście drogą do zrobienia. Ale jeśli twoim celem jest rozszerzenie o jedną klasę, to czy nie jest to dlaczego Ruby sprawił, że rozszerzanie klas w ten sposób było tak łatwe?
MCB
1
@MCB, Każdy duży projekt zawiera kilka historii o trudnym do zlokalizowania błędzie wprowadzonym przez małpy. Oto artykuł Avdiego na temat zła związanego z łataniem : devblog.avdi.org/2008/02/23/… . Ruby 2.0 wprowadza nową funkcję o nazwie, Refinementsktóra rozwiązuje większość problemów z małpami łatania ( yehudakatz.com/2010/11/30/ruby-2-0-refinements-in-practice ). Czasami jest po to, by zmusić cię do kuszenia losu. A czasami to robisz.
Harish Shetty,
1
@TrantorLiu Yes. Zaktualizowałem odpowiedź, aby odzwierciedlała najnowszą dokumentację (wygląda na to, że class_methods zostało wprowadzone w 2014 r. Github.com/rails/rails/commit/ ... )
Harish Shetty
70

Możesz po prostu rozszerzyć klasę i po prostu użyć dziedziczenia.

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

class Foo < AbstractModel
end

class Bar < AbstractModel
end
Toby Hede
źródło
I jakby tego pomysłu, ponieważ jest to standardowy sposób to zrobić, ale ... ja się nie istnieje błąd Tabela „moboolo_development.abstract_models”: Lista pól z abstract_models. Gdzie mam to położyć?
xpepermint
23
Dodaj self.abstract_class = truedo swojego AbstractModel. Railsy będą teraz rozpoznawać model jako model abstrakcyjny.
Harish Shetty,
Łał! Nie sądziłem, że to możliwe. Wypróbowałem to wcześniej i zrezygnowałem, gdy ActiveRecord zakrztusił się, szukając AbstractModelw bazie danych. Kto by pomyślał, że prosty seter pomoże mi SUCHO! (Zaczynałem się wzdychać ... było źle). Dzięki Toby i Harish!
dooleyo
W moim przypadku jest to zdecydowanie najlepszy sposób na zrobienie tego: nie rozszerzam tutaj moich możliwości modelowania obcymi metodami, ale refaktoryzuję wspólne metody dla podobnie zachowujących się obiektów mojej aplikacji. Dziedziczenie ma tutaj znacznie większy sens. Nie ma preferowanej drogi, ale 2 rozwiązania w zależności od tego, co chcesz osiągnąć!
Augustin Riedinger,
To nie działa dla mnie w Rails4. Stworzyłem abstract_model.rb i umieściłem w katalogu moich modeli. wewnątrz modelu miał self.abstract_class = true Następnie sprawiłem, że jeden z moich innych modeli dziedziczy ... User <AbstractModel . W konsoli dostaję: User (zadzwoń do 'User.connection', aby nawiązać połączenie)
Joel Grannas
21

Możesz także używać ActiveSupport::Concerni być bardziej idiomatycznym rdzeniem Rails, takim jak:

module MyExtension
  extend ActiveSupport::Concern

  def foo
  end

  module ClassMethods
    def bar
    end
  end
end

ActiveRecord::Base.send(:include, MyExtension)

[Edytuj] po komentarzu od @daniel

Następnie wszystkie modele będą miały metodę foodołączoną jako metoda instancji, a metody w niej ClassMethodszawarte jako metody klas. Np. FooBar < ActiveRecord::BaseBędziesz mieć: FooBar.bariFooBar#foo

http://api.rubyonrails.org/classes/ActiveSupport/Concern.html

nikola
źródło
5
Zauważ, że InstanceMethodsjest to przestarzałe od Railsów 3.2, po prostu umieść swoje metody w treści modułu.
Daniel Rikowski
Umieściłem ActiveRecord::Base.send(:include, MyExtension)inicjalizator i wtedy to zadziałało. Szyny 4.1.9
6 stóp Dan
18

W przypadku Rails 4, koncepcja wykorzystania zagadnień do modularyzacji i OSUSZANIA modeli była najważniejsza.

Obawy w zasadzie pozwalają na grupowanie podobnego kodu modelu lub w wielu modelach w jednym module, a następnie używanie tego modułu w modelach. Oto przykład:

Rozważmy model artykułu, model zdarzenia i model komentarzy. Artykuł lub wydarzenie ma wiele komentarzy. Komentarz należy do artykułu lub wydarzenia.

Tradycyjnie modele mogą wyglądać tak:

Model komentarzy:

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

Model artykułu:

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

Model zdarzenia

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

Jak możemy zauważyć, istnieje znaczący fragment kodu wspólny zarówno dla modelu zdarzenia, jak i artykułu. Korzystając z obaw, możemy wyodrębnić ten wspólny kod w oddzielnym module.

W tym celu utwórz plik commentable.rb w app / model / problems.

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

A teraz twoje modele wyglądają tak:

Model komentarzy:

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

Model artykułu:

class Article < ActiveRecord::Base
    include Commentable
end

Model zdarzenia

class Event < ActiveRecord::Base    
    include Commentable
end

Jedną kwestią, którą chciałbym podkreślić podczas używania obaw, jest to, że obawy powinny być używane do grupowania „na podstawie domeny”, a nie grupowania „technicznego”. Na przykład grupowanie domen jest takie jak „Możliwość komentowania”, „Możliwość tagowania” itp. Grupowanie techniczne będzie wyglądać jak „FinderMethods”, „ValidationMethods”.

Oto link do posta, który okazał się bardzo przydatny do zrozumienia problemów w modelach.

Mam nadzieję, że napisanie pomoże :)

Aaditi Jain
źródło
7

Krok 1

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

Krok 2

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

Krok 3

There is no step 3 :)
Witalij Kushner
źródło
1
Chyba krok 2 należy umieścić w config / environment.rb. To nie działa dla mnie :(. Czy możesz napisać więcej pomocy? Thx.
xpepermint
5

Rails 5 zapewnia wbudowany mechanizm przedłużania ActiveRecord::Base.

Osiąga się to poprzez zapewnienie dodatkowej warstwy:

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

i wszystkie modele dziedziczą po nim:

class Post < ApplicationRecord
end

Zobacz np. Ten post na blogu .

Cegła suszona na słońcu
źródło
4

Żeby dodać do tego tematu, chwilę zastanawiałem się, jak przetestować takie rozszerzenia (poszedłem dalej ActiveSupport::Concern).

Oto jak skonfigurowałem model do testowania moich rozszerzeń.

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
Will Tomlins
źródło
4

W Railsach 5 wszystkie modele są dziedziczone z ApplicationRecord i daje to dobry sposób na dołączanie lub rozszerzanie innych bibliotek rozszerzeń.

# 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

Załóżmy, że moduł metod specjalnych musi być dostępny we wszystkich modelach, dołącz go do pliku application_record.rb. Jeśli chcemy zastosować to do określonego zestawu modeli, uwzględnij go w odpowiednich klasach modeli.

# 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

Jeśli chcesz mieć metody zdefiniowane w module jako metody klasowe, rozszerz moduł do ApplicationRecord.

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

Mam nadzieję, że pomoże to innym!

Ashik Salman
źródło
0

mam

ActiveRecord::Base.extend Foo::Bar

w inicjatorze

Dla modułu jak poniżej

module Foo
  module Bar
  end
end
Ed Richards
źródło