Rails: полиморфные отношения "многие ко многим"

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

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

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

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

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

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

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

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

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

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

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


person Alexander Trauzzi    schedule 14.07.2009    source источник
comment
Из шести пунктов списка пять из них тривиально выполнить, если вы отбросите шестой, я не использую наследование одной таблицы. Что касается вашей точки зрения на STI ниже, поскольку дополнительные столбцы действительно вас беспокоят, рассмотрите возможность использования делегирования для передачи дополнительных данных и поведения другим моделям.   -  person austinfromboston    schedule 15.07.2009
comment
Вытолкнуть его наружу - вот что привело к этому. Хотя STI не вариант. Я бы хотел, чтобы это было, потому что, да ... Все это большие поклонники. Но я хочу, чтобы хранимые данные были связными, и было бы довольно много разных типов целей. Я все еще нахожу несколько поразительным, что нет такого способа собрать смешанную коллекцию, как эта. Мой дизайн кажется довольно удачным.   -  person Alexander Trauzzi    schedule 15.07.2009
comment
Мне удалось реализовать большую часть желаемой функциональности с помощью has_many_polymorphs. Единственное остающееся ограничение заключается в том, что я все еще застрял в определении каждого полиморфного типа в моем родительском (Task). Дополнительные решения приветствуются, но я не уверен, что решение будет у нас до новой версии rails или обновления has_many_polymorphs!   -  person Alexander Trauzzi    schedule 15.07.2009
comment
См. Следующее: rubyforge.org/forum/   -  person Alexander Trauzzi    schedule 15.07.2009
comment
Созданный билет: рельсы .lighthouseapp.com / projects / 8994 / билеты /   -  person Alexander Trauzzi    schedule 16.07.2009
comment
Как частый любитель (сделать учебник, подождать два года, сделать учебник), я был полностью шокирован, когда увидел, что Rails не поддерживает такую ​​базовую концепцию, как эта. Я понимаю, что шаблон STI подходит во многих случаях, но когда иерархия классов начинает разрастаться, а свойства каждого подкласса расходятся, это просто неприемлемо называть его предпочтительным подходом. Это может быть проще, даже разумно, но у него все еще есть возможность создать такой беспорядок в БД, что полагаться исключительно на STI для всего наследования моделей пахнет антипаттерном.   -  person Justin Searls    schedule 23.08.2009
comment
Кто-нибудь знает, решена ли эта проблема в rails 3? Было бы действительно неплохо иметь возможность применять шаблоны проектирования в рельсах в чистом виде!   -  person Eric Steen    schedule 24.08.2012


Ответы (7)


Вы можете комбинировать полиморфизм и 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

...И так далее.

person SFEley    schedule 08.10.2009
comment
Кажется, это решает проблему очень просто и на 100% поддерживается Rails. - person Daniel Beardsley; 08.04.2010
comment
Обратите внимание, что :as => target должно быть :as => :target в обоих случаях. Хорошее решение - person Yorick Sijsling; 06.05.2012
comment
Ооо! Ты прав, Йорик, хороший улов. Я правильно отредактировал. - person SFEley; 17.05.2012
comment
Я получаю эквивалент ActiveRecord::HasManyThroughAssociationNotFoundError: Could not find the association│ :assignment in model Task при попытке настроить это в моем приложении на Rails 3. Я использую модули (оба находятся в одном модуле), может ли это вызвать эту ошибку? - person joshhepworth; 21.11.2012
comment
Я столкнулся с аналогичной проблемой, но на полиморфном конце отношения. Думаю, этот ответ неполный, я добавил дополнительные has_many :assigments, :as => target к моделям Task и Vehicle, а has_many :tasks, :through => :assignment, :as => :target сокращен до has_many :tasks, :through => :assignments - person Dfr; 12.12.2012
comment
не следует использовать присвоение во множественном числе в операторах has_many, например has_many :targets, :through => :assignments? - person Alan; 16.07.2013
comment
Я думаю, это должно быть has_many :assignments, а затем has_many :targets, :through => :assignments. - person lobati; 13.08.2013

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

  • Получение задач из цели (Магазин / Транспортное средство) работает, но не в обратном направлении. Это в основном потому, что вы не можете пройти ассоциацию a: through с полиморфным типом данных, потому что SQL не может определить, в какой таблице он находится.
  • Каждой модели с ассоциацией: through требуется прямая ассоциация с промежуточной таблицей.
  • Ассоциация: through 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

person Raphael Ottoni    schedule 19.09.2015
comment
Я знаю, что это произошло через три года, но у меня похожая проблема, и я пытаюсь обдумать ваше решение. Кажется, все в порядке, но единственное различие между моделями Store и Vehicle - это сегмент :as => :target в последней строке. Это было намеренно? Почему Магазин и Транспортное средство должны отличаться? Просто хочу подтвердить! - person TheBrownCoder; 12.08.2018

Упомянутое вами решение 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 каждый раз, когда вы создаете новый тип подцели. Похоже, что способ Rails требует от вас изменения двух файлов для создания двунаправленной ассоциации. has_many_polymorphs требует только изменения файла задач. Мне кажется, это победа. Или, по крайней мере, было бы, если бы вам все равно не приходилось редактировать новый файл модели.

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

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

store
software
office
vehicle

Создайте config / initializers / 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

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

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

person EmFi    schedule 11.10.2009

Использование 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

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

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

Ваше здоровье!

person montrealmike    schedule 12.01.2012

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

Я думаю, что создание модели 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 :: Associates :: ClassMethods rdoc pages.

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

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

person Cody Brimhall    schedule 14.07.2009
comment
Поверьте, я уже давно бился об этом. Как только я что-нибудь придумываю, это будет очень широко распространено. Я несколько удивлен, что это не было решено раньше, поскольку сейчас в моде смешанные наборы результатов! :) - person Alexander Trauzzi; 15.07.2009
comment
Кроме того, я не использую STI, потому что это неэффективно и не так, как я хочу хранить свои данные. Скорее всего, я свяжу дополнительные варианты поведения с подклассами Target сверх значений по умолчанию. Мне просто не нравятся большие таблицы с множеством столбцов :) - person Alexander Trauzzi; 15.07.2009

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

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

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

person Rare Pleasures    schedule 22.08.2009

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

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

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

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

person ndp    schedule 26.09.2009
comment
Верно, но тогда Task должна знать о каждом типе таргетинга. Я хотел бы иметь возможность создавать целевые объекты и не изменять задачу каждый раз, когда добавляется новая. - person Alexander Trauzzi; 26.09.2009
comment
Это рубин. Вы можете написать метод класса own_to_task (или модуль), который будет включен в ваши магазины, офисы и т.д. Я не рекомендую такой подход, я просто говорю ... - person ndp; 27.09.2009