철도에서 STI 서브 클래스의 경로를 처리하는 모범 사례


175

내 레일 뷰와 컨트롤러는 뒤덮 redirect_to, link_toform_for메소드 호출. 때때로 link_to그리고 redirect_to그들이 연결하고 경로 (예에서 명시 적입니다 link_to 'New Person', new_person_path)하지만, 여러 번 경로 (예를 암시 적이다 link_to 'Show', person).

모델에 단일 테이블 상속 (STI)을 추가하고 (예 :) Employee < Person이 모든 메소드가 하위 클래스의 인스턴스 (예 :)를 중단합니다 Employee. 레일이 실행 link_to @person되면 오류가 발생합니다 undefined method employee_path' for #<#<Class:0x000001022bcd40>:0x0000010226d038>. Rails는 객체의 클래스 이름 (직원)으로 정의 된 경로를 찾고 있습니다. 이 직원 경로는 정의되어 있지 않으며 직원 컨트롤러가 없으므로 작업도 정의되지 않습니다.

이 질문은 전에 요청되었습니다 :

  1. 에서 StackOverflow의 대답은 전체 코드베이스에 LINK_TO 등의 모든 인스턴스를 편집하고 명시 적 경로를 명시하는 것입니다
  2. StackOverflow에 다시 두 사람이 사용하는 것이 좋습니다 routes.rb부모 클래스의 서브 클래스 리소스 (매핑 map.resources :employees, :controller => 'people'). 같은 SO 질문에서 가장 좋은 대답은 코드베이스의 모든 인스턴스 객체를 사용하여 유형 캐스팅을 제안합니다..becomes
  3. StackOverflow의 또 다른 방법은 Do Repeat Yourself 캠프에서 가장 좋은 방법이며 모든 하위 클래스에 대해 중복 스캐 폴딩을 만드는 것이 좋습니다.
  4. 여기에 다시 같은 질문이 있습니다. 여기서 가장 좋은 대답은 틀린 것 같습니다 (Rails magic Just Works!)
  5. 웹의 다른 곳에서 에서 F2Andy가 코드의 모든 경로에서 편집을 권장하는 이 블로그 게시물을 했습니다 .
  6. 블로그 게시물 단일 테이블 상속 및 RESTful 경로Logical Reality Design 에서 위의 SO 답변 2와 같이 서브 클래스의 리소스를 수퍼 클래스 컨트롤러에 매핑하는 것이 좋습니다.
  7. 알렉스 Reisner는 게시물이 레일에서 단일 테이블 상속 그가에서 상위 클래스에 하위 클래스의 자원을 매핑에 대해 옹호하는, routes.rb만 어획량에서 파손을 라우팅하기 때문에, link_to그리고 redirect_to있지만에서을 form_for. 따라서 하위 클래스가 클래스에 대해 거짓말을 갖도록 부모 클래스에 메소드를 추가하는 것이 좋습니다. 잘 들리지만 그의 방법으로 오류가 발생했습니다 undefined local variable or method `child' for #.

가장 우아한 보인다 가장 공감대를 가지고 (그러나 그것은 모두가 아니다 대답 그래서 우아하고도 많은 합의)는 당신에 리소스를 추가입니다 routes.rb. 이 기능이 작동하지 않는 한form_for . 명확성이 필요합니다! 위의 선택을 증류하기 위해 내 옵션은

  1. 서브 클래스의 자원을 슈퍼 클래스의 컨트롤러에 매핑한다 routes.rb form_for를 호출 할 필요가 없기를 바랍니다)
  2. 클래스가 서로에게 있도록 레일 내부 메소드를 대체
  3. 경로를 변경하거나 객체를 타입 캐스팅하여 객체의 동작 경로가 암시 적 또는 명시 적으로 호출되는 코드의 모든 인스턴스를 편집하십시오.

이 모든 상충되는 답변으로 판결이 필요합니다. 좋은 대답이없는 것 같습니다. 이것이 레일 디자인에서 실패한 것입니까? 그렇다면 수정 될 수있는 버그입니까? 아니면 그렇지 않다면 누군가가 나를 바로 잡을 수 있고 각 옵션의 장단점을 안내하거나 옵션이 아닌 이유를 설명하고 어떤 것이 옳은 답인지, 왜 그런지를 희망합니다. 아니면 웹에서 찾지 못하는 정답이 있습니까?


1
Alex Reisner의 코드에는 오타가 있었으며 블로그에 댓글을 달고 나서 수정되었습니다. 알렉스의 솔루션이 실행 가능하기를 바랍니다. 내 질문은 여전히 ​​선언 : 올바른 솔루션은 무엇입니까?
ziggurism

1
3 년이 되었지만 rookieonrails.blogspot.com/2008/01/… 에서이 블로그 게시물 과 레일스 메일 링리스트에서 정보를 얻을 수있는 관련 대화를 찾았습니다 . 응답자 중 하나는 다형성 헬퍼와 명명 된 헬퍼의 차이점을 설명합니다.
ziggurism

2
나열하지 않는 옵션은 Rails를 패치하여 link_to, form_for 등이 단일 테이블 상속에 적합하도록하는 것입니다. 그것은 힘든 일 일지 모르지만 그것은 내가보고 싶어하는 것입니다.
M. Scott Ford

답변:


140

이것은 부작용을 최소화 할 수있는 가장 간단한 솔루션입니다.

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

이제 예상대로 url_for @person매핑됩니다 contact_path.

작동 방식 : URL 헬퍼 YourModel.model_name는 모델을 반영하고 단수 / 복수 경로 키를 생성하는 데 의존 합니다. 여기는 Person기본적으로 내가 Contact친구 와 같다고 말합니다 .


4
나는 똑같은 일을 생각하고 있었지만 #model_name이 Rails의 다른 곳에서 사용될 수 있고이 변경이 정상적인 기능을 방해 할 수 있다고 걱정했습니다. 이견있는 사람?
nkassis

3
나는 신비한 낯선 사람 @nkassis에 전적으로 동의합니다. 이것은 멋진 해킹이지만 레일의 내부를 파열하지 않는다는 것을 어떻게 알 수 있습니까?
tsherif

6
명세서. 또한이 코드를 프로덕션 환경에서 사용하므로 1) 모델 간 관계, 2) STI 모델 인스턴스화 ( build_x/ 를 통해)가 엉망이되지 않음을 증명할 수 있습니다 create_x. 다른 한편으로, 마법을 가지고 놀 때의 가격은 무엇이 변할 수 있는지 100 % 확신하지 못합니다.
Prathan Thananart

10
클래스에 따라 속성에 다른 사람 이름을 사용하려고하면 i18n이 중단됩니다.
Rufo Sanchez

4
이와 같이 완전히 재정의하는 대신 필요한 비트를 재정의하면됩니다.
sj26

47

나는 같은 문제가 있었다. STI를 사용한 후 form_for메소드가 잘못된 하위 URL에 게시되었습니다.

NoMethodError (undefined method `building_url' for

