1

我正在使用 awesome_nested_set gem,并试图获取 has_many 关系的所有后代。一个用户有许多任务共享给他们,每个任务都有很多孩子。我如何获得用户 tasks_shared_to 及其所有孩子的列表(请注意,孩子不会单独与用户共享)?

用户型号:

has_many :tasks_shared_to, through: :user_task_shares, source: :task

任务模型:

acts_as_nested_set
belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by_id'
belongs_to :parent, :class_name => 'Task', :foreign_key => 'parent_id'

我知道这很容易通过每个 tasks_shared_to 循环然后调用来完成,self_and_descendants但我希望能够做到这一点,而无需在每个循环中进行单独的 SQL 调用。

4

1 回答 1

0

我最终放弃了 awesome_nested_set gem,然后浏览了这个优秀的博客:

http://hashrocket.com/blog/posts/recursive-sql-in-activerecord

然后我修改了类方法以接受一个对象数组,提取到一个模块并添加了一些方法,如下所示:

module RecursiveTree
  def self.included(recipient)
    recipient.extend(ModelClassMethods)
  end

  module ModelClassMethods
    def tree_for_ids(methods)
      self.where("#{table_name}.id IN (#{self.tree_sql_for_ids(self.sql_clean(methods))})")
    end

    def tree_for_id(id)
      self.where("#{table_name}.id IN (#{self.tree_sql_for_ids("id = #{id}")})")
    end

    def sql_clean(methods)
      sql_to_exec = ""
      methods.each do |method|
        sql_string = method.to_sql #note we can pass methods and they will not get executed unless we chain something to them - because of lazy instatiation
        #remove everything after the "ORDER" string as can't order more than once and order doesnt matter anyway
        sql_string = sql_string.slice(0..(sql_string.index('ORDER')-2)) unless sql_string.index('ORDER').nil?
        #add an OR to the front if there is already some text in the sql_to_exec
        sql_to_exec += "#{" OR " unless sql_to_exec.empty?}#{table_name}.id IN (#{sql_string})"
      end
      sql_to_exec.gsub! '*', 'id' #sub to only return the ids
    end

    def tree_sql_for_ids(sql_id_array)
      tree_sql = sql_id_array ? <<-SQL
        WITH RECURSIVE search_tree(id, path) AS (
            SELECT id, ARRAY[id]
            FROM #{table_name}
            WHERE #{sql_id_array}
          UNION ALL
            SELECT #{table_name}.id, path || #{table_name}.id
            FROM search_tree
            JOIN #{table_name} ON #{table_name}.parent_id = search_tree.id
            WHERE NOT #{table_name}.id = ANY(path)
        )
        SELECT id FROM search_tree ORDER BY path
      SQL
      : "NULL"
    end

    def ancestors_sql_for(instance)
      ancestors_sql = instance.id ? <<-SQL
        WITH RECURSIVE search_tree(id, path) AS (
            SELECT parent_id, ARRAY[parent_id]
            FROM #{table_name}
            WHERE id = #{instance.id}
          UNION ALL
            SELECT #{table_name}.parent_id, path || #{table_name}.parent_id
            FROM search_tree
            JOIN #{table_name} ON #{table_name}.id = search_tree.id
            WHERE NOT (#{table_name}.parent_id IS NULL)
        )
        SELECT id FROM search_tree ORDER BY path
      SQL
      : "NULL"
    end
  end

  #instance methods go here:
  def ancestors
    self.class.where("#{self.class.table_name}.id IN (#{self.class.ancestors_sql_for(self)})").order("#{self.class.table_name}.id")
  end

  def ancestor_complete
    ancestors.where(complete: true).any?
  end

  def top_level_parent
    is_top_level_parent? ? self : self.ancestors.where(parent_id: nil).first
  end

  def is_top_level_parent?
    self.parent.nil?
  end

  def descendants
    self.id ? self_and_descendants.where.not(id: self.id) : self.class.none #self.class.none return an empty ActiveRecord relation
  end

  def self_and_descendants
    self.id ? self.class.tree_for_id(self.id) : self.class.none #self.class.none return an empty ActiveRecord relation
  end
end

然后包含在我的任务模型中:

class Task < ActiveRecord::Base
  include RecursiveTree #in the lib folder
...
end

十个我可以用任意数量的方法调用,例如:

u1 = User.find(1)
Task.tree_for_ids([u1.tasks_shared_to, u1.tasks_shared_to_through_groups])

只需 1 个查询即可完成工作!

于 2015-09-20T23:44:51.383 回答