Rails: много-много полиморфных отношений - PullRequest
31 голосов
/ 15 июля 2009

Смотрите комментарии для обновлений.

Я изо всех сил пытался получить четкий и прямой ответ на этот вопрос, надеюсь, на этот раз я его получу! : D У меня определенно есть чему еще поучиться с Rails, однако я понимаю проблему, с которой сталкиваюсь, и буду очень признателен за дополнительную помощь.

  • У меня есть модель под названием «Задача».
  • У меня есть абстрактная модель под названием "Цель".
  • Я хотел бы связать несколько экземпляров подклассов Target с Task.
  • Я не использую наследование одной таблицы.
  • Я хотел бы запросить полиморфное отношение, чтобы вернуть смешанный результирующий набор подклассов Target.
  • Я хотел бы запросить отдельные экземпляры подклассов Target, чтобы получить задачи, с которыми они связаны.

Итак, я считаю, что полиморфные отношения «многие ко многим» между Задачами и подклассами Целей находятся в порядке. Более подробно, я смогу делать такие вещи в консоли (и, конечно, в другом месте):

task = Task.find(1)
task.targets
[...array of all the subclasses of Target here...]

Но! Предполагая, что существуют модели «Магазин», «Программное обеспечение», «Офис», «Транспортное средство», которые являются подклассами «Target», было бы неплохо также пересмотреть взаимосвязь в другом направлении:

store = Store.find(1)
store.tasks
[...array of all the Tasks this Store is related to...]
software = Software.find(18)
software.tasks
[...array of all the Tasks this Software is related to...]

Таблицы базы данных, подразумеваемые полиморфными отношениями, по-видимому, способны выполнить этот обход, но я вижу некоторые повторяющиеся темы в попытке найти ответ, который для меня побеждает дух полиморфных отношений:

  • Все еще используя мой пример, люди, кажется, хотят определить Магазин, Программное обеспечение, Офис, Автомобиль в Задаче, что, как мы можем сразу сказать, не является полиморфным отношением, поскольку возвращает только один тип модели .
  • Подобно последнему пункту, люди все еще хотят определять Магазин, Программное обеспечение, Офис и Транспорт в Задаче в одной форме или форме. Важным моментом здесь является то, что отношения слепы по отношению к подклассам. Мои полиморфы первоначально будут взаимодействовать только как цели, а не как их отдельные типы подклассов. Определение каждого подкласса в Задании снова начинает разрушать цель полиморфных отношений.
  • Я вижу, что модель для таблицы соединений может быть в порядке, что мне кажется несколько правильным, за исключением того, что она добавляет некоторую сложность, которую, как я предполагал, Rails захочет покончить. Я признаю неопытность по этому поводу.

Кажется, это небольшая дыра либо в функциональности рельсов, либо в знаниях коллективного сообщества. Так что, надеюсь, stackoverflow сможет вести хронику моего поиска ответа!

Спасибо всем, кто помогает!

Ответы [ 7 ]

55 голосов
/ 08 октября 2009

Вы можете объединить полиморфизм и has_many :through, чтобы получить гибкое отображение:

class Assignment < ActiveRecord::Base
  belongs_to :task
  belongs_to :target, :polymorphic => true
end

class Task < ActiveRecord::Base
  has_many :targets, :through => :assignment
end

class Store < ActiveRecord::Base
  has_many :tasks, :through => :assignment, :as => :target
end

class Vehicle < ActiveRecord::Base
  has_many :tasks, :through => :assignment, :as => :target
end

... и так далее.

11 голосов
/ 19 сентября 2015

Хотя ответ, предложенный SFEley, велик, есть некоторые недостатки:

  • Получение заданий из цели (Store / Vehicle) работает, но задом наперед не будет. Это в основном потому, что вы не можете перейти через ассоциацию к полиморфному типу данных, потому что SQL не может определить, в какой таблице он находится.
  • Каждая модель с: сквозной связью нуждается в прямой связи с промежуточной таблицей
  • Ассоциация: via Assignment должна быть во множественном числе
  • Оператор: as не будет работать вместе с: through, его нужно сначала указать с прямой связью, необходимой для промежуточной таблицы

Имея это в виду, мое простейшее решение было бы:

class Assignment < ActiveRecord::Base
  belongs_to :task
  belongs_to :target, :polymorphic => true
end

class Task < ActiveRecord::Base
  has_many :assignments
  # acts as the the 'has_many targets' needed
  def targets
    assignments.map {|x| x.target}
  end
end

class Store < ActiveRecord::Base
  has_many :assignments, as: :target
  has_many :tasks, :through => :assignment
end

class Vehicle < ActiveRecord::Base
  has_many :assignments, as: :target
  has_many :tasks, :through => :assignment, :as => :target
end

Ссылка: http://blog.hasmanythrough.com/2006/4/3/polymorphic-through

1 голос
/ 13 января 2012

Использование STI:

class Task < ActiveRecord::Base
end

class StoreTask < Task
  belongs_to :store, :foreign_key => "target_id"
end

class VehicleTask < Task
  belongs_to :vehicle, :foreign_key => "target_id"
end

class Store < ActiveRecord::Base
  has_many :tasks, :class_name => "StoreTask", :foreign_key => "target_id"
end

class Vehicle < ActiveRecord::Base
  has_many :tasks, :class_name => "VehicleTask", :foreign_key => "target_id"
end