하위 클래스에 대한 추가 경로를 추가하고 동일한 컨트롤러를 가리 켰습니다.

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

또한 :

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

이 경우 구조는 실제로 건물 (자식)입니다.

님과 함께 제출 한 후 저에게 효과적입니다 form_for.


2
이것은 작동하지만 경로에 불필요한 경로가 많이 추가됩니다. 덜 방해적인 방법으로 이것을 할 수있는 방법이 없습니까?
Anders Kindberg

1
route.rb 파일에서 프로그래밍 방식으로 경로를 설정할 수 있으므로 하위 경로를 설정하기 위해 약간의 메타 프로그래밍을 수행 할 수 있습니다. 그러나 클래스가 캐시되지 않은 환경 (예 : 개발)에서는 먼저 클래스를 미리로드해야합니다. 따라서 어느 쪽이든 다른 방법으로 서브 클래스를 지정해야합니다. 예는 gist.github.com/1713398 을 참조하십시오 .
Chris Bloom

필자의 경우 객체 이름 (경로)을 사용자에게 노출시키는 것은 바람직하지 않습니다 (그리고 사용자를 혼란스럽게합니다).
laffuste

33

: 나는 당신이 살펴 보시기 바랍니다 https://stackoverflow.com/a/605172/445908을 "form_for"사용할 수 있도록 할 것이다이 방법을 사용.

