Raylardaki STI alt sınıfları için rotaları ele almak için en iyi uygulamalar


175

Benim Raylar görüş ve denetleyiciler ile çevrili redirect_to, link_tove form_foryöntem çağrıları. Bazen link_tove redirect_tobağladıkları yollarda açıktırlar (örneğin link_to 'New Person', new_person_path), ancak birçok kez yollar örtüktür (örn. link_to 'Show', person).

Benim model (say) için bazı tek tablo kalıtım (STI) eklemek Employee < Personve tüm bu yöntemler alt sınıf (say Employee) bir örneği için kırmak ; raylar yürütüldüğünde link_to @personhata verir undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. Rails, çalışanın nesnenin sınıf adıyla tanımlanan bir rota arıyor. Bu çalışan yolları tanımlanmamıştır ve çalışan denetleyicisi yoktur, bu nedenle eylemler de tanımlanmaz.

Bu soru daha önce sorulmuştur:

  1. At StackOverflow , cevap tüm kod temeli link_to vb her örneğini düzenlemek ve açıkça yolunu ortaya koymaktır
  2. On StackOverflow yine iki kişi kullanıyor önermek routes.rbana sınıfına alt sınıf kaynakları (haritaya map.resources :employees, :controller => 'people'). Aynı SO sorusunun en üst yanıtı, kod tabanındaki her örnek nesneyi kullanarak.becomes
  3. StackOverflow'da bir başka soru da, en iyi cevap Kendini Tekrarla kampında yol ve her alt sınıf için yinelenen iskele oluşturmayı öneriyor.
  4. İşte aynı soru, en iyi cevabın sadece yanlış gibi göründüğü SO'da tekrar aynı soru (Rails magic Just Works!)
  5. Web'in başka yerlerinde, F2Andy'nin kodun her yerinde yolda düzenlemeyi önerdiği bu blog gönderisini buldum .
  6. Mantıksal Gerçeklik Tasarımında Tek Tablo Devralma ve RESTful Routes blog yayınında , yukarıdaki SO yanıt numarası 2'de olduğu gibi alt sınıfın kaynaklarının üst sınıf denetleyicisine eşlenmesi önerilir.
  7. Alex Reisner bir yazı vardır Rails Tek tablo Kalıtım içeri üst sınıfa çocuk sınıflarının kaynaklarını haritalama karşı savunan ettiği, routes.rbsadece yakalar gelen kırılmaları yönlendirme beri, link_tove redirect_tofakat gelen form_for. Bu nedenle, alt sınıfların sınıfları hakkında yalan söylemesi için ana sınıfa bir yöntem eklemeyi önerir. Kulağa hoş geliyor, ama yöntemi bana hata verdi undefined local variable or method `child' for #.

Bu yüzden en zarif görünen ve en fazla fikir birliğine sahip olan cevap (ama o kadar da zarif ya da bu kadar fikir birliği değil), kaynakları sizin için eklemektir routes.rb. Bunun dışında işe yaramıyor form_for. Netliğe ihtiyacım var! Yukarıdaki seçimleri damıtmak için seçeneklerim

  1. alt sınıfın kaynaklarını üst sınıftaki denetleyiciye eşler routes.rb(ve herhangi bir alt sınıfta form_for çağırmam gerekmez)
  2. Sınıfların birbirlerine yatmasını sağlamak için rayların iç yöntemlerini geçersiz kıl
  3. Kodda, bir nesnenin eylemine giden yolun dolaylı ya da açıkça çağrıldığı her örneği, yolu değiştirerek ya da nesneyi yazarak düzenleyin.

Tüm bu çelişkili cevaplarla bir karara ihtiyacım var. Bana öyle geliyor ki iyi bir cevap yok. Bu rayların tasarımında başarısız mı? Eğer öyleyse, düzeltilebilecek bir hata mı? Ya da değilse, o zaman birisinin beni doğrudan buna ayarlayabileceğini, her seçeneğin artılarını ve eksilerini yürütebileceğini umuyorum (veya bunun neden bir seçenek olmadığını açıklayın) ve hangisinin doğru cevap olduğunu ve neden olduğunu umuyorum. Yoksa internette bulamadığım doğru bir cevap var mı?


1
Alex Reisner'ın kodunda, bloguna yorum yaptıktan sonra düzelttiği bir yazım hatası vardı. Umarım şimdi Alex'in çözümü uygulanabilir. Sorum hala geçerli: hangisi doğru çözüm?
ziggurism

1
Yaklaşık üç yaşında olmasına rağmen, bu blog gönderisini rookieonrails.blogspot.com/2008/01/… adresinde ve raylar posta listesinden bağlantılı konuşmayı bilgilendirici buldum . Yanıtlayanlardan biri polimorfik yardımcılar ile adlandırılmış yardımcılar arasındaki farkı açıklar.
ziggurism

2
Listelemediğiniz bir seçenek, Rails'i bağlantılandırmaktır, böylece link_to, form_for ve benzeri yerler tek tablo devralma ile güzelleşir. Bu zor bir iş olabilir, ama düzeltmeyi görmek istediğim bir şey.
M. Scott Ford

Yanıtlar:


140

Bu, minimum yan etki ile ortaya koyabildiğim en basit çözüm.

class Person < Contact
  def self.model_name
    Contact.model_name
  end
end

Şimdi url_for @personiçin eşler contact_pathbeklendiği gibi.

Nasıl çalışır: URL yardımcıları YourModel.model_namemodele yansımaya ve (birçok şey arasında) tekil / çoğul yol anahtarları üretmeye güvenirler . Burada Persontemelde dostum gibi olduğumuContact söylüyor , ona sor .


4
Aynı şeyi yapmayı düşünüyordum, ancak #model_name'in Rails'in başka bir yerinde kullanılabileceğinden ve bu değişikliğin normal işleyişe müdahale edebileceğinden endişelendim. Düşüncesi olan var mı?
nkassis

3
Gizemli yabancı @nkassis'e tamamen katılıyorum. Bu harika bir hack, ama rayların iç kısımlarını yakmadığınızı nereden biliyorsunuz?
tsherif

6
Özellikleri. Ayrıca, bu kodu üretimde kullanıyoruz ve karışıklık yaratmadığını kanıtlayabilirim: 1) modeller arası ilişkiler, 2) STI modeli örnekleme ( build_x/ aracılığıyla create_x). Öte yandan, sihirle oynamanın fiyatı neyin değişebileceğinden asla% 100 emin olamayacağınızdır.
Prathan Thananart

10
Sınıfa bağlı olarak öznitelikler için farklı insan adlarına sahip olmaya çalışıyorsanız, bu i18n'yi keser.
Rufo Sanchez

4
Bu şekilde tamamen geçersiz kılmak yerine, ihtiyacınız olan bitleri geçersiz kılabilirsiniz. Bkz. Gist.github.com/sj26/5843855
sj26

47

Ben de aynı problemi yaşadım. STI kullandıktan sonra, form_foryöntem yanlış alt URL'ye gönderiyordu.

NoMethodError (undefined method `building_url' for

