What is the best way to set up a polymorphic collection such that a child is only allowed to have one parent? WheeledOne
asked this recently in #polymorphs
. It turns out there are several solutions.
example
A good example of such a structure is a file tree. Directory can contain Directories and Files in its directory_contents
, but a directory_contents
item can’t belong to multiple parent directories.
an association which i can define
We could merely have a belongs_to :directory
in File
and Directory
, and construct a directory_contents
field by hand, like so:
class Directory < ActiveRecord::Base
has_many :directories
has_many :files
def directory_contents
directories + files
end
end
This way we don’t need a join table. However, the simplest solution, in this case, is not the best. The query for directory_contents
will scale at O(n) where n is the number of classes in the polymorphic collection. Also, you can’t add arbitrary items to the collection; instead, you have to assign the parent to the item, which may be conceptually inappropriate in some cases.
it feels so good to be real
What if we want behavior and attributes on the association? For example, maybe we want to record the time and date the file was added to the directory independently of the time and date the contents of the file were modified. Or maybe we just want to avoid the scaling problem mentioned above, by using has_many_polymorphs
and its optimized SQL.
class Directory < ActiveRecord::Base
has_many_polymorphs :directory_contents,
:from => [:directories, :files],
:as => :parent_directory
end
But now a file or directory could belong to two or more parents. If we want a tag-style system, we’re done. But maybe we really loved Pilot Wings, Jamiroquai, and flannel, and multiple parentage is unacceptable, since this is the ‘90s.
We could add validates_uniqueness_of
on the join table:
class DirectoriesDirectoryContent < ActiveRecord::Base
belongs_to :directory_content, :polymorphic_true
belongs_to :parent_directory, :class => Directory,
:foreign_key => :parent_directory_id
validates_uniqueness_of :directory_content_id,
:scope => [:directory_content_type]
end
This will enforce the one-parent association we want. However, we will always have to check for and delete existing records for a child before we can save the new association.
(The scope is necessary because we want a File and a Directory with the same id
to be able co-exist in the join table.)
deeper underground in activerecord
When we move a file in our desktop filesystems, it no longer belongs to the old directory. We don’t have to unlink it by hand first; in fact, we don’t have to pay any attention to the old linkage at all. How can we do this in ActiveRecord? With a callback.
class DirectoriesDirectoryContent < ActiveRecord::Base
# snip
def before_save
if record = DirectoriesDirectoryContent.find(:first,
:conditions => "id != #{id}
AND directory_content_id = #{directory_content_id}
AND directory_content_type = #{directory_content_type}")
record.destroy
end
true
end
end
Now saving a new association will remove any existing association with the same child (excepting itself).
Virtual insanity! He was really talking about join tables…