Thực tiễn tốt nhất để xử lý các tuyến đường cho các lớp con STI trong đường ray


175

Rails quan điểm và điều khiển của tôi được rải rác với redirect_to, link_toform_forcác cuộc gọi phương pháp. Đôi khi link_toredirect_torõ ràng trong các đường dẫn mà chúng đang liên kết (ví dụ link_to 'New Person', new_person_path), nhưng nhiều lần các đường dẫn được ẩn (ví dụ link_to 'Show', person).

Tôi thêm một số kế thừa bảng đơn (STI) vào mô hình của tôi (giả sử Employee < Person) và tất cả các phương thức này đều phá vỡ một thể hiện của lớp con (giả sử Employee); khi đường ray thực thi link_to @person, nó lỗi với undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. Rails đang tìm kiếm một tuyến đường được xác định bởi tên lớp của đối tượng, đó là nhân viên. Các tuyến nhân viên này không được xác định và không có bộ điều khiển nhân viên nên các hành động cũng không được xác định.

Câu hỏi này đã được hỏi trước đây:

  1. Tại StackOverflow , câu trả lời là chỉnh sửa mọi phiên bản của link_to, v.v. trong toàn bộ cơ sở mã của bạn và nêu rõ đường dẫn
  2. Trên StackOverflow một lần nữa, hai người đề nghị sử dụng routes.rbđể ánh xạ các tài nguyên của lớp con sang lớp cha ( map.resources :employees, :controller => 'people'). Câu trả lời hàng đầu trong cùng câu hỏi SO đó gợi ý việc tạo kiểu cho mọi đối tượng thể hiện trong cơ sở mã bằng cách sử dụng.becomes
  3. Còn một câu hỏi khác tại StackOverflow , câu trả lời hàng đầu là cách trong trại Do Lặp lại chính mình và gợi ý tạo giàn giáo trùng lặp cho mỗi lớp con.
  4. Đây là câu hỏi tương tự một lần nữa tại SO, trong đó câu trả lời hàng đầu dường như chỉ sai (Rails Magic Just Works!)
  5. Ở những nơi khác trên web, tôi tìm thấy bài đăng trên blog này trong đó F2Andy khuyên bạn nên chỉnh sửa trong đường dẫn ở mọi nơi trong mã.
  6. Trên bài đăng trên blog Kế thừa bảng đơn và các tuyến RESTful tại Thiết kế thực tế logic, nên ánh xạ các tài nguyên cho lớp con tới bộ điều khiển siêu lớp, như trong câu trả lời SO số 2 ở trên.
  7. Alex Reisner có một bài đăng Kế thừa bảng đơn trong Rails , trong đó ông chủ trương chống lại việc ánh xạ tài nguyên của các lớp con sang lớp cha mẹ routes.rb, vì điều đó chỉ bắt các ngắt định tuyến từ link_toredirect_to, chứ không phải từ form_for. Vì vậy, ông đề nghị thay vì thêm một phương thức vào lớp cha để có được các lớp con nói dối về lớp của chúng. Nghe có vẻ hay, nhưng phương pháp của anh ấy đã cho tôi lỗi undefined local variable or method `child' for #.

Vì vậy, câu trả lời mà dường như thanh lịch nhất và có sự đồng thuận nhất (nhưng nó không phải là tất cả tao nhã, cũng không phải nhiều sự đồng thuận), là thêm các nguồn lực để bạn routes.rb. Ngoại trừ điều này không làm việc cho form_for. Tôi cần một chút rõ ràng! Để chắt lọc các lựa chọn ở trên, các tùy chọn của tôi là

  1. ánh xạ tài nguyên của lớp con tới bộ điều khiển của lớp cha trong routes.rb(và hy vọng tôi không cần gọi form_for trên bất kỳ lớp con nào)
  2. Ghi đè các phương thức nội bộ đường ray để làm cho các lớp nằm với nhau
  3. Chỉnh sửa mọi phiên bản trong mã trong đó đường dẫn đến hành động của đối tượng được gọi ngầm hoặc rõ ràng, thay đổi đường dẫn hoặc truyền kiểu đối tượng.

Với tất cả những câu trả lời mâu thuẫn này, tôi cần một phán quyết. Dường như với tôi như không có câu trả lời tốt. Đây có phải là một thất bại trong thiết kế đường ray? Nếu vậy, nó có phải là một lỗi có thể được sửa chữa? Hoặc nếu không, thì tôi hy vọng ai đó có thể giúp tôi giải quyết vấn đề này, hướng dẫn tôi những ưu và nhược điểm của từng lựa chọn (hoặc giải thích tại sao đó không phải là một lựa chọn), và câu trả lời nào là đúng và tại sao. Hoặc có một câu trả lời đúng mà tôi không tìm thấy trên web?


1
Có một lỗi đánh máy trong mã của Alex Reisner, anh ấy đã sửa lỗi sau khi tôi nhận xét trên blog của anh ấy. Vì vậy, hy vọng bây giờ giải pháp của Alex là khả thi. Câu hỏi của tôi vẫn đứng: đó là giải pháp đúng?
ziggurism

1
Mặc dù khoảng ba tuổi, tôi đã tìm thấy bài đăng trên blog này tại rookieonrails.blogspot.com/2008/01/NH và cuộc trò chuyện được liên kết từ danh sách gửi thư đường ray thông tin. Một trong những người trả lời mô tả sự khác biệt giữa người trợ giúp đa hình và người trợ giúp có tên.
ziggurism

2
Một tùy chọn mà bạn không liệt kê là vá Rails sao cho link_to, form_for và vị trí tương tự đẹp với kế thừa bảng đơn. Đó có thể là một công việc khó khăn, nhưng đó là điều mà tôi rất muốn thấy cố định.
M. Scott Ford

Câu trả lời:


140

Đây là giải pháp đơn giản nhất tôi có thể đưa ra với tác dụng phụ tối thiểu.

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

Bây giờ url_for @personsẽ ánh xạ đến contact_pathnhư mong đợi.

Cách thức hoạt động: Trình trợ giúp URL dựa vào YourModel.model_nameđể phản ánh mô hình và tạo (trong số nhiều thứ) các khóa tuyến số ít / số nhiều. Dưới đây Personlà về cơ bản nói tôi giống như Contactanh chàng, hỏi anh ta .


4
Tôi đã nghĩ làm điều tương tự, nhưng lo lắng rằng #model_name có thể được sử dụng ở nơi khác trong Rails và thay đổi này có thể cản trở hoạt động bình thường. Có suy nghĩ gì không?
nkrame

3
Tôi hoàn toàn đồng ý với người lạ bí ẩn @nkrame. Đây là một hack tuyệt vời, nhưng làm thế nào để bạn biết bạn không làm hỏng nội bộ của đường ray?
tsherif

6
Thông số kỹ thuật. Ngoài ra, chúng tôi sử dụng mã này trong sản xuất và tôi có thể chứng thực rằng nó không gây rối: 1) mối quan hệ giữa các mô hình, 2) khởi tạo mô hình STI (thông qua build_x/ create_x). Mặt khác, giá chơi với ma thuật là bạn không bao giờ chắc chắn 100% những gì có thể thay đổi.
Prathan Thananart

10
Điều này phá vỡ i18n nếu bạn đang cố gắng có các tên người khác nhau cho các thuộc tính tùy thuộc vào lớp.
Rufo Sanchez

4
Thay vì ghi đè hoàn toàn như thế này, bạn chỉ có thể ghi đè các bit bạn cần. Xem gist.github.com/sj26/5843855
sj26

47

Tôi đã từng gặp vấn đề tương tự. Sau khi sử dụng STI, form_forphương pháp đã đăng sai url con.

NoMethodError (undefined method `building_url' for

