QuasarLog: Запись вторая. Обход полиморфного HMT

мая 6, 2012  |  Published in CMS, QuasarLog, Ruby on Rails, Ruby on Rails 3  |  2 Comments

Что было сделано?

  • Удалена сущность Document, а вместе с тем и понятие документов. В Quasar будут присутствовать ресурсы, например статья, продукт или топик форума — это ресурсы. Здесь понятие ресурс несколько отличается от ресурса REST архитектуры, точнее первое множество входит во второе. В общем, в Quasar вместо документов теперь ресурсы.
  • Был добавлен первый ресурс — Article (статья). Он представляет собой какую-то совсем простую сущность, например пост в блоге или статью в новостном сайте. Для начала достаточно этой единственной сущности (ресурса), а затем мы добавим, например Product и Topic.
  • Добавлена константа RESTYPES, которая хранит имена моделей — ресурсов Quasar CMS. Эти модели должны храниться в директории app/models/resources.
  • Добавлена модель AttachmentLink, которая должна перелинковывать между собой ресурсы, точнее хранить связи между ресурсом и его вложениями (вложения — также ресурсы). Здесь и обнаружилась интересная проблема, о которой рассказано далее.
  • Добавлены счетчики для attached/master ресурсов.


Проблема и ее рашение
Оказывается в ActiveRecord нельзя создавать полиморфные has_many :trough => … (HMT) ассоциации. Ниже приведенный упрощенный код возвращает ошибку:

 ActiveRecord::HasManyThroughAssociationPolymorphicSourceError:
       Cannot have a has_many :through association ‘Article#attachments’ on the polymorphic object ‘Attachment#attachment’.

#app/models/attachment_link.rb
class AttachmentLink < ActiveRecord::Base
  #...

  belongs_to :master,     polymorphic: true
  belongs_to :attachment, polymorphic: true
end

#app/models/resources/article.rb
class Article < ActiveRecord::Base
#...

  has_many :attachment_links, as: :master,     dependent: :destroy
  has_many :master_links,     as: :attachment, dependent: :destroy, class_name: "AttachmentLink"
  has_many :attachments, through: :attachment_links
  has_many :masters,     through: :master_links

  def attach(attachment)
    attlink = self.attachment_links.new
    attlink.attachment = attachment
    attlink.save
  end
end

К сожалению, такое решение не работает. Для реализации связывания типа master -> attachment было решено добавить новую сущность — Resource. Resource хранит список всех ресурсов (Article, Product, Topic и т.д.) и является промежуточным звеном связывания. Resource будет:

has_many   :attachment_links, as: :master, dependent: :destroy
has_many   :attachments, through: :attachment_links
belongs_to :real_resource, polymorphic: true

А ресурсы вроде Article будут:

has_many :master_links,     as: :attachment,    dependent: :destroy, class_name: "AttachmentLink"
has_many :masters,     through: :master_links

has_one  :resource,         as: :real_resource, dependent: :destroy
has_many :attachments, through: :resource

К сожалению, и этот способ не помог, была выброшена все так же ошибка — ActiveRecord::HasManyThroughAssociationPolymorphicSourceError. Немного  в исходниках Rails я нашел вот такой код:

if through_reflection.options[:polymorphic]
  raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
end

То есть, при любая HMT ассоциация с полиморфной сущностью невозможна. Странно конечно, что это не реализовано, ну да ладно. Данный код находится в методе ActiveRecord::Reflection::ThroughReflection.check_validity!. Я не стал сильно вникать в то, зачем необходима такая проверка ассоциации, но видимо она там не зря. Разумеется, можно переписать кусок Rails своим кодом, который, возможно, будет пораждать множество конфликтов.

Я решил пойти другим путем. Я решил реализовать ассоциацию с вложениями вручную, то есть без has_many. Например, списки ссылок на master/attached ресурсы получаются через такой код:

def attachment_links()
  @attachment_links ||= AttachmentLink.where("master_id = ? AND master_type = ?", id,  class_name).limit(attachments_count)
end

def master_links()
  @master_links ||= AttachmentLink.where("attachment_id = ? AND attachment_type = ?", id,  class_name).limit(masters_count)
end

Хеш присоединенных ресурсов получаем вот так:

def attachments()
  @attachments ||= {}

  if @attachments.empty?
    attachment_links.each do |attlink|
      attclass_name = attlink.attachment_type
      attclass = attclass_name.constantize
      attachment = attclass.where(id: attlink.attachment_id).limit(1)
      @attachments[attclass_name] ||= []

      unless @attachments[attclass_name].include? attachment
        @attachments[attclass_name] << attachment
      end
    end
  end

  return @attachments
end

Новый attachment добавляется вот так:

def attach(attachment)
  if new_attlink(attachment).save
    increment_attachments_count
    reload_attachments_with attachment
    attachment.increment_masters_count
  end
end

def new_attlink(attachment = nil)
  AttachmentLink.new do |attlink|
    attlink.master_id   = self.id
    attlink.master_type = class_name

    if attachment.present?
      attlink.attachment_id   = attachment.id
      attlink.attachment_type = attachment.class_name
    end
  end
end

Разумеется этот код не совершенен ии будет еще улучшаться, но на первое время он вполне годится.
Напомню, что у нас есть группа во Вконтакте: , а еще за QuasarCMS можно следить на  .

Responses

  1. Aleksey Demidov says:

    мая 6, 2012 at 11:21 (#)

      has_many :attachments, :through => :attachment_links, :source_type => 'Article'
      has_many :masters,     :through => :master_links, :source_type => 'Article'
    

    Вообщем-то это делается так.

  2. admin says:

    мая 6, 2012 at 11:55 (#)

    Здесь это не подходит потому, что ресурсов множество кроме статьи, а source_type только один тип может принять. Я вот не знаю, можно его в цикле использовать обходя все типы ресурсов или нет… Попробую проверить в ближайшее время.

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.