В вашей базе данных вам понадобятся: Task type:string и Task target_id:integer

Преимущество заключается в том, что теперь у вас есть сквозная модель для каждого типа задачи, которая может быть конкретной.

См. Также ИППП и полиморфная модель вместе

Ура!

1 голос
/ 11 октября 2009

Решение has_many_polymorphs, о котором вы упоминаете, не так уж и плохо.

class Task < ActiveRecord::Base
  has_many_polymorphs :targets, :from => [:store, :software, :office, :vehicle]
end

Кажется, делает все, что вы хотите.

Предоставляет следующие методы:

к заданию:

t = Task.first
t.targets   # Mixed collection of all targets associated with task t
t.stores    # Collection of stores associated with task t
t.softwares # same but for software
t.offices   # same but for office
t.vehicles  # same but for vehicles

на программное обеспечение, магазин, офис, транспортное средство:

s = Software.first    # works for any of the subtargets.
s.tasks               # lists tasks associated with s

Если я правильно слежу за комментариями, единственной оставшейся проблемой является то, что вам не нужно изменять app / models / task.rb каждый раз, когда вы создаете новый тип Subtarget. Путь Rails, кажется, требует, чтобы вы изменили два файла для создания двунаправленной ассоциации. has_many_polymorphs требует только, чтобы вы изменили файл Tasks. Похоже, победа для меня. Или, по крайней мере, так было бы, если бы вам не пришлось редактировать новый файл модели.

Есть несколько способов обойти это, но они кажутся слишком много работы, чтобы избежать изменения одного файла время от времени. Но если вы настроены против изменения Task самостоятельно, чтобы добавить к полиморфным отношениям, вот мое предложение:

Храните список подзадач, я собираюсь предложить в lib / subtargets отформатировать одну запись на строку, которая по сути является table_name.underscore. (Заглавные буквы имеют префикс подчеркивания, а затем все делается строчными)

store
software
office
vehicle

Создайте config / initializer / subtargets.rb и заполните его следующим образом:

SubtargetList = File.open("#{RAILS_ROOT}/lib/subtargets").read.split.reject(&:match(/#/)).map(&:to_sym)

Затем вы захотите создать собственный генератор или новое задание на рейк. Чтобы создать новую подзадачу и добавить имя модели в файл списка подзадач, определенный выше. Вы, вероятно, в конечном итоге сделаете что-то голое, что сделает изменение и передаст аргументы стандартному генератору.

Извините, мне не очень хочется сейчас вас через это проходить, но вот некоторые ресурсы

Наконец, замените список в объявлении has_many_polymorphs на SubtargetList

class Task < ActiveRecord::Base
  has_many_polymorphs :targets, :from => SubtargetList
end

С этого момента вы можете добавить новую подзадачу с

$ script/generate subtarget_model home

И это автоматически обновит ваш полиморфный список, как только вы перезагрузите консоль или перезапустите рабочий сервер.

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

0 голосов
/ 26 сентября 2009

Использовали ли вы этот метод грубой силы:

class Task 
  has_many :stores
  has_many :softwares
  has_many :offices
  has_many :vehicles

  def targets
    stores + softwares + offices + vehicles
  end
  ...

Возможно, это не так уж и элегантно, но, честно говоря, это не так уж многословно, и в этом коде нет ничего неэффективного.

0 голосов
/ 22 августа 2009

Я согласен с другими, я бы пошел на решение, которое использует смесь ИППП и делегирования было бы гораздо проще реализовать.

В основе вашей проблемы лежит то, где хранить записи всех подклассов Target. ActiveRecord выбирает базу данных через модель STI.

Вы можете сохранить их в переменной класса в Target и использовать унаследованный обратный вызов для добавления новых. Затем вы можете динамически сгенерировать необходимый код из содержимого этого массива и использовать method_missing.

0 голосов
/ 15 июля 2009

Это может быть не особенно полезный ответ, но, говоря просто, я не думаю, что есть простой или автоматизированный способ сделать это. По крайней мере, не так просто, как с более простыми ассоциациями «один-ко-многим».

Я думаю, что создание модели ActiveRecord для таблицы соединений является правильным способом решения этой проблемы. Нормальные отношения has_and_belongs_to_many предполагают объединение двух указанных таблиц, тогда как в вашем случае кажется, что вы хотите объединить между tasks и любым из stores, softwares, offices или vehicles ( Кстати, есть ли причина не использовать STI здесь? Похоже, это поможет уменьшить сложность за счет ограничения количества таблиц, которые у вас есть). Таким образом, в вашем случае таблице соединения также необходимо знать имя задействованного подкласса Target. Что-то вроде

create_table :targets_tasks do |t|
  t.integer :target_id
  t.string :target_type
  t.integer :task_id
end

Затем в вашем классе Task, ваших подклассах Target и классе TargetsTask вы можете установить has_many ассоциации, используя ключевое слово :through, как описано в ActiveRecord :: Associations :: ClassMethods rdoc pages .

Но, тем не менее, это только поможет вам, потому что :through не будет знать, использовать поле target_type в качестве имени подкласса Target. Для этого вы можете написать несколько пользовательских фрагментов SQL выбора / поиска, которые также описаны в ActiveRecord :: Associations :: ClassMethods .

Надеюсь, это заставит вас двигаться в правильном направлении. Если вы найдете полное решение, я хотел бы увидеть его!

...