Cuối cùng tôi đã thêm vào các tuyến bổ sung cho các lớp con và chỉ chúng vào cùng các bộ điều khiển

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

Ngoài ra:

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

trong trường hợp này cấu trúc thực sự là một tòa nhà (lớp con)

Nó dường như làm việc cho tôi sau khi gửi với form_for.


2
Điều này hoạt động, nhưng thêm rất nhiều đường dẫn không cần thiết trong các tuyến đường của chúng tôi. Không có cách nào để làm điều này theo cách ít xâm phạm hơn sao?
Anders Kindberg

1
Bạn có thể thiết lập các tuyến theo chương trình trong tệp Rout.rb của mình, vì vậy bạn có thể thực hiện một chút lập trình meta để thiết lập các tuyến con. Tuy nhiên, trong các môi trường nơi các lớp không được lưu trong bộ nhớ cache (ví dụ: phát triển), bạn cần tải trước các lớp trước. Vì vậy, bằng cách này hay cách khác bạn cần chỉ định các lớp con ở đâu đó. Xem gist.github.com/1713398 để biết ví dụ.
Chris Bloom

Trong trường hợp của tôi, việc để lộ tên đối tượng (đường dẫn) cho người dùng là không mong muốn (và gây nhầm lẫn cho người dùng).
laffuste