Çocuk sınıfları için ekstra yollar ekledim ve aynı kontrolörlere yönlendirdim.

 resources :structures
 resources :buildings, :controller => 'structures'
 resources :bridges, :controller => 'structures'

Bunlara ek olarak:

<% form_for(@structure, :as => :structure) do |f| %>

bu durumda yapı aslında bir yapıdır (çocuk sınıfı)

Bir başvuru yaptıktan sonra benim için çalışıyor gibi görünüyor form_for.


2
Bu işe yarıyor, ancak rotalarımıza pek çok gereksiz yol ekliyor. Bunu daha az müdahaleci bir şekilde yapmanın bir yolu yok mu?
Anders Kindberg

1
Route.rb dosyanızda rotaları programlı olarak ayarlayabilirsiniz, böylece alt yolları ayarlamak için biraz meta programlama yapabilirsiniz. Ancak, sınıfların önbelleğe alınmadığı ortamlarda (örn. Geliştirme) önce sınıfları önceden yüklemeniz gerekir. Yani bir şekilde veya başka bir yerde alt sınıfları belirtmeniz gerekir. Örnek için gist.github.com/1713398 adresine bakın .
Chris Bloom

Benim durumumda, nesne adını (yol) kullanıcıya göstermek arzu edilmez (ve kullanıcı için kafa karıştırıcı).
laffuste



