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 ...
Đô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]+)*/
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_column là 1/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.