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

Ассоциация полиморфных принадлежит определенному типу

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

class Note < ActiveRecord::Base
  # The true polymorphic association
  belongs_to :subject, polymorphic: true

  # Same as subject but where subject_type is 'Volunteer'
  belongs_to :volunteer, source_association: :subject
  # Same as subject but where subject_type is 'Participation'
  belongs_to :participation, source_association: :subject
end

Я пробовал огромное количество комбинаций из чтения об ассоциациях на ApiDock, но ничего похожего не делает именно то, что я хочу. Здесь лучшее, что у меня есть до сих пор:

class Note < ActiveRecord::Base
  belongs_to :subject, polymorphic: true
  belongs_to :volunteer, class_name: "Volunteer", foreign_key: :subject_id, conditions: {notes: {subject_type: "Volunteer"}}
  belongs_to :participation, class_name: "Participation", foreign_key: :subject_id, conditions: {notes: {subject_type: "Participation"}}
end

И я хочу пройти этот тест:

describe Note do
  context 'on volunteer' do
    let!(:volunteer) { create(:volunteer) }
    let!(:note) { create(:note, subject: volunteer) }
    let!(:unrelated_note) { create(:note) }

    it 'narrows note scope to volunteer' do
      scoped = Note.scoped
      scoped = scoped.joins(:volunteer).where(volunteers: {id: volunteer.id})
      expect(scoped.count).to be 1
      expect(scoped.first.id).to eq note.id
    end

    it 'allows access to the volunteer' do
      expect(note.volunteer).to eq volunteer
    end

    it 'does not return participation' do
      expect(note.participation).to be_nil
    end

  end
end

Первый тест проходит, но вы не можете напрямую вызвать отношение:

  1) Note on volunteer allows access to the volunteer
     Failure/Error: expect(note.reload.volunteer).to eq volunteer
     ActiveRecord::StatementInvalid:
       PG::Error: ERROR:  missing FROM-clause entry for table "notes"
       LINE 1: ...."deleted" = 'f' AND "volunteers"."id" = 7798 AND "notes"."s...
                                                                    ^
       : SELECT  "volunteers".* FROM "volunteers"  WHERE "volunteers"."deleted" = 'f' AND "volunteers"."id" = 7798 AND "notes"."subject_type" = 'Volunteer' LIMIT 1
     # ./spec/models/note_spec.rb:10:in `block (3 levels) in <top (required)>'

Почему?

Причина, по которой я хочу сделать это, заключается в том, что я создаю область, основанную на анализе строки запроса, включая объединение в различные модели /etc; код, используемый для создания области, значительно сложнее, чем выше - он использует collection.reflections и т.д. Мое текущее решение работает для этого, но оно оскорбляет меня. Я не могу вызвать отношения непосредственно из экземпляра Note.

Я мог бы решить это, разделив его на две проблемы: напрямую используя области

  scope :scoped_by_volunteer_id, lambda { |volunteer_id| where({subject_type: 'Volunteer', subject_id: volunteer_id}) }
  scope :scoped_by_participation_id, lambda { |participation_id| where({subject_type: 'Participation', subject_id: participation_id}) }

а затем просто используя getter для note.volunteer/note.participation, который просто возвращает note.subject, если он имеет право subject_type (в противном случае - nil), но я решил, что в Rails должен быть лучший способ?

4b9b3361

Ответ 1

Я столкнулся с подобной проблемой. и я, наконец, сгладил лучшее и наиболее надежное решение, используя ассоциацию самоначислений, как показано ниже.

class Note < ActiveRecord::Base
  # The true polymorphic association
  belongs_to :subject, polymorphic: true

  # The trick to solve this problem
  has_one :self_ref, :class_name => self, :foreign_key => :id

  has_one :volunteer, :through => :self_ref, :source => :subject, :source_type => Volunteer
  has_one :participation, :through => :self_ref, :source => :subject, :source_type => Participation
end

Чистый и простой, тестируется только на Rails 4.1, но я думаю, что он должен работать для предыдущих версий.

Ответ 2

Я застрял в этой обратной связи, и в Rails 4.2.1 я, наконец, обнаружил это. Надеюсь, это поможет кому-то, если они используют новую версию Rails. Ваш вопрос был самым близким к тому, что я нашел в связи с тем вопросом, который у меня был.

belongs_to :volunteer, foreign_key: :subject_id, foreign_type: 'Volunteer'

Ответ 3

Я нашел хакерский способ обойти эту проблему. У меня есть аналогичный вариант использования в моем проекте, и я нашел, что это работает. В вашей модели Note вы можете добавлять такие ассоциации:

class Note
  belongs_to :volunteer, 
    ->(note) {where('1 = ?', (note.subject_type == 'Volunteer')},
    :foreign_key => 'subject_id'
end

Вам нужно будет добавить один из них для каждой модели, к которой вы хотите прикреплять заметки. Чтобы сделать этот процесс DRYer, я бы рекомендовал создать такой модуль:

 module Notable
   def self.included(other)
     Note.belongs_to(other.to_s.underscore.to_sym, 
       ->(note) {where('1 = ?', note.subject_type == other.to_s)},
       {:foreign_key => :subject_id})
   end
 end

Затем включите это в свои модели волонтеров и участников.

[EDIT]

Немного лучше лямбда:

 ->(note) {(note.subject_type == "Volunteer") ? where('1 = 1') : none}

По какой-то причине замена "where" на "все" не работает. Также обратите внимание, что "none" доступен только в Rails 4.

[MOAR EDIT]

Я не запускаю рельсы 3,2 атм, поэтому я не могу проверить, но я думаю, что вы можете добиться аналогичного результата, используя Proc для условий, например:

belongs_to :volunteer, :foreign_key => :subject_id, 
  :conditions => Proc.new {['1 = ?', (subject_type == 'Volunteer')]}

Возможно, стоит сделать снимок

Ответ 4

Вы можете сделать так:

belongs_to :volunteer, -> {
  where(notes: { subject_type: 'Volunteer' }).includes(:notes)
}, foreign_key: :subject_id

includes выполните left join, поэтому у вас есть отношение notes со всеми volunteer и participation. И вы пройдете subject_id, чтобы найти свою запись.