14

@Phanhan Thananart fikrini takiben hiçbir şey yok etmemeye çalışıyorum. (çok fazla sihir olduğu için)

class Person < Contact
  model_name.class_eval do
    def route_key
     "contacts"
    end
    def singular_route_key
      superclass.model_name.singular_route_key
    end
  end
end

Artık url_for @person, beklenen şekilde contact_path ile eşleşecek.


14

Ben de bu problemle ilgili sorun yaşıyordum ve bu cevaba bizimkine benzer bir soru üzerine geldim. Benim için çalıştı.

form_for @list.becomes(List)

Burada gösterilen cevap: Aynı denetleyiciyle STI yolunu kullanma

.becomesEsas senin gibi STI problemleri çözmek için kullanıldığı gibi bir yöntem tanımlanır form_forbiri.

.becomesBurada bilgi: http://apidock.com/rails/ActiveRecord/Base/becomes

Süper geç cevap, ama bu bulabildiğim en iyi cevap ve benim için iyi çalıştı. Umarım bu biraz yardımcı olur. Şerefe!


5

Tamam, bu Raylar alanında bir ton hayal kırıklığı yaşadım ve aşağıdaki yaklaşıma vardım, belki bu başkalarına yardımcı olacaktır.

Öncelikle, ağın üstünde ve etrafında bir dizi çözümün, müşteri tarafından sağlanan parametrelerde sabitleştirmeyi önerdiğini unutmayın. Bu, Ruby'nin sembolleri çöp toplamaması nedeniyle bir saldırganın rastgele semboller oluşturmasına ve kullanılabilir belleği kullanmasına izin verdiği için bilinen bir DoS saldırı vektörüdür.

Aşağıda, model alt sınıflarının örneklenmesini destekleyen ve yukarıdaki contantize probleminden GÜVENLİ olan yaklaşımı uyguladım. Rayların 4 yaptıklarına çok benzer, ancak aynı zamanda birden fazla alt sınıflamaya izin verir (Raylar 4'ün aksine) ve Raylar 3'te çalışır.

# initializers/acts_as_castable.rb
module ActsAsCastable
  extend ActiveSupport::Concern

  module ClassMethods

    def new_with_cast(*args, &block)
      if (attrs = args.first).is_a?(Hash)
        if klass = descendant_class_from_attrs(attrs)
          return klass.new(*args, &block)
        end
      end
      new_without_cast(*args, &block)
    end

    def descendant_class_from_attrs(attrs)
      subclass_name = attrs.with_indifferent_access[inheritance_column]
      return nil if subclass_name.blank? || subclass_name == self.name
      unless subclass = descendants.detect { |sub| sub.name == subclass_name }
        raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
      end
      subclass
    end

    def acts_as_castable
      class << self
        alias_method_chain :new, :cast
      end
    end
  end
end

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

Yukarıda önerilenlere benzer birçok 'alt sınıfta yükleme sorunu' için çeşitli yaklaşımlar denedikten sonra, güvenilir çalıştı tek şey model sınıflarım 'requir_dependency' kullanmak olduğunu buldum. Bu, sınıf yüklemesinin geliştirme sırasında düzgün çalışmasını sağlar ve üretimde sorun yaratmaz. Geliştirmede, 'requir_dependency' olmadan AR, tür sütununda eşleştirme için yayılan SQL'i etkileyen tüm alt sınıfları bilmeyecektir. Buna ek olarak, 'requir_dependency' olmadan, aynı anda model sınıflarının birden fazla sürümüyle karşılaşabilirsiniz! (örneğin, bir temel veya ara sınıfı değiştirdiğinizde bu olabilir, alt sınıflar her zaman yeniden yüklenmez ve eski sınıftan alt sınıf bırakılır)

# contact.rb
class Contact < ActiveRecord::Base
  acts_as_castable
end

require_dependency 'person'
require_dependency 'organisation'

I18n kullandığım ve farklı alt sınıfların öznitelikleri için farklı dizelere ihtiyacım olduğu için yukarıda önerilen model_adı geçersiz kılmıyorum, örneğin: tax_identifier Kuruluş için 'ABN' ve Kişi için 'TFN' (Avustralya'da) olur.

Ayrıca, yukarıda belirtildiği gibi, türü ayarlayarak rota eşlemeyi kullanıyorum:

resources :person, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Person.sti_name } }
resources :organisation, :controller => 'contacts', :defaults => { 'contact' => { 'type' => Organisation.sti_name } }

