STI, один контроллер
Я новичок в рельсах, и я как бы застрял в этой проблеме с дизайном, что может быть легко решить, но я никуда не денусь:
У меня есть два разных вида рекламы: основные моменты и сделки. Оба они имеют одинаковые атрибуты: название, описание и одно изображение (с помощью скрепки). У них также есть те же действия, которые применяются на них: индекс, новый, редактировать, создавать, обновлять и уничтожать.
Я установил STI следующим образом:
Модель объявления: ad.rb
class Ad < ActiveRecord::Base
end
Сделка: bargain.rb
class Bargain < Ad
end
Выделите модель: highlight.rb
class Highlight < Ad
end
Проблема в том, что я хотел бы иметь только один контроллер (AdsController
), который выполняет действия, которые я сказал по сделкам или основным моментам, в зависимости от URL-адреса, например www.foo.com/bargains [/...] или www.foo.com/highlights [/...].
Например:
- GET www.foo.com/highlights = > список всех отображаемых объявлений.
- GET www.foo.com/highlights/new = > форма для создания новой подсветки
и т.д...
Как я могу это сделать?
Спасибо!
Ответы
Ответ 1
Во-первых. Добавьте несколько новых маршрутов:
resources :highlights, :controller => "ads", :type => "Highlight"
resources :bargains, :controller => "ads", :type => "Bargain"
И исправьте некоторые действия в AdsController
. Например:
def new
@ad = Ad.new()
@ad.type = params[:type]
end
Для лучшего подхода для всего этого задания контроллера смотрите этот комментарий
Это все. Теперь вы можете перейти к localhost:3000/highlights/new
, а новый Highlight
будет инициализирован.
Действие индекса может выглядеть так:
def index
@ads = Ad.where(:type => params[:type])
end
Перейдите к localhost:3000/highlights
, и появится список основных моментов.
localhost:3000/bargains
и т.д.
URLS
<%= link_to 'index', :highlights %>
<%= link_to 'new', [:new, :highlight] %>
<%= link_to 'edit', [:edit, @ad] %>
<%= link_to 'destroy', @ad, :method => :delete %>
за полиморфность:)
<%= link_to 'index', @ad.class %>
Ответ 2
fl00r имеет хорошее решение, однако я бы сделал одну настройку.
Это может потребоваться или не потребоваться в вашем случае. Это зависит от того, какое поведение меняется в ваших моделях STI, особенно в проверках и жизненных циклах.
Добавьте приватный метод к вашему контроллеру, чтобы преобразовать ваш параметр типа в фактическую константу класса, которую вы хотите использовать:
def ad_type
params[:type].constantize
end
Однако это небезопасно. Добавить белый список типов:
def ad_types
[MyType, MyType2]
end
def ad_type
params[:type].constantize if params[:type].in? ad_types
end
Подробнее о методе константы рельсов здесь: http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-constantize
Затем в действиях контроллера вы можете:
def new
ad_type.new
end
def create
ad_type.new(params)
# ...
end
def index
ad_type.all
end
И теперь вы используете фактический класс с правильным поведением вместо родительского класса с установленным типом атрибута.
Ответ 3
Я просто хотел включить эту ссылку, потому что есть несколько интересных трюков, связанных с этой темой.
Алекс Рейснер - Наследование отдельных таблиц в рельсах
Ответ 4
[Переписано более простым решением, которое работает полностью:]
Итерация по другим ответам, я придумал следующее решение для одного контроллера с одиночным наложением таблицы, который хорошо работает с сильными параметрами в Rails 4.1. Просто включите: введите как разрешенный параметр, вызвав ошибку ActiveRecord::SubclassNotFound
, если введен неверный тип. Более того, тип не обновляется, потому что SQL-запрос явно ищет старый тип. Вместо этого :type
нужно обновлять отдельно с помощью update_column, если он отличается от текущего, установленного и является допустимым типом. Также обратите внимание, что мне удалось перегрузить все списки типов.
# app/models/company.rb
class Company < ActiveRecord::Base
COMPANY_TYPES = %w[Publisher Buyer Printer Agent]
validates :type, inclusion: { in: COMPANY_TYPES,
:message => "must be one of: #{COMPANY_TYPES.join(', ')}" }
end
Company::COMPANY_TYPES.each do |company_type|
string_to_eval = <<-heredoc
class #{company_type} < Company
def self.model_name # http://stackoverflow.com/a/12762230/1935918
Company.model_name
end
end
heredoc
eval(string_to_eval, TOPLEVEL_BINDING)
end
И в контроллере:
# app/controllers/companies_controller.rb
def update
@company = Company.find(params[:id])
# This separate step is required to change Single Table Inheritance types
new_type = params[:company][:type]
if new_type != @company.type && Company::COMPANY_TYPES.include?(new_type)
@company.update_column :type, new_type
end
@company.update(company_params)
respond_with(@company)
end
И маршруты:
# config/routes.rb
Rails.application.routes.draw do
resources :companies
Company::COMPANY_TYPES.each do |company_type|
resources company_type.underscore.to_sym, type: company_type, controller: 'companies', path: 'companies'
end
root 'companies#index'
Наконец, я рекомендую использовать responders gem и установить строительные леса для использования responseers_controller, который совместим с STI. Конфигурация для лесов:
# config/application.rb
config.generators do |g|
g.scaffold_controller "responders_controller"
end
Ответ 5
Я знаю, что это старый вопрос, вот шаблон, который мне нравится, который включает ответы от @flOOr и @Alan_Peabody. (Проверено в Rails 4.2, возможно, работает в Rails 5)
В вашей модели создайте свой белый список при запуске. В dev это должно быть загружено.
class Ad < ActiveRecord::Base
Rails.application.eager_load! if Rails.env.development?
TYPE_NAMES = self.subclasses.map(&:name)
#You can add validation like the answer by @dankohn
end
Теперь мы можем ссылаться на этот белый список в любом контроллере для создания правильной области видимости, а также в коллекции для выбора типа в форме и т.д.
class AdsController < ApplicationController
before_action :set_ad, :only => [:show, :compare, :edit, :update, :destroy]
def new
@ad = ad_scope.new
end
def create
@ad = ad_scope.new(ad_params)
#the usual stuff comes next...
end
private
def set_ad
#works as normal but we use our scope to ensure subclass
@ad = ad_scope.find(params[:id])
end
#return the scope of a Ad STI subclass based on params[:type] or default to Ad
def ad_scope
#This could also be done in some kind of syntax that makes it more like a const.
@ad_scope ||= params[:type].try(:in?, Ad::TYPE_NAMES) ? params[:type].constantize : Ad
end
#strong params check works as expected
def ad_params
params.require(:ad).permit({:foo})
end
end
Нам нужно обрабатывать наши формы, потому что маршрутизация должна быть отправлена контроллеру базового класса, несмотря на фактический тип объекта. Для этого мы используем "становится", чтобы обмануть построитель форм в правильную маршрутизацию и директиву: as, чтобы заставить входные имена быть базовым классом. Эта комбинация позволяет нам использовать немодифицированные маршруты (ресурсы: объявления), а также сильную проверку параметров params [: ad], возвращающихся из формы.
#/views/ads/_form.html.erb
<%= form_for(@ad.becomes(Ad), :as => :ad) do |f| %>