Praktik terbaik untuk menangani rute untuk subclass IMS di rel

175

Tampilan dan pengontrol Rails saya dipenuhi redirect_to,link_to , dan form_formetode panggilan. Kadang link_to- kadang dan redirect_toeksplisit di jalur yang mereka tautkan (misalnya link_to 'New Person', new_person_path), tetapi berkali-kali jalur tersebut tersirat (misalnya link_to 'Show', person).

Saya menambahkan beberapa tabel pewarisan tunggal (STI) ke model saya (katakanlah Employee < Person), dan semua metode ini istirahat untuk turunan dari subclass (katakanlah Employee); ketika rel dieksekusi link_to @person, kesalahan dengan undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. Rails mencari rute yang ditentukan oleh nama kelas objek, yaitu karyawan. Rute karyawan ini tidak ditentukan, dan tidak ada pengontrol karyawan sehingga tindakannya juga tidak ditentukan.

Pertanyaan ini telah diajukan sebelumnya:

  1. Di StackOverflow , jawabannya adalah mengedit setiap instance dari link_to dll di seluruh basis kode Anda, dan nyatakan path secara eksplisit
  2. Di StackOverflow lagi, dua orang menyarankan menggunakan routes.rbuntuk memetakan sumber daya subclass ke kelas induk (map.resources :employees, :controller => 'people' ). Jawaban teratas dalam pertanyaan SO yang sama menyarankan tipe-casting setiap instance objek dalam basis kode menggunakan.becomes
  3. Namun satu lagi di StackOverflow , jawaban teratas adalah cara di kamp Do Repeat Yourself, dan menyarankan untuk membuat duplikat perancah untuk setiap subclass.
  4. Ini dia pertanyaan yang sama lagi di SO, di mana jawaban teratas sepertinya salah (Rails magic Just Works!)
  5. Di tempat lain di web, saya menemukan posting blog ini mana F2Andy merekomendasikan pengeditan di jalur mana pun dalam kode.
  6. Pada posting blog Single Table Inheritance dan RESTful Routes at Logical Reality Design, direkomendasikan untuk memetakan sumber daya untuk subclass ke pengontrol superclass, seperti pada jawaban SO nomor 2 di atas.
  7. Alex Reisner memiliki posting Single Table Inheritance in Rails , di mana ia menganjurkan untuk tidak memetakan sumber daya dari kelas anak ke kelas induk di routes.rb, karena itu hanya menangkap kerusakan rute dari link_todan redirect_to, tetapi tidak dari form_for. Jadi dia merekomendasikan sebagai gantinya menambahkan metode ke kelas induk untuk membuat subclass berbohong tentang kelas mereka. Kedengarannya bagus, tetapi metodenya memberi saya kesalahan undefined local variable or method `child' for #.

Jadi jawaban yang tampaknya paling elegan dan memiliki paling konsensus (tapi itu tidak semua yang elegan, atau yang banyak konsensus), adalah menambah sumber daya untuk Anda routes.rb. Kecuali ini tidak berhasil form_for. Saya perlu kejelasan! Untuk menyaring pilihan di atas, opsi saya adalah

  1. memetakan sumber daya dari subclass ke controller dari superclass di routes.rb(dan harap saya tidak perlu memanggil form_for pada subclass mana pun)
  2. Mengganti metode internal rel untuk membuat kelas berbohong satu sama lain
  3. Edit setiap instance dalam kode di mana path ke tindakan objek dipanggil secara implisit atau eksplisit, baik mengubah path atau ketik-casting objek.

Dengan semua jawaban yang saling bertentangan ini, saya perlu keputusan. Bagi saya sepertinya tidak ada jawaban yang baik. Apakah ini gagal dalam desain rel? Jika demikian, apakah itu bug yang mungkin diperbaiki? Atau jika tidak, maka saya berharap seseorang dapat meluruskan hal ini, memandu saya melalui pro dan kontra dari setiap opsi (atau menjelaskan mengapa itu bukan pilihan), dan mana yang merupakan jawaban yang tepat, dan mengapa. Atau ada jawaban yang tepat yang tidak saya temukan di web?

ziggurisme
sumber
1
Ada kesalahan ketik pada kode Alex Reisner, yang dia perbaiki setelah saya berkomentar di blognya. Jadi mudah-mudahan sekarang solusi Alex layak. Pertanyaan saya masih tetap: solusi mana yang tepat?
ziggurism
1
Meskipun usianya sekitar tiga tahun, saya menemukan posting blog ini di rookieonrails.blogspot.com/2008/01/… dan percakapan yang ditautkan dari milis rel informatif. Salah satu responden menjelaskan perbedaan antara penolong polimorfik dan penolong yang diberi nama.
ziggurism
2
Opsi yang tidak Anda daftarkan adalah menambal Rails sehingga link_to, form_for dan tempat sejenisnya bagus dengan pewarisan tabel tunggal. Itu mungkin pekerjaan yang sulit, tetapi itu adalah sesuatu yang saya ingin lihat diperbaiki.
M. Scott Ford

Jawaban:

140

Ini adalah solusi paling sederhana yang saya dapat buat dengan efek samping minimal.

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

Sekarang url_for @personakan dipetakan contact_pathsesuai yang diharapkan.

Cara kerjanya: Pembantu URL mengandalkan YourModel.model_nameuntuk merefleksikan model dan menghasilkan (di antara banyak hal) kunci rute tunggal / jamak. Di sini Personpada dasarnya mengatakan aku seperti Contactpria, tanyakan padanya .

Prathan Thananart
sumber
4
Saya berpikir untuk melakukan hal yang sama, tetapi khawatir bahwa #model_name mungkin digunakan di tempat lain di Rails, dan bahwa perubahan ini dapat mengganggu fungsi normal. Adakah pikiran?
nkassis
3
Saya sangat setuju dengan orang asing misterius @ nkassis. Ini adalah retasan keren, tetapi bagaimana Anda tahu Anda tidak merusak internal rails?
tsherif
6
Spesifikasi. Juga, kami menggunakan kode ini dalam produksi, dan saya dapat membuktikan bahwa itu tidak mengacaukan: 1) hubungan antar-model, 2) Instansiasi model STI (via build_x/ create_x). Di sisi lain, harga bermain sihir adalah Anda tidak pernah 100% yakin apa yang dapat berubah.
Prathan Thananart
10
Ini memecah i18n jika Anda mencoba memiliki nama manusia yang berbeda untuk atribut tergantung pada kelas.
Rufo Sanchez
4
Daripada benar-benar mengesampingkan seperti ini, Anda bisa mengganti bit yang Anda butuhkan. Lihat gist.github.com/sj26/5843855
sj26
47

Saya memiliki masalah yang sama. Setelah menggunakan IMS, form_formetode memposting ke url anak yang salah.

NoMethodError (undefined method `building_url' for

