Tìm hiểu thêm về gem Ancestry

Đôi khi trong công việc bạn phải động đến dữ liệu dạng cây thư mục, gem Ancestry hỗ trợ khá tốt vấn đề này, việc hiểu rõ hơn về gem này giúp bạn chủ động hơn trong công việc Link: https://github.com/stefankroes/ancestry

Gem Ancestry khá giống gem Paranoia, nghĩa laf cũng tạo thêm 1 method trong ActiveRecord::Base

class << ActiveRecord::Base
  def has_ancestry options = {}
  ...
  end
end

Ngoài console ta có thể add ancestry cho 1 model 1 cách dynamically bằng cách gọi hàm has_ancestry

Order.has_ancestry
 => [Order(id: integer, course_order_id: ...
2.3.0 :012 > Order.roots
  Order Load (18.7ms)  SELECT "orders".* FROM "orders" WHERE "orders"."deleted_at" IS NULL AND "orders"."ancestry" IS NULL
ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR:  column orders.ancestry does not exist

Tuy nhiên do options đưa vào chưa có chỉ định ancestry_column nên câu query chưa đúng, gỉa sử ta dùng cột uid (cột phải có dạng string) là cột lưu ancestry_column

 Order.has_ancestry ancestry_column: :uid
 => [Order(id: integer, course_order_id: integer...
Order.roots
  Order Load (0.5ms)  SELECT "orders".* FROM "orders" WHERE "orders"."deleted_at" IS NULL AND "orders"."uid" IS NULL

Ta thấy câu query đã đúng, cột lưu ancestry_column đã được chỉ định trong câu query. Ta có thể lấy tên cột này bằng cáh gọi ancestry_column

 Order.ancestry_column
 => :uid 

Các câu query của gem Ancestry: https://github.com/stefankroes/ancestry/blob/master/lib/ancestry/has_ancestry.rb#L43

    scope :roots, lambda { where(ancestry_column => nil) }
    scope :ancestors_of, lambda { |object| where(ancestor_conditions(object)) }
    scope :path_of, lambda { |object| where(path_conditions(object)) }
    scope :children_of, lambda { |object| where(child_conditions(object)) }
    scope :descendants_of, lambda { |object| where(descendant_conditions(object)) }
    scope :subtree_of, lambda { |object| where(subtree_conditions(object)) }
    scope :siblings_of, lambda { |object| where(sibling_conditions(object)) }
    scope :ordered_by_ancestry, lambda {
      if %w(mysql mysql2 sqlite postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::MAJOR >= 5
        reorder("coalesce(#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}, '')")
      else
        reorder("(CASE WHEN #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)} IS NULL THEN 0 ELSE 1 END), #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}")
      end
    }
    scope :ordered_by_ancestry_and, lambda { |order|
      if %w(mysql mysql2 sqlite postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::MAJOR >= 5
        reorder("coalesce(#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}, ''), #{order}")
      else
        reorder("(CASE WHEN #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)} IS NULL THEN 0 ELSE 1 END), #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}, #{order}")
      end
    }
    scope :path_of, lambda { |object| to_node(object).path }

Hàm ancestor_conditions nằm tại https://github.com/stefankroes/ancestry/blob/master/lib/ancestry/instance_methods.rb#L110 Tại console ta có thể gọi như sau:

Order.first.ancestor_conditions
  Order Load (1.3ms)  SELECT  "orders".* FROM "orders" WHERE "orders"."deleted_at" IS NULL  ORDER BY "orders"."id" ASC LIMIT 1
 => #<Arel::Nodes::In:0x0000000987a4c0 @left=#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x0000000640d980 @name="orders", @engine=Order(id: integer, course_order_id: integer, user_id: integer, created_at: datetime, updated_at: datetime, data_import_result_id: integer, deleted_at: datetime, total: decimal, item_total: decimal, shipment_total: decimal, payment_total: decimal, adjustment_total: decimal, tax_total: decimal, status: integer, uid: string, staff_id: integer, order_merge_bundler_id: integer, order_split_bundler_id: integer, canceled_on: date, ordered_on: date, author_id: integer, order_type_id: integer, device_kind: integer, is_latest: boolean, previous_uid: string, is_allocated: boolean, shop_id: integer, continuation_times: integer, currency_id: integer, lock_version: integer), @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name=:id>, @right=[#<Arel::Nodes::Casted:0x0000000987a4e8 @val=0, @attribute=#<struct Arel::Attributes::Attribute relation=#<Arel::Table:0x0000000640d980 @name="orders", @engine=Order(id: integer, course_order_id: integer, user_id: integer, created_at: datetime, updated_at: datetime, data_import_result_id: integer, deleted_at: datetime, total: decimal, item_total: decimal, shipment_total: decimal, payment_total: decimal, adjustment_total: decimal, tax_total: decimal, status: integer, uid: string, staff_id: integer, order_merge_bundler_id: integer, order_split_bundler_id: integer, canceled_on: date, ordered_on: date, author_id: integer, order_type_id: integer, device_kind: integer, is_latest: boolean, previous_uid: string, is_allocated: boolean, shop_id: integer, continuation_times: integer, currency_id: integer, lock_version: integer), @columns=nil, @aliases=[], @table_alias=nil, @primary_key=nil>, name=:id>>]>

Gía trị trả về dạng Arel, mình đã có 1 bàn viết sơ qua về dạng này (https://viblo.asia/pham.huy.cuong/posts/7prv31LkMKod)

Các hàm ancestor_conditions, path_conditions, child_conditions, descendant_conditions, subtree_conditions, sibling_conditions được đặt tại https://github.com/stefankroes/ancestry/blob/master/lib/ancestry/instance_methods.rb và đều có thể gọi theo cách trên, gía trị trả về là dạng Arel.

Cột ancestry_column được validate format ở: https://github.com/stefankroes/ancestry/blob/master/lib/ancestry/has_ancestry.rb#L37

validates_format_of ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true

Ancestry::ANCESTRY_PATTERN: /\A[0-9]+(\/[0-9]+)*\Z/

Validate xem id của recordcó thuộc mảng id những record cha của nó (phần này nhằm tránh vòng lặp vô cùng, ví dụ gía trị của ancestry_column1/4/8/9 mà id của record là 4 thì sẽ raise ra lỗi): https://github.com/stefankroes/ancestry/blob/master/lib/ancestry/has_ancestry.rb#L40

validate :ancestry_exclude_self

Hàm ancestry_exclude_self nằm tại: https://github.com/stefankroes/ancestry/blob/master/lib/ancestry/instance_methods.rb#L4

def ancestry_exclude_self
  errors.add(:base, "#{self.class.name.humanize} cannot be a descendant of itself.") if ancestor_ids.include? self.id
end

Ví dụ đối với model StockLocation:

StockLocation.last
  StockLocation Load (0.6ms)  SELECT  "stock_locations".* FROM "stock_locations"  ORDER BY "stock_locations"."id" DESC LIMIT 1
 => #<StockLocation id: 36, name: "Local 36", code: "A3600", ancestry: "30/34", priority: 36, warehouse_id: 1, created_at: "2017-01-16 07:49:24", updated_at: "2017-01-16 07:49:24", is_folder: false, position: 2>
 > StockLocation.last.valid?
  StockLocation Load (1.1ms)  SELECT  "stock_locations".* FROM "stock_locations"  ORDER BY "stock_locations"."id" DESC LIMIT 1
 => true 

Ta thử set lại gía trị cội ancestry với gía trị bao gồm id của record này

stock_location = StockLocation.last
  StockLocation Load (1.0ms)  SELECT  "stock_locations".* FROM "stock_locations"  ORDER BY "stock_locations"."id" DESC LIMIT 1
 => #<StockLocation id: 36, name: "Local 36", code: "A3600", ancestry: "30/34", priority: 36, warehouse_id: 1, created_at: "2017-01-16 07:49:24", updated_at: "2017-01-16 07:49:24", is_folder: false, position: 2> 
> stock_location.ancestry = "30/36/34"
 => "30/36/34" 
> stock_location.valid?
 => false 

Cảm ơn và hi vọng bài viết có ích trong công việc của bạn.


All Rights Reserved