Rails / ActiveRecord: Как правильно установить связь «многие ко многим», где одна сторона является полиморфной? - PullRequest
0 голосов
/ 23 февраля 2019

У меня есть Badge, Comment и Project.Комментарии и проекты могут иметь много значков.У значка может быть много комментариев и проектов.

Значки указывают на комментарии и проекты через объединенную таблицу с именем реакции.

# reaction.rb
class Reaction < ApplicationRecord
  belongs_to :badge
  belongs_to :reaction_target, polymorphic: true
end

# badge.rb
class Badge < ApplicationRecord
  has_many :reactions
  has_many :reaction_targets, through: :reactions
end

# comment.rb
class Comment < ApplicationRecord
  has_many :reactions, as: :reaction_target
  has_many :badges, through: :reactions
end

# project.rb
class Project < ApplicationRecord
  has_many :reactions, as: :reaction_target
  has_many :badges, through: :reactions
end

Теперь я могу добавлять значки к целям реакции:

> @comment.badges << Badge.first_or_create(name: "test")
=> [#<Badge:0x00007fcff7619d28
  id: 1,
  name: "test",
  created_at: Sat, 23 Feb 2019 18:25:54 UTC +00:00,
  updated_at: Sat, 23 Feb 2019 18:25:54 UTC +00:00>]

Но я не могу сделать обратное:

> Badge.first_or_create(name: "test").reaction_targets << @comment
ActiveRecord::HasManyThroughAssociationPolymorphicSourceError: Cannot have
a has_many :through association 'Badge#reaction_targets' on the
polymorphic object 'ReactionTarget#reaction_target' without 'source_type'.
Try adding 'source_type: "ReactionTarget"' to 'has_many :through'
definition. from /Users /elephant/.rvm/gems/ruby-2.6.0/gems/activerecord-5
.2.2/lib/active_record/reflecti on.rb:932:in `check_validity!'

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

class Badge < ApplicationRecord
  has_many :reactions
  has_many :reaction_targets, through: :reactions, source_type: "ReactionTarget"
end

Но затем я получаю сообщение об ошибке:

> Badge.first_or_create(name: "test").reaction_targets
NameError: uninitialized constant Badge::ReactionTarget

Я пытаюсь справиться с этим.Что мне не хватает?Где я ошибся?Что мешает мне определить цели реакции со стороны значка?

1 Ответ

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

TL; DR:

app / models / badge.rb

class Badge < ApplicationRecord
  has_many :reactions

  has_many :reaction_target_comments, through: :reactions, source: :reaction_target, source_type: 'Comment'
  has_many :reaction_target_projects, through: :reactions, source: :reaction_target, source_type: 'Project '
  # ... etc
end

Объяснение

Насколько я знаю, для полиморфных многих-в-ко-многим, вы не можете выполнить

class Badge < ApplicationRecord
  has_many :reaction_targets, through: :reactions
  # ...
end

... (возникнет ошибка, как вы уже заметили), потому что, если это возможно, это означает, что выполнение badge.reaction_targets должно вернуть массивэкземпляров "другой модели", то есть:

badge = Badge.find(1)
puts badge.reaction_targets.to_a
# => [<Comment id: 1>,
#     <Project id: 45>,
#     <SomeModel id: 99>,
#     <COmment id: 3>,
#     ...]

^ Приведенный выше код выглядит действительно интуитивно верно, не так ли?... потому что он должен возвращать массив записей различного типа, что совершенно логично, потому что это полиморфные отношения, верно?Да, это совершенно верно, но становится проблематичным в отношении того, какую строку SQL следует генерировать.См. Пример SQL-эквивалента ниже:

puts badge.reaction_targets
# =>  SELECT "WHAT_TABLE_1".* FROM "WHAT_TABLE_1" INNER JOIN reactions ...
#     SELECT "WHAT_TABLE_2".* FROM "WHAT_TABLE_2" INNER JOIN reactions ...
#     SELECT "WHAT_TABLE_3".* FROM "WHAT_TABLE_3" INNER JOIN reactions ...
#     ... etc

^ ..., поскольку ожидается, что reaction_targets будет иметь экземпляры с различными моделями, тогда представьте, какой SQL-запрос необходим для получения всех записей?Хотя, я думаю, что можно получить их все, но это, вероятно, будет не прямой оператор SQL, а, вероятно, имеющий комбинацию некоторой логики Ruby-кода на стороне приложения.Кроме того, тип возвращаемого значения должен быть не ActiveRecord::Associations::CollectionProxy, а, вероятно, новым типом объекта, специально предназначенного для удовлетворения этих has_many полиморфных отношений.Я имею в виду, представьте, если вы сделаете что-то, как показано ниже:

badge.reaction_targets.where(is_enabled: true, first_name: 'Hello')
# or even more complex:
badge.reaction_targets.joins(:users).where(users: { email: 'email@example.com' }).

^ ... Я не думаю, что есть прямое SQL-выражение, которое выше для полиморфных объединений, и поэтому, возможно, поэтомувам необходимо добавить source и source_type для разных «моделей», как и мой ответ выше.

Возможное решение

Если вы нормально возвращаете объект Array вместообычного ActiveRecord::Associations::CollectionProxy объекта вы можете сделать что-то вроде ниже.(Хотя вам все равно придется указывать каждое полиморфное has_many отношение и, возможно, добавить еще в будущем.)

class Badge < ApplicationRecord
  has_many :reactions

  has_many :reaction_target_comments, through: :reactions, source: :reaction_target, source_type: 'Comment'
  has_many :reaction_target_projects, through: :reactions, source: :reaction_target, source_type: 'Project'

  def reaction_targets
    reaction_target_comments.to_a + reaction_target_projects.to_a
  end
end

Пример использования:

badge = Badge.find(1)
puts badge.reaction_targets
# =>[<Comment id: 1>,
#    <Comment id: 45>,
#    <Project id: 99>,
#    <Project id: 3>,
#    ...]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...