Saya akhirnya menambahkan rute tambahan untuk kelas anak dan mengarahkan mereka ke pengontrol yang sama

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

Selain itu:

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

dalam hal ini struktur sebenarnya adalah sebuah bangunan (kelas anak)

Tampaknya bekerja untuk saya setelah melakukan pengiriman dengan form_for.

James
sumber
2
Ini berfungsi, tetapi menambahkan banyak jalur yang tidak perlu di rute kami. Apakah tidak ada cara untuk melakukan ini dengan cara yang tidak terlalu mengganggu?
Anders Kindberg
1
Anda dapat mengatur rute secara programatik dalam file route.rb Anda, sehingga Anda bisa melakukan sedikit pemrograman meta untuk mengatur rute anak. Namun, di lingkungan di mana kelas tidak di-cache (misalnya pengembangan), Anda perlu memuat kelas terlebih dahulu. Jadi satu atau lain cara Anda perlu menentukan subclass di suatu tempat. Lihat gist.github.com/1713398 untuk contoh.
Chris Bloom
Dalam kasus saya, mengekspos nama objek (path) ke pengguna tidak diinginkan (dan membingungkan bagi pengguna).
laffuste
33

Saya sarankan Anda melihat di: https://stackoverflow.com/a/605172/445908 , menggunakan metode ini akan memungkinkan Anda untuk menggunakan "form_for".

