1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

has_manyなassociationでexistsを使ったサブクエリをscopeで書いてみる。

Posted at

正直言って既出な感が強いのですが、個人的な備忘として残しておきます。

前提

class Parent < ActiveRecord::Base
  has_many :children, :dependent => :destroy

  def finished?
    children.unfinished.count == 0
  end
end

class Child < ActiveRecord
  belongs_to :parent

  scope :finished,   ->{ where( arel_table[:finished].eq(true) )  }
  scope :unfinished, ->{ where( arel_table[:finished].eq(false) ) }
end

Childは、booleanのfinishedフラグを持っており、Parentは、has_manyなchildrenがすべてfinished = trueである時にfinishedであり、それ以外はunfinishedであるとします。
この状態でunfinishedなParentをすべて取得したいという場合、SQLではexistsを使ってこんな感じに書けると思います。

SELECT `parents`.* FROM `parents` 
WHERE EXISTS (
  SELECT `children`.* FROM `children` 
  WHERE `children`.`finished` = 0 
  AND `parents`.`id` = `children`.`parent_id`
)

しかし、まんまSQLで書いたのではRailsの意味がありませんので、ActiveRecordのscopeで書いて、こんな感じで呼び出せるようにしたいです。

Parent.unfinished

とりあえず書いてみる。

というわけでscopeとして書いてみると、こんな感じでしょうか?

class Parent < ActiveRecord::Base
  has_many :children, :dependent => :destroy

  def finished?
    children.unfinished.count == 0
  end

  scope :finished,   ->{
    children_table = Child.arel_table
    condition = Child.where( children_table[:parent_id].eq( arel_table[:id] ).and( children_table[:finished].eq(false) ) )
    where( condition.exists.not )
  }

  scope :unfinished,   ->{
    children_table = Child.arel_table
    condition = Child.where( children_table[:parent_id].eq( arel_table[:id] ).and( children_table[:finished].eq(false) ) )
    where( condition.exists )
  }
end

class Child < ActiveRecord
  belongs_to :parent

  scope :finished,   ->{ where( arel_table[:finished].eq(true) )  }
  scope :unfinished, ->{ where( arel_table[:finished].eq(false) ) }
end

しかし、OOPのカプセル化的にはChildがfinishとなるchildren.finished=trueという条件は、Childだけが知っていればいいことですので、Parent側に持つのは微妙かなーと。
というわけでconditionはChildに持ってもらいましょう。

条件部分だけをChildに移す。

class Parent < ActiveRecord::Base
  has_many :children, :dependent => :destroy

  def finished?
    children.unfinished.count == 0
  end

  scope :finished,   ->{ where( Child.where_unfinished_not_exists ) }
  scope :unfinished, ->{ where( Child.where_unfinished_exists ) }
end

class Child < ActiveRecord
  belongs_to :parent

  scope :finished,   ->{ where( arel_table[:finished].eq(true) )  }
  scope :unfinished, ->{ where( arel_table[:finished].eq(false) ) }

  scope :where_unfinished_exists, ->{ unfinished.where( Parent.arel_table[:id].eq( arel_table[:parent_id] ) ).exists }
  scope :where_unfinished_not_exists, ->{ unfinished.where( Parent.arel_table[:id].eq( arel_table[:parent_id] ) ).exists.not }
end

ついでなので、Parent.arel_table[:id].eq( Child.arel_table[:parent_id] )はParent側からscopeの引数として渡せるようにしてしまえば、
他のClassからwhere_unfinished_exists等を使いたい、という場合には再利用できますね。まぁ恐らくしないでしょうが(笑

条件を引数化する。

というわけで、最終的にはこうなりました。

class Parent < ActiveRecord::Base
  has_many :children, :dependent => :destroy

  def finished?
    children.unfinished.count == 0
  end

  children_join_condition = arel_table[:id].eq( Child.arel_table[:parent_id] )
  scope :finished,   ->{ where( Child.where_unfinished_not_exists( children_join_condition ) ) }
  scope :unfinished, ->{ where( Child.where_unfinished_exists( children_join_condition ) ) }
end

class Child < ActiveRecord
  belongs_to :parent

  scope :finished,   ->{ where( arel_table[:finished].eq(true) )  }
  scope :unfinished, ->{ where( arel_table[:finished].eq(false) ) }

  scope :where_unfinished_exists, ->(cond){ unfinished.where( cond ).exists }
  scope :where_unfinished_not_exists, ->(cond){ unfinished.where( cond ).exists.not }
end

しかし、Parentのchildren_join_conditionがどうも冗長なんですよね。
Parentはchildrenのアソシエーションを持ってるから、そこから取り出せるんじゃ?とか思ったんですが上手くいきませんでした。

誰かいい方法をご存じないでしょうか?

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?