Rota eşlemesine ek olarak, InheritedResources ve SimpleForm kullanıyorum ve yeni eylemler için aşağıdaki genel form sarmalayıcısını kullanıyorum:

simple_form_for resource, as: resource_request_name, url: collection_url,
      html: { class: controller_name, multipart: true }

... ve düzenleme işlemleri için:

simple_form_for resource, as: resource_request_name, url: resource_url,
      html: { class: controller_name, multipart: true }

Ve bu iş yapmak için, benim temel ResourceContoller içinde görünüm için yardımcı bir yöntem olarak InheritedResource'un resource_request_name maruz:

helper_method :resource_request_name 

InheritedResources kullanmıyorsanız, 'ResourceController' öğesinde aşağıdakine benzer bir şey kullanın:

# controllers/resource_controller.rb
class ResourceController < ApplicationController

protected
  helper_method :resource
  helper_method :resource_url
  helper_method :collection_url
  helper_method :resource_request_name

  def resource
    @model
  end

  def resource_url
    polymorphic_path(@model)
  end

  def collection_url
    polymorphic_path(Model)
  end

  def resource_request_name
    ActiveModel::Naming.param_key(Model)
  end
end

Her zaman başkalarının deneyimlerini ve iyileştirmelerini duymaktan mutluluk duyarız.


