Подтвердить что ты не робот

Почему полиморфная ассоциация не работает для ИППП, если столбец типа полиморфной ассоциации не указывает на базовую модель ИППП?

У меня есть случай полиморфной ассоциации и STI.

# app/models/car.rb
class Car < ActiveRecord::Base
  belongs_to :borrowable, :polymorphic => true
end

# app/models/staff.rb
class Staff < ActiveRecord::Base
  has_one :car, :as => :borrowable, :dependent => :destroy
end

# app/models/guard.rb
class Guard < Staff
end

Чтобы полиморфная ассоциация работала, согласно документации API на Полиморфной ассоциации, http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Polymorphic+Associations, мне нужно установить borrowable_type к base_class моделей STI, то есть в моем случае Staff.

Вопрос: почему он не работает, если borrowable_type установлен в класс STI?

Некоторые тесты, чтобы доказать это:

# now the test speaks only truth

# test/fixtures/cars.yml
one:
  name: Enzo
  borrowable: staff (Staff)

two:
  name: Mustang
  borrowable: guard (Guard)

# test/fixtures/staffs.yml
staff:
  name: Jullia Gillard

guard:
  name: Joni Bravo
  type: Guard 

# test/units/car_test.rb

require 'test_helper'

class CarTest < ActiveSupport::TestCase
  setup do
    @staff = staffs(:staff)
    @guard = staffs(:guard) 
  end

  test "should be destroyed if an associated staff is destroyed" do
    assert_difference('Car.count', -1) do
      @staff.destroy
    end
  end

  test "should be destroyed if an associated guard is destroyed" do
    assert_difference('Car.count', -1) do
      @guard.destroy
    end
  end

end

Но это похоже на экземпляр Персонал. Результаты:

# Running tests:

F.

Finished tests in 0.146657s, 13.6373 tests/s, 13.6373 assertions/s.

  1) Failure:
test_should_be_destroyed_if_an_associated_guard_is_destroyed(CarTest) [/private/tmp/guineapig/test/unit/car_test.rb:16]:
"Car.count" didn't change by -1.
<1> expected but was
<2>.

Спасибо

4b9b3361

Ответ 1

Хороший вопрос. У меня была такая же проблема с Rails 3.1. Похоже, вы не можете этого сделать, потому что это не работает. Вероятно, это намеченное поведение. По-видимому, использование полиморфных ассоциаций в сочетании с однонаправленным наследованием (STI) в Rails является немного сложным.

Текущая документация Rails для Rails 3.2 дает этот совет для объединения полиморфных ассоциаций и STI:

Использование полиморфных ассоциаций в сочетании с одной таблицей Наследование (STI) немного сложно. Для того чтобы ассоциации работайте, как ожидалось, убедитесь, что вы храните базовую модель для ИППП модели в столбце типа полиморфной ассоциации.

В вашем случае базовой моделью будет "Персонал", т.е. "заемный_тип" должен быть "Персоналом" для всех предметов, а не "Охрана". Можно создать производный класс как базовый класс, используя "становится": guard.becomes(Staff). Можно установить столбец "заемный_тип" непосредственно в базовый класс "Персонал" или, как предлагает документация Rails, автоматически преобразовать его с помощью

class Car < ActiveRecord::Base
  ..
  def borrowable_type=(sType)
     super(sType.to_s.classify.constantize.base_class.to_s)
  end

Ответ 2

Более старый вопрос, но проблема в Rails 4 все еще сохраняется. Другой вариант - динамически создавать/перезаписывать метод _type с озабоченностью. Это было бы полезно, если ваше приложение использует множественные полиморфные ассоциации с STI и вы хотите сохранить логику в одном месте.

Эта проблема будет захватывать все полиморфные ассоциации и гарантировать, что запись всегда сохраняется с использованием базового класса.

# models/concerns/single_table_polymorphic.rb
module SingleTablePolymorphic
  extend ActiveSupport::Concern

  included do
    self.reflect_on_all_associations.select{|a| a.options[:polymorphic]}.map(&:name).each do |name|
      define_method "#{name.to_s}_type=" do |class_name|
        super(class_name.constantize.base_class.name)
      end
    end
  end
end

Затем просто включите его в свою модель:

class Car < ActiveRecord::Base
  belongs_to :borrowable, :polymorphic => true
  include SingleTablePolymorphic