ActiveRecord::Base#becomes

2
나는에서 렌더링 모두에 대한 위해 명시 적으로 URL을 설정했다 제대로 저장합니다. <%= form_for @child, :as => :child, url: @child.becomes(Parent)
lulalala

4
@lulalala Try<%= form_for @child.becomes(Parent)
Richard Jones


14

@Prathan Thananart의 아이디어에 따라 아무것도 파괴하지 마십시오. (매우 많은 마법이 관련되어 있기 때문에)

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

이제 url_for @person은 예상대로 contact_path에 매핑됩니다.


14

나도이 문제에 어려움을 겪고 있었고 우리와 비슷한 질문에 대한 대답이 나왔습니다. 그것은 나를 위해 일했다.

form_for @list.becomes(List)

여기에 표시된 답변 : 동일한 컨트롤러에서 STI 경로 사용

.becomes방법은 주로 STI 문제를 해결하는 데 사용되는 것으로 정의됩니다 form_for.

.becomes여기 정보 : http://apidock.com/rails/ActiveRecord/Base/becomes

매우 늦은 답변이지만 이것이 내가 찾을 수있는 가장 좋은 답변이며 그것은 나에게 잘 작동했습니다. 이것이 도움이되기를 바랍니다. 건배!


5

좋아, Ive는 Rails의이 지역에서 많은 좌절을 겪었고 다음과 같은 접근법에 도달했을 것입니다. 아마도 이것이 다른 사람들을 도울 것입니다.

먼저, 위와 주변의 많은 솔루션에서 클라이언트가 제공 한 매개 변수를 상수로 사용하는 것이 좋습니다. Ruby는 심볼을 가비지 수집하지 않으므로 공격자가 임의의 심볼을 만들고 사용 가능한 메모리를 사용할 수 있으므로 DoS 공격 경로로 알려져 있습니다.

나는 모델 서브 클래스의 인스턴스화를 지원하는 위의 접근 방식을 구현했으며 위의 지속적인 문제로부터 안전합니다. Rails 4의 기능과 매우 유사하지만 Rails 4와 달리 하나 이상의 하위 클래스 수준을 허용하며 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)

위에서 제안한 것과 비슷한 '서브 클래스 로딩의 서브 클래스 로딩'에 대한 다양한 접근법을 시도한 후, 모델 클래스에서 'require_dependency'를 사용하는 것이 안정적으로 작동하는 유일한 방법이라는 것을 알았습니다. 이를 통해 개발시 클래스 로딩이 올바르게 작동하고 프로덕션에 문제가 발생하지 않습니다. 개발시 'require_dependency'가 없으면 AR은 모든 서브 클래스에 대해 알지 못하며 이는 유형 열에서 일치시키기 위해 생성 된 SQL에 영향을줍니다. 또한 'require_dependency'가 없으면 여러 버전의 모델 클래스가 동시에있는 상황에 처할 수도 있습니다! (예 : 기본 또는 중간 클래스를 변경하면 하위 클래스가 항상 다시로드되는 것처럼 보이지 않고 이전 클래스에서 하위 클래스로 남아있는 경우)

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

require_dependency 'person'
require_dependency 'organisation'

또한 I18n을 사용하고 다른 하위 클래스의 속성에 대해 다른 문자열이 필요하기 때문에 위에서 제안한 것처럼 model_name을 재정의하지 않습니다.

위에서 제안한대로 경로 매핑을 사용하여 유형을 설정합니다.

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

