From e9bee20fad24e77f526bbd4df1c1aac90cf39f5f Mon Sep 17 00:00:00 2001 From: Ben Nappier Date: Sun, 23 Apr 2023 12:04:31 -0600 Subject: [PATCH 1/2] initial go at making things polymorphic --- lib/closure_tree/hierarchy_maintenance.rb | 16 +++++++++------- lib/closure_tree/model.rb | 13 ++++++++----- lib/closure_tree/support.rb | 17 ++++++++++++++--- .../templates/create_hierarchies_table.rb.erb | 4 +++- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/lib/closure_tree/hierarchy_maintenance.rb b/lib/closure_tree/hierarchy_maintenance.rb index f7c5492d..41107634 100644 --- a/lib/closure_tree/hierarchy_maintenance.rb +++ b/lib/closure_tree/hierarchy_maintenance.rb @@ -66,10 +66,10 @@ def rebuild!(called_by_rebuild = false) unless root? _ct.connection.execute <<-SQL.squish INSERT INTO #{_ct.quoted_hierarchy_table_name} - (ancestor_id, descendant_id, generations) - SELECT x.ancestor_id, #{_ct.quote(_ct_id)}, x.generations + 1 + (ancestor_id, ancestor_type, descendant_id, descendant_type, generations) + SELECT x.ancestor_id, x.ancestor_type, #{_ct.quote(_ct_id)}, x.descendant_type, x.generations + 1 FROM #{_ct.quoted_hierarchy_table_name} x - WHERE x.descendant_id = #{_ct.quote(_ct_parent_id)} + WHERE x.descendant_id = #{_ct.quote(_ct_parent_id)} AND x.descendant_type = #{_ct.quote(self.class.to_s)} SQL end @@ -97,8 +97,8 @@ def delete_hierarchy_references SELECT DISTINCT descendant_id FROM (SELECT descendant_id FROM #{_ct.quoted_hierarchy_table_name} - WHERE ancestor_id = #{_ct.quote(id)} - OR descendant_id = #{_ct.quote(id)} + WHERE (ancestor_id = #{_ct.quote(id)} AND ancestor_type = #{_ct.quote(self.class.to_s)}) + OR (descendant_id = #{_ct.quote(id)} AND descendant_type = #{_ct.quote(self.class.to_s)}) ) #{ _ct.t_alias_keyword } x ) SQL end @@ -119,13 +119,15 @@ def cleanup! hierarchy_table = hierarchy_class.arel_table [:descendant_id, :ancestor_id].each do |foreign_key| - alias_name = foreign_key.to_s.split('_').first + "s" + key_prefix = foreign_key.to_s.split('_') + key_as_type = "#{key_prefix}_type".to_sym + alias_name = key_prefix.first + "s" alias_table = Arel::Table.new(table_name).alias(alias_name) arel_join = hierarchy_table.join(alias_table, Arel::Nodes::OuterJoin) .on(alias_table[primary_key].eq(hierarchy_table[foreign_key])) .join_sources - lonely_childs = hierarchy_class.joins(arel_join).where(alias_table[primary_key].eq(nil)) + lonely_childs = hierarchy_class.joins(arel_join).where(alias_table[primary_key].eq(nil)).where(key_as_type => self.class.to_s) ids = lonely_childs.pluck(foreign_key) hierarchy_class.where(hierarchy_table[foreign_key].in(ids)).delete_all diff --git a/lib/closure_tree/model.rb b/lib/closure_tree/model.rb index cc64ae4d..3875d739 100644 --- a/lib/closure_tree/model.rb +++ b/lib/closure_tree/model.rb @@ -11,11 +11,14 @@ module Model foreign_key: _ct.parent_column_name, inverse_of: :children, touch: _ct.options[:touch], - optional: true + optional: true, + polymorphic: true + where_for_ancestors = { ancestor_type: _ct.model_class.to_s } + where_for_descendants = { descendant_type: _ct.model_class.to_s } order_by_generations = -> { Arel.sql("#{_ct.quoted_hierarchy_table_name}.generations ASC") } - has_many :children, *_ct.has_many_order_with_option, **{ + has_many :children, *_ct.has_many_order_with_option_and_where({ parent_type: _ct.model_class.to_s }), **{ class_name: _ct.model_class.to_s, foreign_key: _ct.parent_column_name, dependent: _ct.options[:dependent], @@ -28,7 +31,7 @@ def hash_tree(options = {}) end end - has_many :ancestor_hierarchies, *_ct.has_many_order_without_option(order_by_generations), + has_many :ancestor_hierarchies, *_ct.has_many_order_without_option_and_where(order_by_generations, where_for_ancestors), class_name: _ct.hierarchy_class_name, foreign_key: 'descendant_id' @@ -36,13 +39,13 @@ def hash_tree(options = {}) through: :ancestor_hierarchies, source: :ancestor - has_many :descendant_hierarchies, *_ct.has_many_order_without_option(order_by_generations), + has_many :descendant_hierarchies, *_ct.has_many_order_without_option_and_where(order_by_generations, where_for_descendants), class_name: _ct.hierarchy_class_name, foreign_key: 'ancestor_id' has_many :self_and_descendants, *_ct.has_many_order_with_option(order_by_generations), through: :descendant_hierarchies, - source: :descendant + source: :descendant, source_type: _ct.model_class.to_s end # Delegate to the Support instance on the class: diff --git a/lib/closure_tree/support.rb b/lib/closure_tree/support.rb index e887090e..df3e4b00 100644 --- a/lib/closure_tree/support.rb +++ b/lib/closure_tree/support.rb @@ -36,11 +36,10 @@ def hierarchy_class_for_model hierarchy_class = parent_class.const_set(short_hierarchy_class_name, Class.new(model_class.superclass)) use_attr_accessible = use_attr_accessible? include_forbidden_attributes_protection = include_forbidden_attributes_protection? - model_class_name = model_class.to_s hierarchy_class.class_eval do include ActiveModel::ForbiddenAttributesProtection if include_forbidden_attributes_protection - belongs_to :ancestor, class_name: model_class_name - belongs_to :descendant, class_name: model_class_name + belongs_to :ancestor, polymorphic: true + belongs_to :descendant, polymorphic: true attr_accessible :ancestor, :descendant, :generations if use_attr_accessible def ==(other) self.class == other.class && ancestor_id == other.ancestor_id && descendant_id == other.descendant_id @@ -84,6 +83,10 @@ def has_many_order_without_option(order_by_opt) [lambda { order(order_by_opt.call) }] end + def has_many_order_without_option_and_where(order_by_opt, where_clause) + [lambda { where(where_clause).order(order_by_opt.call) }] + end + def has_many_order_with_option(order_by_opt=nil) order_options = [order_by_opt, order_by].compact [lambda { @@ -92,6 +95,14 @@ def has_many_order_with_option(order_by_opt=nil) }] end + def has_many_order_with_option_and_where(order_by_opt=nil, where_clause) + order_options = [order_by_opt, order_by].compact + [lambda { + order_options = order_options.map { |o| o.is_a?(Proc) ? o.call : o } + where(where_clause).order(order_options) + }] + end + def ids_from(scope) scope.pluck(model_class.primary_key) end diff --git a/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb b/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb index 18d7671c..470e6e94 100644 --- a/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +++ b/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb @@ -2,11 +2,13 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version def change create_table :<%= migration_name %>, id: false do |t| t.<%= primary_key_type %> :ancestor_id, null: false + t.string :ancestor_type, null: false t.<%= primary_key_type %> :descendant_id, null: false + t.string :descendant_type, null: false t.integer :generations, null: false end - add_index :<%= migration_name %>, [:ancestor_id, :descendant_id, :generations], + add_index :<%= migration_name %>, [:ancestor_id, :ancestor_type, :descendant_id, :descendant_type, :generations], unique: true, name: "<%= file_name %>_anc_desc_idx" From 1c5c4d14b77e8d35e275ff94a44ad97a55471963 Mon Sep 17 00:00:00 2001 From: Ben Nappier Date: Mon, 24 Apr 2023 15:28:40 -0600 Subject: [PATCH 2/2] make some fixes and add poly_children helper --- lib/closure_tree/hierarchy_maintenance.rb | 4 ++-- lib/closure_tree/model.rb | 15 ++++++++++++++- lib/closure_tree/support.rb | 3 +-- lib/closure_tree/support_attributes.rb | 2 +- .../templates/create_hierarchies_table.rb.erb | 10 +++++----- 5 files changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/closure_tree/hierarchy_maintenance.rb b/lib/closure_tree/hierarchy_maintenance.rb index 41107634..2278b5a9 100644 --- a/lib/closure_tree/hierarchy_maintenance.rb +++ b/lib/closure_tree/hierarchy_maintenance.rb @@ -67,7 +67,7 @@ def rebuild!(called_by_rebuild = false) _ct.connection.execute <<-SQL.squish INSERT INTO #{_ct.quoted_hierarchy_table_name} (ancestor_id, ancestor_type, descendant_id, descendant_type, generations) - SELECT x.ancestor_id, x.ancestor_type, #{_ct.quote(_ct_id)}, x.descendant_type, x.generations + 1 + SELECT #{_ct.quote(parent.id)}, #{_ct.quote(parent.class.to_s)}, #{_ct.quote(_ct_id)}, x.descendant_type, x.generations + 1 FROM #{_ct.quoted_hierarchy_table_name} x WHERE x.descendant_id = #{_ct.quote(_ct_parent_id)} AND x.descendant_type = #{_ct.quote(self.class.to_s)} SQL @@ -79,7 +79,7 @@ def rebuild!(called_by_rebuild = false) _ct_reorder_siblings if !called_by_rebuild end - children.find_each { |c| c.rebuild!(true) } + children.each { |c| c.rebuild!(true) } _ct_reorder_children if _ct.order_is_numeric? && children.present? end diff --git a/lib/closure_tree/model.rb b/lib/closure_tree/model.rb index 3875d739..3dd1c76e 100644 --- a/lib/closure_tree/model.rb +++ b/lib/closure_tree/model.rb @@ -5,7 +5,6 @@ module Model extend ActiveSupport::Concern included do - belongs_to :parent, nil, class_name: _ct.model_class.to_s, foreign_key: _ct.parent_column_name, @@ -48,6 +47,20 @@ def hash_tree(options = {}) source: :descendant, source_type: _ct.model_class.to_s end + def poly_children + pp hierarchy_class.all.map { |h| h.slice(:ancestor_type, :ancestor_id, :descendant_type, :descendant_id, :generations) } + + foo = hierarchy_class + .where(ancestor_id: id, ancestor_type: _ct.model_class.to_s, generations: 1) + .map { |h_data| { type: h_data.descendant_type, id: h_data.descendant_id } } + .group_by { |h_data| h_data[:type] } + .map { |type, h_data_array| type.constantize.where(id: h_data_array.map { |h_data| h_data[:id] }) } + .flatten + + p foo + foo + end + # Delegate to the Support instance on the class: def _ct self.class._ct diff --git a/lib/closure_tree/support.rb b/lib/closure_tree/support.rb index df3e4b00..f4c409ae 100644 --- a/lib/closure_tree/support.rb +++ b/lib/closure_tree/support.rb @@ -57,8 +57,7 @@ def hierarchy_table_name # We need to use the table_name, not something like ct_class.to_s.demodulize + "_hierarchies", # because they may have overridden the table name, which is what we want to be consistent with # in order for the schema to make sense. - tablename = options[:hierarchy_table_name] || - remove_prefix_and_suffix(table_name).singularize + "_hierarchies" + tablename = "hierarchies" ActiveRecord::Base.table_name_prefix + tablename + ActiveRecord::Base.table_name_suffix end diff --git a/lib/closure_tree/support_attributes.rb b/lib/closure_tree/support_attributes.rb index 38879ade..162518fa 100644 --- a/lib/closure_tree/support_attributes.rb +++ b/lib/closure_tree/support_attributes.rb @@ -17,7 +17,7 @@ def quoted_value(value) end def hierarchy_class_name - options[:hierarchy_class_name] || model_class.to_s + "Hierarchy" + "Hierarchy" end def primary_key_column diff --git a/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb b/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb index 470e6e94..56f69173 100644 --- a/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb +++ b/lib/generators/closure_tree/templates/create_hierarchies_table.rb.erb @@ -1,6 +1,6 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %> def change - create_table :<%= migration_name %>, id: false do |t| + create_table :hierarchies, id: false do |t| t.<%= primary_key_type %> :ancestor_id, null: false t.string :ancestor_type, null: false t.<%= primary_key_type %> :descendant_id, null: false @@ -8,11 +8,11 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version t.integer :generations, null: false end - add_index :<%= migration_name %>, [:ancestor_id, :ancestor_type, :descendant_id, :descendant_type, :generations], + add_index :hierarchies, [:ancestor_id, :ancestor_type, :descendant_id, :descendant_type, :generations], unique: true, - name: "<%= file_name %>_anc_desc_idx" + name: "anc_desc_idx" - add_index :<%= migration_name -%>, [:descendant_id], - name: "<%= file_name %>_desc_idx" + add_index :hierarchies, [:descendant_id], + name: "desc_idx" end end