I'm trying to implement the typical gof composite pattern:
example class diagram
I'm kind of lost when it comes to querying it later on. For example would there be a nice way to query all Composites without any ancestors?
My initial idea was to create something like that with ActiveRecord
class Component < ActiveRecord::Base
belongs_to :childrenable, :polymorphic => true
has_and_belongs_to_many: composites
end
class Leaf < ActiveRecord::Base
has_many: components, :as => :childrenable
end
class Composite < ActiveRecord::Base
has_many: components, :as => :childrenable
has_and_belongs_to_many :components
end
Would that work? How would I build a list like that (in the View later on f.ex.)?:
CompositeA
->Item
->CompositeB
->ItemA
->CompositeC
->ItemA
->ItemB
I'm just a bit lost when it comes to the query. Are there any best practices for this problem?
There are couple of aspects before the actual solution:
Many-to-many
It can be solved using a many-to-many relationship with itself.
Model
class Component < ActiveRecord::Base
# Add as many attributes you need
attr_accessible :name
has_and_belongs_to_many :children,
:class_name => "Component",
:join_table => "children_containers",
:foreign_key => "container_id",
:association_foreign_key => "child_id"
has_and_belongs_to_many :containers,
:class_name => "Component",
:join_table => "children_containers",
:foreign_key => "child_id",
:association_foreign_key => "container_id"
# All Components that do not belong to any container
scope :roots, -> {where("not exists (select * from children_containers where child_id=components.id)")}
# All Components that have no children
scope :leaves, -> {where("not exists (select * from children_containers where container_id=components.id)")}
# Is this Component at root level
def root?
self.containers.empty?
end
# Is this Component at leaf level
def leaf?
self.children.empty?
end
# Notice the recursive call to traverse the Component hierarchy
# Similarly, it can be written to output using nested <ul> and <li>s as well.
def to_s(level=0)
"#{' ' * level}#{name}\n" + children.map {|c| c.to_s(level + 1)}.join
end
end
Migration
class CreateComponents < ActiveRecord::Migration
def change
create_table :components do |t|
t.string :name
t.timestamps
end
create_table :children_containers, :id => false do |t|
t.references :child
t.references :container
end
add_index :children_containers, :child_id
add_index :children_containers, [:container_id, :child_id], :unique => true
end
end
Sample code
["R1", "R2", "L1", "L2", "C1", "C2", "C3"].each {|n| Component.create(:name => n)}
[
["R1", "C1"],
["R2", "C2"],
["R1", "C3"],
["R2", "C3"],
["C1", "L1"],
["C2", "L2"],
["C3", "L1"],
["C3", "L2"]
].each {|pair| p,c=pair; Component.find_by_name(p).children << Component.find_by_name(c)}
puts Component.roots.map(&:name).to_s
# ["R1", "R2"]
puts Component.leaves.map(&:name).to_s
# ["L1", "L2"]
puts Component.find_by_name("R1").to_s
# R1
# C1
# L1
# C3
# L1
# L2
One-to-many
It is much simpler in this case. Use Ancestry (https://github.com/stefankroes/ancestry) in Component model. It will provide all the operations that are needed. Alternatively acts_as_tree can be used instead of Ancestry.
Let me know if you need sample code for this.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With