Tạo tree node model với ancestry - Rails
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 ...
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!