33

Tôi khuyên bạn nên xem tại: https://stackoverflow.com/a/605172/445908 , sử dụng phương pháp này sẽ cho phép bạn sử dụng "form_for".

ActiveRecord::Base#becomes

2
Tôi đã phải đặt url một cách rõ ràng để nó vừa hiển thị từ lưu đúng cách. <%= form_for @child, :as => :child, url: @child.becomes(Parent)
lulalala

4
@lulalala Hãy thử<%= form_for @child.becomes(Parent)
Richard Jones


14

Theo ý tưởng của @Prathan Thananart nhưng cố gắng không phá hủy gì cả. (vì có quá nhiều phép thuật liên quan)

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

Bây giờ url_for @person sẽ ánh xạ tới contact_path như mong đợi.


14

Tôi cũng gặp rắc rối với vấn đề này và đã nhận được câu trả lời này cho một câu hỏi tương tự như của chúng tôi. Nó làm việc cho tôi.

form_for @list.becomes(List)

Trả lời hiển thị ở đây: Sử dụng đường dẫn STI với cùng bộ điều khiển

Các .becomesphương pháp được định nghĩa là sử dụng chủ yếu để giải quyết vấn đề STI như của bạn form_formột.

.becomesthông tin ở đây: http://apidock.com/rails/ActiveRecord/Base/becomes

Phản hồi siêu muộn, nhưng đây là câu trả lời tốt nhất tôi có thể tìm thấy và nó hoạt động tốt với tôi. Hy vọng điều này sẽ giúp một số người. Chúc mừng!


5

Ok, Ive đã có rất nhiều thất vọng trong lĩnh vực Rails này, và đã đi đến phương pháp sau đây, có lẽ điều này sẽ giúp những người khác.

Trước hết hãy lưu ý rằng một số giải pháp trên và trên mạng gợi ý sử dụng hằng số trên các tham số do khách hàng cung cấp. Đây là một vectơ tấn công DoS đã biết vì Ruby không thu thập các biểu tượng, do đó cho phép kẻ tấn công tạo các biểu tượng tùy ý và tiêu thụ bộ nhớ khả dụng.

Tôi đã thực hiện phương pháp bên dưới hỗ trợ khởi tạo các lớp con mô hình và AN TOÀN từ vấn đề tiếp theo ở trên. Nó rất giống với những gì rails 4 làm, nhưng cũng cho phép nhiều hơn một cấp độ phân lớp (không giống như Rails 4) và hoạt động trong 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)

Sau khi thử các cách tiếp cận khác nhau để 'tải lớp con trong vấn đề phát sinh' tương tự như những gì được đề xuất ở trên, tôi thấy điều duy nhất hoạt động đáng tin cậy là sử dụng 'allow_dependency' trong các lớp mô hình của tôi. Điều này đảm bảo rằng tải lớp hoạt động đúng trong phát triển và không gây ra vấn đề gì trong sản xuất. Trong quá trình phát triển, AR không có 'request_dependency' sẽ không biết về tất cả các lớp con, điều này ảnh hưởng đến SQL được phát ra để khớp trên cột loại. Ngoài ra, không có 'allow_dependency', bạn cũng có thể gặp phải tình huống có nhiều phiên bản của các lớp mô hình cùng một lúc! (ví dụ: điều này có thể xảy ra khi bạn thay đổi một lớp cơ sở hoặc lớp trung gian, các lớp con dường như không luôn tải lại và bị bỏ lại lớp con từ lớp cũ)

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

require_dependency 'person'
require_dependency 'organisation'

Tôi cũng không ghi đè model_name như được đề xuất ở trên vì tôi sử dụng I18n và cần các chuỗi khác nhau cho các thuộc tính của các lớp con khác nhau, ví dụ: tax_identifier trở thành 'ABN' cho Tổ chức và 'TFN' cho Người (ở Úc).

Tôi cũng sử dụng ánh xạ tuyến đường, như được đề xuất ở trên, đặt loại:

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

Ngoài ánh xạ tuyến đường, tôi đang sử dụng InheritedResource và SimpleForm và tôi sử dụng trình bao bọc biểu mẫu chung sau đây cho các hành động mới:

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

