ActiveRecordで作った木構造のリーフのみ取りたい。

やりたい

has_many関連がempty?を返すようなレコードを上から一気に取りたいですね。

前提

class Node < ActiveRecord::Base
  belongs_to :owner
  belongs_to :parent, class_name: Node, foreign_key: :node_id
  has_many :nodes

  # 略
end

Nodeは必ずownerをもち、木の取得に使いますので、この話では木自体の取得は扱いません(データベースを用いた木構造のさまざまな話はSQLアンチパターンに色々載っていておもしろいですよ)

クエリがいっぱい出るメソッド

ownerからルートを取得し、以下のようなメソッドで掘っていく方法があります。

class Node < ActiveRecord::Base
  # 略

  def leaf?
    nodes.empty?
  end

  def pick_leaves(picked = [])
    return picked << self if leaf?

    nodes.includes(:nodes).inject(picked) do |a, node|
      node.pick_leaves(a)
    end
  end
end

(オーバーフローするかしら)

1クエリでとる

今回は木の構造は問題にされておらず、ただリーフが欲しいだけですので、比較的に簡単に取得できます。

owner配下のNodeをマトにして、子ノードを持たないノードを返す、というクエリをなげます。

class Owner < ActiveRecord::Base
  has_many :nodes

  def leaves
    nodes.includes(:nodes).references(:nodes).where(nodes_nodes: {id: nil})
  end
end

includes(:nodes).references(:nodes)LEFT OUTER JOINを行います。全てのNodeから子ノードが参照できる状態になるので、子ノードがいない、つまりリーフを抽出します。

なおjoinsを用いたINNER JOINでは、逆にリーフが除外されます。