Tạo tree node model với ancestry - Rails
Bài đăng này đã không được cập nhật trong 7 năm
Giới thiệu
Hôm nay tôi sẽ giới thiệu đến các bạn một công cụ trong Rails - giúp chúng ta dễ dàng hơn trong việc phân cấp có tính kế thừa( hoặc là cấu trúc tree) cho một single model - Ancestry. Việc phân cấp này cần thiết khi chúng ta có nhiều records cùng được định nghĩa bởi một model, mà ta muốn chỉ định đâu là record cha, đâu là record con. Ancestry sử dụng một single database column. Bằng cách sử dụng Ancestry, chúng ta có thể truy vấn được tất cả quan hệ cơ bản của tree structure, đó là: ancestors, parent, root, children, siblings, descendants, và tất cả chúng đều được fetch bởi một câu truy vấn SQL. Ngoài ra còn thêm các tính năng truy vấn được các STI support, scopes, depth caching, depth constraints, dễ dàng migration từ gem cũ, kiểm tra tính toàn vẹn, sắp xếp các (sub) tree vào các hashes và phục vụ các chiến lược khác nhau với các records mồ côi (orphaned records).
Installation
Để áp dụng Ancestry vào ActiveRecord
model ta có thể cài đặt như sau:
Install
Add vào gem:
# Gemfile
gem 'ancestry'
Chạy lệnh
bundle install
Add ancestry vào như một column table:
rails g migration add_ancestry_to_[table] ancestry:string:index
migrate database:
rake db:migrate
Add ancestry vào model:
# app/models/[model.rb]
class [Model] < ActiveRecord::Base
has_ancestry
end
model của bạn bây giờ đã có cấu trúc tree.
Sử dụng acts_as_tree
thay vì has_ancestry
Trong version 1.2.0 method acts_as_tree
đã được thay đổi tên thành has_ancestry
để cho phép sử dụng cả hai gem acts_as_tree
và gem ancestry trong cùng một application. Method acts_as_tree
sẽ tiếp tục được support trong thời gian tới.
Tổ chức các records thành cấu trúc tree.
Bạn có thể sử dụng thuộc tính parent để tổ chức thành cấu trúc tree của bạn. Nếu bạn có id của record bạn muốn sử dụng như là parent và bạn không muốn fetch nó thì bạn có thể sử dụng parent_id.
Giống như bất kì attributes ảo nào, parent và parent_id có thể set được giá trị bằng cách: parent=, parent_id= trên một bản ghi hoặc đưa chúng vào hash để truyền vào new, create, update_attributes và update_attributes!
.
Ví dụ:
TreeNode.create! :name => 'Stinky', :parent => TreeNode.create!(:name => 'Squeeky')
Bạn cũng có thể tạ ra một children thông qua relation children:
node.children.create :name => 'Stinky'
Duyệt cây (Navigating your tree)
parent Returns the parent of the record, nil for a root node
parent_id Returns the id of the parent of the record, nil for a root node
root Returns the root of the tree the record is in, self for a root node
root_id Returns the id of the root of the tree the record is in
root?, is_root? Returns true if the record is a root node, false otherwise
ancestor_ids Returns a list of ancestor ids, starting with the root id and ending with the parent id
ancestors Scopes the model on ancestors of the record
path_ids Returns a list the path ids, starting with the root id and ending with the node's own id
path Scopes model on path records of the record
children Scopes the model on children of the record
child_ids Returns a list of child ids
has_parent? Returns true if the record has a parent, false otherwise
has_children? Returns true if the record has any children, false otherwise
is_childless? Returns true is the record has no children, false otherwise
siblings Scopes the model on siblings of the record, the record itself is included*
sibling_ids Returns a list of sibling ids
has_siblings? Returns true if the record's parent has more than one child
is_only_child? Returns true if the record is the only child of its parent
descendants Scopes the model on direct and indirect children of the record
descendant_ids Returns a list of a descendant ids
subtree Scopes the model on descendants and itself
subtree_ids Returns a list of all ids in the record's subtree
depth Return the depth of the node, root nodes are at depth 0
Nếu record là root, thì các root khác được coi là anh chị em (cùng cấp)
Options cho has_ancestry
:ancestry_column Pass in a symbol to store ancestry in a different column
:orphan_strategy Instruct Ancestry what to do with children of a node that is destroyed:
:destroy All children are destroyed as well (default)
:rootify The children of the destroyed node become root nodes
:restrict An AncestryException is raised if any children exist
:adopt The orphan subtree is added to the parent of the deleted node.
If the deleted node is Root, then rootify the orphan subtree.
:cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
If you turn depth_caching on for an existing model:
- Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
- Build cache: TreeNode.rebuild_depth_cache!
:depth_cache_column Pass in a symbol to store depth cache in a different column
:primary_key_format Supply a regular expression that matches the format of your primary key.
By default, primary keys only match integers ([0-9]+).
:touch Instruct Ancestry to touch the ancestors of a node when it changes, to
invalidate nested key-based caches. (default: false)
Scopes
Nếu có thể, các phương pháp duyệt sẽ trả về các scopes thay vì các records, điều đó có nghĩa là thêm các điều kiện, sắp xếp, giới hạn ... Có thể được áp dụng và kết quả có thể được lấy ra, tính, hoặc kiểm tra sự tồn tại.
ví dụ:
node.children.where(:name => 'Mary').exists?
node.subtree.order(:name).limit(10).each do; ...; end
node.descendants.count
Để tiện lợi, một vài scopes được đặt ở mức class:
roots Root nodes
ancestors_of(node) Ancestors of node, node can be either a record or an id
children_of(node) Children of node, node can be either a record or an id
descendants_of(node) Descendants of node, node can be either a record or an id
subtree_of(node) Subtree of node, node can be either a record or an id
siblings_of(node) Siblings of node, node can be either a record or an id
Thậm chí có thể dùng chúng như những relation của model:
node.children.create
node.siblings.create!
TestNode.children_of(node_id).new
TestNode.siblings_of(node_id).create
Select các records theo độ sâu (depth)
Khi cache độ sâu được enabled ( xem các options về has_ancestry), 5 scopes được đặt tên khác có thể được sử dụng để select các records:
before_depth(depth) Return nodes that are less deep than depth (node.depth < depth)
to_depth(depth) Return nodes up to a certain depth (node.depth <= depth)
at_depth(depth) Return nodes that are at depth (node.depth == depth)
from_depth(depth) Return nodes starting from a certain depth (node.depth >= depth)
after_depth(depth) Return nodes that are deeper than depth (node.depth > depth)
depth scope cũng có sẵn để có thể gọi đến các hậu duệ (descendants) của node, trong trường hợp này giá trị của độ sâu được diễn giải tương đối:
node.subtree(:to_depth => 2) Subtree of node, to a depth of node.depth + 2 (self, children and grandchildren)
node.subtree.to_depth(5) Subtree of node to an absolute depth of 5
node.descendants(:at_depth => 2) Descendant of node, at depth node.depth + 2 (grandchildren)
node.descendants.at_depth(10) Descendants of node at an absolute depth of 10
node.ancestors.to_depth(3) The oldest 4 ancestors of node (its root and 3 more)
node.path(:from_depth => -2) The node's grandparent, parent and the node itself
node.ancestors(:from_depth => -6, :to_depth => -4)
node.path.from_depth(3).to_depth(4)
node.descendants(:from_depth => 2, :to_depth => 4)
node.subtree.from_depth(10).to_depth(12)
Xin lưu ý rằng các ràng buộc về độ sâu không thể truyền được ncestor_ids và path_ids. Lý do cho điều này đó là cả hai relations này đều có thể được fetched từ ancestry tổ tiên mà không cần truy vấn database. Bạn có thể sử dụng:
ancestors(depth_options).map(&:id)
or
Arrangement
Ancestry dễ dàng sắp xếp các subtree vào các nested hashes để dễ dàng cho việc quản lý và điều hướng sau khi lấy ra từ database. Có một điều lưu ý đó là khi dịch sang tiếng Việt thì Arrangement và Sort đều có nghĩa là sắp xếp, tuy nhiên ở đây ý nghĩa lại khác nhau.
TreeNode sau khi sắp xếp trả về:
{ #<TreeNode id: 100018, name: "Stinky", ancestry: nil>
=> { #<TreeNode id: 100019, name: "Crunchy", ancestry: "100018">
=> { #<TreeNode id: 100020, name: "Squeeky", ancestry: "100018/100019">
=> {}
}
}
}
arrange method cũng hoạt động với scoped class:
TreeNode.find_by_name('Crunchy').subtree.arrange
Method arrange lấy ActiveRecord
tìm các options. Nếu bạn muốn các hashes được ordered, bạn bên truyền order vào arrange thay vì dùng scope:
TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
Để sắp xếp các hashes lồng nhau:
gọi TreeNode.arrange_serializable
:
[
{
"ancestry" => nil, "id" => 1, "children" => [
{ "ancestry" => "1", "id" => 2, "children" => [] }
]
}
]
Bạn cũng có thể cung cấp serialization logic của riêng bạn bằng cách sử dụng blocks:
Sử dụng ActiveModel
Serializers:
TreeNode.arrange_serializable do |parent, children|
MySerializer.new(parent, children: children)
end
Hoặc tạo hashes đơn giản:
TreeNode.arrange_serializable do |parent, children|
{
my_id: parent.id
my_children: children
}
end
Dễ dàng đưa về dạng json
:
TreeNode.arrange_serializable.to_json
Bạn cũng có thể truyền order vào arrange_serializable
như với arrange
:
TreeNode.arrange_serializable(:order => :name)
Sorting
Sort:
TreeNode.sort_by_ancestry(array_of_nodes)
Kiểm tra và phục hồi tính toàn vẹn
Ancestry có hỗ trợ một số method giúp có thể phát hiện vấn đề về tính toàn vẹn ( ví dụ như trường hợp ancestry thành vòng tròn) và restoring đảm bảo tính toàn vẹn. Để check thì có thể dùng:
[Model].check_ancestry_integrity!.
Lỗi AncestryIntegrityException sẽ raise nếu vi phạm.
bạn có thể định nghĩa :report => :list
để lưu trữ một list các exceptions, hoặc :report => :echo
để xuất ra các exceptions đấy.
Để restore có thể sử dụng:
[Model].restore_ancestry_integrity!
Dưới đây là ví dụ:
>> stinky = TreeNode.create :name => 'Stinky'
$ #<TreeNode id: 1, name: "Stinky", ancestry: nil>
>> squeeky = TreeNode.create :name => 'Squeeky', :parent => stinky
$ #<TreeNode id: 2, name: "Squeeky", ancestry: "1">
>> stinky.update_attribute :parent, squeeky
$ true
>> TreeNode.all
$ [#<TreeNode id: 1, name: "Stinky", ancestry: "1/2">, #<TreeNode id: 2, name: "Squeeky", ancestry: "1/2/1">]
>> TreeNode.check_ancestry_integrity!
!! Ancestry::AncestryIntegrityException: Conflicting parent id in node 1: 2 for node 1, expecting nil
>> TreeNode.restore_ancestry_integrity!
$ [#<TreeNode id: 1, name: "Stinky", ancestry: 2>, #<TreeNode id: 2, name: "Squeeky", ancestry: nil>]
Bổ sung thêm, nếu bạn cảm thấy có gì đó sai sai về depth:
TreeNode.rebuild_depth_cache!
All rights reserved