Rails充当has_many关系中所有记录的嵌套集返回后代

时间:2015-09-20 22:24:48

标签: ruby-on-rails acts-as-nested-set

我正在使用awesome_nested_set gem并尝试获取has_many关系的所有后代。用户有许多共享任务,每个任务都有很多孩子。我可以获得一个用户task_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调用的情况下执行此操作。

1 个答案:

答案 0 :(得分: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])

这只能在一次查询中完成工作!