... và để chỉnh sửa hành động:

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

Và để làm cho công việc này hoạt động, trong ResourceContoller cơ sở của tôi, tôi đã hiển thị resource_Vquest_name của InheritedResource như một phương thức trợ giúp cho chế độ xem:

helper_method :resource_request_name 

Nếu bạn không sử dụng Nguồn thừa kế, thì hãy sử dụng một cái gì đó như sau trong 'ResourceContoder' của bạ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

Luôn luôn vui khi nghe người khác trải nghiệm và cải tiến.


Theo kinh nghiệm của tôi (ít nhất là trong Rails 3.0.9), hằng số không thành công nếu hằng số được đặt tên bởi chuỗi không tồn tại. Vậy làm thế nào nó có thể được sử dụng để tạo ra các biểu tượng mới tùy ý?
Lex Lindsey

4

Gần đây tôi đã ghi lại những nỗ lực của mình để có được mẫu STI ổn định hoạt động trong ứng dụng Rails 3.0. Đây là phiên bản 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

Cách tiếp cận này xoay quanh các vấn đề mà bạn liệt kê cũng như một số vấn đề khác mà những người khác gặp phải với phương pháp STI.


2

Bạn có thể thử điều này, nếu bạn không có tuyến đường lồng nhau:

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

Hoặc bạn có thể đi một con đường khác và sử dụng một số phép thuật OOP như được mô tả ở đây: https://coderwall.com/p/yijmuq

Theo cách thứ hai, bạn có thể tạo các trình trợ giúp tương tự cho tất cả các mô hình lồng nhau của bạn.


2

Đây là một cách sạch an toàn để làm cho nó hoạt động trong các biểu mẫu và trong suốt ứng dụng của bạn mà chúng tôi sử dụng.

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

Sau đó tôi có trong hình thức của tôi. Các phần được thêm vào cho điều này là như :: quận.

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

Hi vọng điêu nay co ich.


2

Giải pháp sạch nhất tôi tìm thấy là thêm các mục sau vào lớp cơ sở:

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

Nó hoạt động cho tất cả các lớp con và an toàn hơn nhiều so với ghi đè toàn bộ đối tượng tên mô hình. Bằng cách chỉ nhắm mục tiêu các khóa tuyến, chúng tôi giải quyết các vấn đề định tuyến mà không phá vỡ I18n hoặc mạo hiểm với bất kỳ tác dụng phụ tiềm ẩn nào gây ra bằng cách ghi đè tên mô hình như được xác định bởi Rails.


1

Nếu tôi xem xét một thừa kế STI như thế này:

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

trong 'app / model / a_model.rb' tôi thêm:

module ManagedAtAModelLevel
  def model_name
    AModel.model_name
  end
end

Và sau đó trong lớp 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

Do đó, tôi thậm chí có thể dễ dàng chọn mô hình nào tôi muốn sử dụng mô hình mặc định và mô hình này mà không cần chạm vào định nghĩa lớp phụ. Rất khô.


1

Cách này hiệu quả với tôi ok (định nghĩa phương thức này trong lớp cơ sở):

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

Bạn có thể tạo phương thức trả về đối tượng Parent giả để định tuyến purpouse

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

và sau đó chỉ cần gọi form_for @ worker.routing_object mà không có kiểu sẽ trả về đối tượng lớp Person


1

Theo câu trả lời @ prathan-thananart và đối với nhiều lớp STI, bạn có thể thêm phần sau vào mô hình mẹ ->

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

Điều đó sẽ làm cho mỗi biểu mẫu với dữ liệu Liên hệ để gửi thông số params[:contact]thay vì params[:contact_person], params[:contact_whatever].


-6

hackish, nhưng chỉ là một cái khác trong danh sách các giải pháp.

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

hoạt động trên đường ray 2.x và 3.x


5
Điều này sửa một vấn đề, nhưng tạo ra một vấn đề khác. Bây giờ khi bạn cố gắng làm Child.newnó trả về một Parentlớp chứ không phải lớp con. Điều này có nghĩa là bạn không thể tạo các lớp con thông qua việc gán khối lượng thông qua bộ điều khiển (vì đây typelà thuộc tính được bảo vệ theo mặc định) trừ khi bạn cũng đặt thuộc tính loại rõ ràng.
Chris Bloom
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.