ActiveRecord::Base#becomes
Siwei Shen 申思维
sumber
2
Saya harus mengatur url secara eksplisit agar URL membuat dari dan menyimpan dengan benar. <%= form_for @child, :as => :child, url: @child.becomes(Parent)
lulalala
4
@lulalala Try<%= form_for @child.becomes(Parent)
Richard Jones
14

Mengikuti gagasan @Prathan Thananart tetapi berusaha untuk tidak menghancurkan apa pun. (Karena ada begitu banyak sihir yang terlibat)

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

Sekarang url_for @person akan memetakan ke contact_path seperti yang diharapkan.

eloyesp
sumber
14

Saya mengalami masalah dengan masalah ini juga dan datang dengan jawaban ini pada pertanyaan yang mirip dengan kita. Ini berhasil untuk saya.

form_for @list.becomes(List)

Jawaban ditunjukkan di sini: Menggunakan jalur IMS dengan pengontrol yang sama

The .becomesMetode didefinisikan sebagai terutama digunakan untuk memecahkan masalah IMS seperti Anda form_forsatu.

.becomesinfo di sini: http://apidock.com/rails/ActiveRecord/Base/becomes

Respons yang sangat terlambat, tetapi ini adalah jawaban terbaik yang bisa saya temukan dan itu bekerja dengan baik untuk saya. Semoga ini bisa membantu seseorang. Bersulang!

Marcus
sumber
5

Ok, saya punya banyak frustrasi di area Rails ini, dan telah sampai pada pendekatan berikut, mungkin ini akan membantu orang lain.

Pertama-tama perlu diketahui bahwa sejumlah solusi di atas dan di sekitar internet menyarankan penggunaan constantize pada parameter yang disediakan klien. Ini adalah vektor serangan DoS yang dikenal karena Ruby tidak membuang simbol pengumpulan, sehingga memungkinkan penyerang untuk membuat simbol sewenang-wenang dan menggunakan memori yang tersedia.

Saya telah menerapkan pendekatan di bawah ini yang mendukung instantiation model subclass, dan AMAN dari masalah contantize di atas. Ini sangat mirip dengan apa yang dilakukan rails 4, tetapi juga memungkinkan lebih dari satu level subclassing (tidak seperti Rails 4) dan bekerja di Rails 3.

# 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)

Setelah mencoba berbagai pendekatan untuk 'pemuatan subclass dalam masalah devlopment' banyak mirip dengan apa yang disarankan di atas, saya menemukan satu-satunya hal yang bekerja dengan andal adalah dengan menggunakan 'memerlukan_dependensi' di kelas model saya. Ini memastikan bahwa pemuatan kelas berfungsi dengan baik dalam pengembangan dan tidak menyebabkan masalah dalam produksi. Dalam pengembangan, tanpa AR 'memerlukan_dependensi' tidak akan tahu tentang semua subclass, yang berdampak pada SQL yang dikeluarkan untuk pencocokan pada kolom tipe. Selain itu tanpa 'memerlukan_dependensi' Anda juga dapat berakhir dalam situasi dengan beberapa versi kelas model pada saat yang sama! (mis. ini bisa terjadi ketika Anda mengubah kelas dasar atau menengah, sub-kelas sepertinya tidak selalu dimuat ulang dan dibiarkan subclass dari kelas lama)

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

require_dependency 'person'
require_dependency 'organisation'

Saya juga tidak menimpa model_name seperti yang disarankan di atas karena saya menggunakan I18n dan memerlukan string yang berbeda untuk atribut subkelas yang berbeda, misalnya: tax_identifier menjadi 'ABN' untuk Organisasi, dan 'TFN' untuk Orang (di Australia).

Saya juga menggunakan pemetaan rute, seperti yang disarankan di atas, mengatur tipe:

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

Selain pemetaan rute, saya menggunakan InheritedResources dan SimpleForm dan saya menggunakan pembungkus formulir umum berikut untuk tindakan baru:

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

... dan untuk tindakan edit:

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

