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

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

# 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

Но, похоже, это верно только для Staff instance.Результаты:

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

Спасибо

Ответы [ 6 ]

31 голосов
/ 30 мая 2012

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

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

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

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

class Car < ActiveRecord::Base
  ..
  def borrowable_type=(sType)
     super(sType.to_s.classify.constantize.base_class.to_s)
  end
14 голосов
/ 07 мая 2014

Более старый вопрос, но проблема в 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
11 голосов
/ 08 апреля 2015

Только что была эта проблема в Rails 4.2.Я нашел два способа решения:

-

Проблема в том, что Rails использует base_class имя отношения 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 запись до создания данных в БД.Это работает очень хорошо и все еще сохраняет полиморфный характер ассоциации.

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 #-> /1668051/relsy-rasshiryayschie-activerecord-base

  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 голосов
/ 27 октября 2015

Есть драгоценный камень. https://github.com/appfolio/store_base_sti_class

Протестировано и работает на разных версиях AR.

0 голосов
/ 24 января 2019

Вы также можете создать настраиваемую область для ассоциации 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), необходимо указать предложение типа с правильным значением. Хотя _id должен работать только с объявлением foreign_key, указывающим имя полиморфной ассоциации.

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

0 голосов
/ 05 июля 2014

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

У меня есть модель с Фирмой в качестве базового класса и Клиентом и Перспективой в качестве классов STI, вот так:полиморфный класс 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

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

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

...