正直言って既出な感が強いのですが、個人的な備忘として残しておきます。
前提
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のアソシエーションを持ってるから、そこから取り出せるんじゃ?とか思ったんですが上手くいきませんでした。
誰かいい方法をご存じないでしょうか?