Dan untuk membuat ini bekerja, di basis saya ResourceContoller saya mengekspos resource_request_name InheritedResource sebagai metode pembantu untuk tampilan:

helper_method :resource_request_name 

Jika Anda tidak menggunakan InheritedResources, gunakan sesuatu seperti yang berikut ini di 'ResourceController' Anda:

# 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

Selalu senang mendengar pengalaman dan peningkatan orang lain.

Andrew Hacking
sumber
Dalam pengalaman saya (setidaknya di Rails 3.0.9), konstanta gagal jika konstanta bernama oleh string belum ada. Jadi bagaimana bisa digunakan untuk membuat simbol baru yang sewenang-wenang?
Lex Lindsey
4

Baru-baru ini saya mendokumentasikan upaya saya untuk mendapatkan pola IMS stabil yang bekerja di aplikasi Rails 3.0. Inilah versi TL; DR:

# 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

Pendekatan ini mengatasi masalah yang Anda daftarkan dan juga sejumlah masalah lain yang dimiliki orang lain dengan pendekatan IMS.

Chris Bloom
sumber
2

Anda dapat mencoba ini, jika Anda tidak memiliki rute bersarang:

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

Atau Anda bisa pergi ke arah lain dan menggunakan sihir OOP seperti yang dijelaskan di sini: https://coderwall.com/p/yijmuq

Dengan cara kedua Anda bisa membuat pembantu serupa untuk semua model bersarang Anda.

everm1nd
sumber
2

Ini adalah cara bersih dan aman untuk membuatnya berfungsi dalam bentuk dan di seluruh aplikasi Anda yang kami gunakan.

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

Kemudian saya miliki dalam bentuk saya. Bagian yang ditambahkan untuk ini adalah sebagai:: distrik.

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

Semoga ini membantu.

Jake
sumber
2

Solusi terbersih yang saya temukan adalah menambahkan yang berikut ke kelas dasar:

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

Ini bekerja untuk semua subclass dan jauh lebih aman daripada menimpa seluruh objek nama model. Dengan hanya menargetkan kunci rute, kami menyelesaikan masalah perutean tanpa melanggar I18n atau mempertaruhkan potensi efek samping yang disebabkan oleh mengganti nama model seperti yang didefinisikan oleh Rails.

Alexander Makarenko
sumber
1

Jika saya mempertimbangkan warisan IMS seperti ini:

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

di 'app / models / a_model.rb' saya tambahkan:

module ManagedAtAModelLevel
  def model_name
    AModel.model_name
  end
end

Dan kemudian di kelas AModel:

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

Oleh karena itu saya bahkan dapat dengan mudah memilih model mana yang ingin saya gunakan yang default, dan ini bahkan tanpa menyentuh definisi sub-kelas. Sangat kering.

Laurent B.
sumber
1

Cara ini berfungsi untuk saya ok (tentukan metode ini di kelas dasar):

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
ka8725
sumber
1

Anda dapat membuat metode yang mengembalikan objek dummy Parent untuk tujuan routing

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

dan kemudian cukup panggil form_for @ employee.routing_object yang tanpa tipe akan mengembalikan objek kelas Person

bergaul
sumber
1

Mengikuti @ prathan-thananart jawaban, dan untuk beberapa kelas IMS, Anda dapat menambahkan yang berikut ke model induk ->

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

Yang akan membuat setiap form dengan data Hubungi untuk mengirim params sebagai params[:contact]bukan params[:contact_person], params[:contact_whatever].

Zlatko Alomerovic
sumber
-6

meretas, tetapi hanya satu lagi ke daftar solusi.

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

bekerja pada rel 2.x dan 3.x

Ohad Levy
sumber
5
Ini memperbaiki satu masalah, tetapi membuat yang lain. Sekarang ketika Anda mencoba melakukannya Child.newmengembalikan Parentkelas daripada subclass. Ini berarti bahwa Anda tidak dapat membuat subclass melalui penugasan massal melalui pengontrol (karena typemerupakan atribut yang dilindungi secara default) kecuali Anda juga mengatur atribut type secara eksplisit.
Chris Bloom