end

Ответ 3

Просто эта проблема возникла в Rails 4.2. Я нашел два способа решения:

-

Проблема заключается в том, что Rails использует имя base_class отношения STI.

Причина этого была задокументирована в других ответах, но суть в том, что основная команда, похоже, считает, что вы должны иметь возможность ссылаться на таблицу, а не на класс для полиморфной ассоциации STI.

Я не согласен с этой идеей, но не являюсь частью команды Rails Core, поэтому у меня нет большого вклада в ее устранение.

Есть два способа исправить это:

-

1) Вставить на уровне модели:

class Association < ActiveRecord::Base

  belongs_to :associatiable, polymorphic: true
  belongs_to :associated, polymorphic: true

  before_validation :set_type

  def set_type
    self.associated_type = associated.class.name
  end
end

Это приведет к изменению записи {x}_type перед созданием данных в db. Это работает очень хорошо и по-прежнему сохраняет полиморфный характер ассоциации.

2) Переопределить основные методы ActiveRecord

#app/config/initializers/sti_base.rb
require "active_record"
require "active_record_extension"
ActiveRecord::Base.store_base_sti_class = false

#lib/active_record_extension.rb
module ActiveRecordExtension #-> http://stackoverflow.com/questions/2328984/rails-extending-activerecordbase

  extend ActiveSupport::Concern

  included do
    class_attribute :store_base_sti_class
    self.store_base_sti_class = true
  end
end

# include the extension 
ActiveRecord::Base.send(:include, ActiveRecordExtension)

####

module AddPolymorphic
  extend ActiveSupport::Concern

  included do #-> http://stackoverflow.com/questions/28214874/overriding-methods-in-an-activesupportconcern-module-which-are-defined-by-a-cl
    define_method :replace_keys do |record=nil|
      super(record)
      owner[reflection.foreign_type] = ActiveRecord::Base.store_base_sti_class ? record.class.base_class.name : record.class.name
    end
  end
end

ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, AddPolymorphic)

Более системным способом устранения проблемы является редактирование основных методов ActiveRecord, которые определяют его. Я использовал ссылки в этот камень, чтобы выяснить, какие элементы необходимо исправлять/отменять.

Это непроверено и все еще нуждается в расширениях для некоторых других частей основных методов ActiveRecord, но, похоже, работает для моей локальной системы.

Ответ 5

Я согласен с общими замечаниями, что это должно быть проще. Тем не менее, вот что сработало для меня.

У меня есть модель с фирмой как базовый класс, а Customer и Prospect - как классы STI, так:

class Firm
end

class Customer < Firm
end

class Prospect < Firm
end

У меня также есть полиморфный класс, Opportunity, который выглядит следующим образом:

class Opportunity
  belongs_to :opportunistic, polymorphic: true
end

Я хочу ссылаться на возможности как на

customer.opportunities

или

prospect.opportunities

Для этого я изменил модели следующим образом.

class Firm
  has_many opportunities, as: :opportunistic
end

class Opportunity
  belongs_to :customer, class_name: 'Firm', foreign_key: :opportunistic_id
  belongs_to :prospect, class_name: 'Firm', foreign_key: :opportunistic_id
end

Я сохраняю возможности с оппортунистическим типом "Фирмы" (базового класса) и соответствующим идентификатором клиента или перспективы как оппортунистической_ид.

Теперь я могу получить клиентские возможности и перспективы. Точно так же, как я хочу.

Ответ 6

Вы также можете создать собственную область видимости для ассоциации has_* для полиморфного типа:

class Staff < ActiveRecord::Base
  has_one :car, 
          ->(s) { where(cars: { borrowable_type: s.class }, # defaults to base_class
          foreign_key: :borrowable_id,
          :dependent => :destroy
end

Поскольку полиморфные объединения используют составной внешний ключ (* _id и * _type), необходимо указать предложение типа с правильным значением. Хотя foreign_key должен работать только с объявлением foreign_key указывающим имя полиморфной ассоциации.

Из-за природы полиморфизма может быть неприятно знать, какие модели являются заемными, поскольку это может быть любая модель в вашем приложении на Rails. Эта связь должна быть объявлена в любой модели, где вы хотите, чтобы принудительное удаление каскада на заемных.