경로 매핑 외에도 InheritedResources 및 SimpleForm을 사용하고 있으며 새 작업에 다음과 같은 일반 폼 래퍼를 사용합니다.

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

... 및 편집 작업 :

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

그리고이 작업을 수행하기 위해 기본 ResourceContoller에서 InheritedResource의 resource_request_name을 뷰의 도우미 메소드로 노출합니다.

helper_method :resource_request_name 

InheritedResources를 사용하지 않는 경우 'ResourceController'에서 다음과 같은 것을 사용하십시오.

# 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

항상 다른 사람의 경험과 개선 사항을 듣고 기뻐하십시오.


내 경험 (적어도 Rails 3.0.9에서)에서 문자열로 명명 된 상수가 없으면 상수는 실패합니다. 그렇다면 어떻게 새로운 임의의 심볼을 생성 할 수 있습니까?
Lex Lindsey

4

최근 에 Rails 3.0 앱에서 안정적인 STI 패턴을 사용하려는 시도를 문서화했습니다 . 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

이 접근 방식은 사용자가 나열한 문제뿐만 아니라 다른 사람들이 STI 접근 방식과 관련하여 여러 가지 다른 문제를 해결합니다.


2

중첩 된 경로가없는 경우이 방법을 시도 할 수 있습니다.

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

또는 다른 방법으로 다음과 같이 OOP-magic을 사용할 수 있습니다. https://coderwall.com/p/yijmuq

두 번째 방법으로 모든 중첩 모델에 대해 유사한 도우미를 만들 수 있습니다.


2

다음은 양식과 응용 프로그램 전체에서 작동하는 안전한 방법입니다.

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

그런 다음 나는 내 형태로 있습니다. 이를 위해 추가 된 부분은 as : : district입니다.

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

도움이 되었기를 바랍니다.


2

내가 찾은 가장 깨끗한 솔루션은 기본 클래스에 다음을 추가하는 것입니다.

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

모든 서브 클래스에서 작동하며 전체 모델 이름 객체를 재정의하는 것보다 훨씬 안전합니다. 라우트 키만 대상으로함으로써 I18n을 중단 시키거나 Rails에서 정의한 모델 이름을 재정의함으로써 발생할 수있는 부작용을 피할 수있는 라우팅 문제를 해결합니다.


1

다음과 같이 STI 상속을 고려하면 :

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'에서 다음을 추가합니다.

module ManagedAtAModelLevel
  def model_name
    AModel.model_name
  end
end

그런 다음 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

따라서 기본 모델을 사용하려는 모델을 쉽게 선택할 수 있으며 하위 클래스 정의를 건드리지 않아도됩니다. 매우 건조합니다.


1

이 방법은 나를 위해 작동합니다 (기본 클래스 에서이 방법을 정의하십시오).

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

라우팅 purpouse에 대한 더미 Parent 객체를 반환하는 메서드를 만들 수 있습니다

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

그런 다음 form_for @ employee.routing_object를 호출하면 유형이 없으면 Person 클래스 객체가 반환됩니다.


1

@ prathan-thananart 답변과 여러 STI 클래스의 경우 부모 모델에 다음을 추가 할 수 있습니다->

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

그러면 연락처 데이터가있는 각 양식 params[:contact]params[:contact_person], 대신 매개 변수를 전송하게 됩니다 params[:contact_whatever].


-6

해시, 그러나 솔루션 목록에 또 다른 하나.

class Parent < ActiveRecord::Base; end

Class Child < Parent
  def class
    Parent
  end
end

레일 2.x 및 3.x에서 작동


5
이것은 하나의 문제를 해결하지만 다른 문제를 만듭니다. 이제 시도 하면 하위 클래스가 아닌 클래스를 Child.new반환합니다 Parent. 즉 type, type 속성을 명시 적으로 설정하지 않으면 컨트롤러를 통한 대량 할당을 통해 서브 클래스를 만들 수 없습니다 ( 기본적으로 보호 속성 이므로 ).
Chris Bloom
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.