Deneyimlerime göre (en azından Rails 3.0.9'da), string tarafından adlandırılan sabit zaten mevcut değilse constantize başarısız olur. Peki gelişigüzel yeni semboller oluşturmak için nasıl kullanılabilir?
Lex Lindsey

4

Geçenlerde bir Rails 3.0 uygulamasında çalışan kararlı bir STI kalıbı elde etme girişimlerimi belgeledim . İşte TL; DR versiyonu:

# app/controllers/kase_controller.rb
class KasesController < ApplicationController

  def new
    setup_sti_model
    # ...
  end

  def create
    setup_sti_model
    # ...
  end

private

  def setup_sti_model
    # This lets us set the "type" attribute from forms and querystrings
    model = nil
    if !params[:kase].blank? and !params[:kase][:type].blank?
      model = params[:kase].delete(:type).constantize.to_s
    end
    @kase = Kase.new(params[:kase])
    @kase.type = model
  end
end

# app/models/kase.rb
class Kase < ActiveRecord::Base
  # This solves the `undefined method alpha_kase_path` errors
  def self.inherited(child)
    child.instance_eval do
      def model_name
        Kase.model_name
      end
    end
    super
  end  
end

# app/models/alpha_kase.rb
# Splitting out the subclasses into separate files solves
# the `uninitialize constant AlphaKase` errors
class AlphaKase < Kase; end

# app/models/beta_kase.rb
class BetaKase < Kase; end

# config/initializers/preload_sti_models.rb
if Rails.env.development?
  # This ensures that `Kase.subclasses` is populated correctly
  %w[kase alpha_kase beta_kase].each do |c|
    require_dependency File.join("app","models","#{c}.rb")
  end
end

Bu yaklaşım, listelediğiniz sorunların yanı sıra, diğerlerinin CYBE yaklaşımlarıyla ilgili sahip olduğu diğer sorunların bir kısmını ele alır.


2

İç içe rotanız yoksa bunu deneyebilirsiniz:

resources :employee, path: :person, controller: :person

Ya da başka bir şekilde gidebilir ve burada açıklandığı gibi bir OOP büyüsü kullanabilirsiniz: https://coderwall.com/p/yijmuq

İkinci şekilde, tüm iç içe geçmiş modelleriniz için benzer yardımcılar yapabilirsiniz.


2

İşte formlarda ve kullandığımız uygulama boyunca çalışmasını sağlamak için güvenli ve temiz bir yol.

resources :districts
resources :district_counties, controller: 'districts', type: 'County'
resources :district_cities, controller: 'districts', type: 'City'

Sonra formumda var. Bunun için eklenen parça şudur: ilçe.

= form_for(@district, as: :district, html: { class: "form-horizontal",         role: "form" }) do |f|

Bu yardımcı olur umarım.


2

Bulduğum en temiz çözüm, temel sınıfa aşağıdakileri eklemektir:

def self.inherited(subclass)
  super

  def subclass.model_name
    super.tap do |name|
      route_key = base_class.name.underscore
      name.instance_variable_set(:@singular_route_key, route_key)
      name.instance_variable_set(:@route_key, route_key.pluralize)
    end
  end
end

Tüm alt sınıflar için çalışır ve tüm model adı nesnesini geçersiz kılmaktan çok daha güvenlidir. Yalnızca rota anahtarlarını hedefleyerek, yönlendirme sorunlarını I18n'yi kırmadan veya Rails tarafından tanımlanan model adının geçersiz kılmasının neden olabileceği olası yan etkileri riske atmadan çözeriz.


1

Böyle bir CYBE mirasını düşünürsem:

class AModel < ActiveRecord::Base ; end
class BModel < AModel ; end
class CModel < AModel ; end
class DModel < AModel ; end
class EModel < AModel ; end

'app / models / a_model.rb' içine ekliyorum:

module ManagedAtAModelLevel
  def model_name
    AModel.model_name
  end
end

Ve sonra AModel sınıfında:

class AModel < ActiveRecord::Base
  def self.instanciate_STI
    managed_deps = { 
      :b_model => true,
      :c_model => true,
      :d_model => true,
      :e_model => true
    }
    managed_deps.each do |dep, managed|
      require_dependency dep.to_s
      klass = dep.to_s.camelize.constantize
      # Inject behavior to be managed at AModel level for classes I chose
      klass.send(:extend, ManagedAtAModelLevel) if managed
    end
  end

  instanciate_STI
end

Bu nedenle, varsayılan modeli kullanmak istediğim modeli kolayca seçebilirim ve bu da alt sınıf tanımına dokunmadan. Çok kuru.


1

Bu şekilde benim için çalışıyor tamam (bu yöntemi temel sınıfta tanımlayın):

def self.inherited(child)
  child.instance_eval do
    alias :original_model_name :model_name
    def model_name
      Task::Base.model_name
    end
  end
  super
end

1

Amaç yönlendirme için kukla Üst nesne döndüren yöntem oluşturabilirsiniz

class Person < ActiveRecord::Base      
  def routing_object
    Person.new(id: id)
  end
end

ve sonra form olmadan Person sınıfı nesnesini döndürecek form_for @ worker.routing_object öğesini çağırmanız yeterlidir.


1

@ Prathan-thananart cevabını takip ederek ve çoklu STI sınıfları için, üst modele aşağıdakileri ekleyebilirsiniz ->

class Contact < ActiveRecord::Base
  def self.model_name
    ActiveModel::Name.new(self, nil, 'Contact')
  end
end

Yani İletişim verilerle her form olarak params'ı göndermek yapacaktır params[:contact]yerine params[:contact_person], params[:contact_whatever].


-6

hackish, ama çözümler listesine sadece bir tane daha.

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

2.x ve 3.x raylarında çalışır


5
Bu bir sorunu giderir, ancak başka bir sorun yaratır. Şimdi bunu yapmaya çalıştığınızda alt sınıf yerine Child.newbir Parentsınıf döndürür . Bu type, type özniteliğini de açıkça ayarlamadığınız sürece , alt sınıfları bir denetleyici aracılığıyla toplu atama yoluyla oluşturamayacağınız anlamına gelir ( varsayılan olarak korumalı bir öznitelik olduğu için).
Chris Bloom
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.