Two models with the first being self-referential:
def Page < ActiveRecord::Base
has_many :source_page_relations,
:class_name => 'PageRelation',
:foreign_key => :child_id,
:dependent => :destroy
has_many :child_page_relations,
:class_name => 'PageRelation',
:foreign_key => :parent_id,
:dependent => :destroy
has_many :children, :through => :child_page_relations
has_many :parents, :through => :source_page_relations
end
def PageRelation < ActiveRecord::Base
belongs_to :parent, :class_name => 'Page', :foreign_key => :parent_id
belongs_to :child, :class_name => 'Page', :foreign_key => :child_id
end
Which means I can easily find both parents and children through @page.parents and @page.children.
Now, here’s the question: How do I find the “orphans” (or trunks, if you want to go tree-style, i.e. without parents) and the dead-ends (or leafs, i.e. without children) on a global basis?
I’m not that firm in SQL, so maybe someone has a fast idea how to accomplish that instead of a brute-force approach which iterates over all the pages?
edit
wait a minute ; there is, actually, a solution : use a left outer join.
this, for instance, will find all trunk pages (just joins all pages with their “parent” associations, and select pages with no association).
i don’t know what impact it will have on performance, though ; i think it should be reasonably
fastnot too slow, but not to use as a common task. Maybe it would be simpler using an Arel Table (but i’m not familiar enough with these, sadly).i don’t know if there is any other way than brute-force iteration here (and if you find one, i’d like to know it).
My advice would be to capture these properties as boolean attributes of the pages and use callbacks to maintain these in a coherent state.
The idea is to add
before_save,after_save,before_destroy… callbacks on thePageModel, so that every time we manipulate a page, we check if it is a “trunk” or a “leaf” (by checking if it has parents or children viaparents.exist?andchildren.exists?) ; then we modify booleantrunkandleafattributes on this page ( or on the associated page in case of a destroy ).This will somehow slow down performance on insert / update / delete, but allow to fetch trunks and leaves really fast with a simple
where( trunk: true )statement. You will probably have to usedefault_scope includes( :parents, :children )on thePagemodel (or at least useincludesa lot) to prevent the number of DB hits to explode.Maybe it’s possible to use the same strategy, but to place the callbacks on the
PageRelationmodel ; it may even be possible to use anObserver. It all depends on your specific needs and coding style, and would be far to long to develop here.