make polymorphic children belong to only one parent

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…