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 можно следить на .
мая 6, 2012 at 11:21 (#)
Вообщем-то это делается так.
мая 6, 2012 at 11:55 (#)
Здесь это не подходит потому, что ресурсов множество кроме статьи, а source_type только один тип может принять. Я вот не знаю, можно его в цикле использовать обходя все типы ресурсов или нет… Попробую проверить в ближайшее время.