SQLの条件を組み立てるRailsプラグイン Condition Builder 1.1
RailsでSQLの条件を扱う、Condition Builderプラグインが便利です。
- 最新版(1.1)
- 説明
- 以前の記事
Conditionクラスのメソッド一覧
通常、ActiveRecordに条件を指定する場合、:conditionsオプションで ["a = ? AND b = ?", 1 , 2] のような配列を指定します。この配列の構築をサポートしてくれるのがConditionBuilderプラグインです。
Condition BuilderはクラスConditionを定義します。
- Condition.block
- クラスメソッドblockは、渡されたブロックでConditionオブジェクトを作成した後、whereメソッドの結果を返します。
- Condition#initialize
- newにブロックが渡された場合、作成されたConditionインスタンスを引数にしてブロックが実行されます。(オブジェクトの作成後にブロックの中身を実行するのとほとんど一緒です。)
- Condition#and
- ANDで接続される条件を追加します。
- Condition#or
- ORで接続される条件を追加します。
- Condition#block
- 渡されたブロックで新たにConditionインスタンスを作成し、これを条件として追加します。AND/ORのどちらで条件が接続されるか曖昧になるため、少なくとも現バージョンではこのメソッドを直接使うべきではありません。代わりにand/orメソッドを使います。
- Condition#where
- ActiveRecordに渡す条件(:conditionsで指定する配列)を返します。
基本的な使い方
Foo.find(:all, :conditions => Condition.block do |c| c.and "a", 1 c.and "b", 2 c.and do |c1| c1.or "c", 3 c1.or "d", 4 end end )
クラスメソッドのblockは、Conditionオブジェクトを構築したあと、:conditionsに渡すための配列を返します。:conditionsは以下の値になります。
:conditions => ["a = ? AND b = ? AND ( c = ? OR d = ? )", 1, 2, 3, 4]
new / where
newで作成して、whereで条件を取得する例。
c = Condition.new c.or "a", 1 c.or "b", 2 Foo.find(:all, :conditions => c.where) # :conditions => ["a = ? OR b = ?", 1, 2]
その他応用
条件の合成、IN、BETWEENの指定方法などの例。サンプルコードにRSpecを利用しています。
require File.dirname(__FILE__) + '/../spec_helper' describe Condition do before(:each) do @condition = Condition.new end it "2つのConditionを合成できる" do c1 = Condition.new c2 = Condition.new c1.or "x", 10 c1.or "y", 20 c2.and "a", 1 c2.and "b", "IN", [2, 3, 4] c2.and c1 # ← c1をsub-conditionにする。 c2.where.should == ["a = ? AND b IN (?) AND (x = ? OR y = ?)", 1, [2, 3, 4], 10, 20] end it "2番目の引数が配列だとINになる" do c = @condition c.and "a", [1, 2, 3] # ← 2番目が配列 c.where.should == ["a IN (?)", [1, 2, 3]] end it "3つの引数を渡してIN, BETWEENなどが使える" do c = @condition c.and "a", "LIKE", "%abc%" c.and "b", "IN", [1, 2, 3] c.and "c", "BETWEEN", 4, 5 c.and "d", ">=", 6 c.where.should == [ "a LIKE ? AND b IN (?) AND c BETWEEN ? AND ? AND d >= ?", "%abc%", [1, 2, 3], 4, 5, 6 ] end it "カラム名をsqlにするとSQLを直に書ける" do c = @condition c.and "sql", "abc = 123" c.block do |d| d.or "x", 1 d.or "y", 2 end c.where.should == ["abc = 123 AND (x = ? OR y = ?)", 1, 2] end it "使えるオペレータが決まっているわけではない" do c = @condition c.and "x", "DUMMY OP", 123 c.where.should == ["x DUMMY OP ?", 123] end # OR関連のテスト。1.0のバグ。1.1で修正済み。 it "orでandのブロックを連結。(A AND B) OR (C AND D)" do c = @condition c.or {|d| d.and "a", 1 d.and "b", 2 } c.or {|d| d.and "c", 3 d.and "d", 4 } c.where.should == ["(a = ? AND b = ?) OR (c = ? AND d = ?)", 1, 2, 3, 4] end it "orで連結。A OR (B AND C) OR (D AND E)" do c = @condition c.block {|d| d.or 1, 1 d.or {|e| e.and "a", 1 e.and "b", 2 } d.or {|e| e.and "c", 3 e.and "d", 4 } } c.where.should == ["(1 = ? OR (a = ? AND b = ?) OR (c = ? AND d = ?))", 1, 1, 2, 3, 4] end it "orで連結。(A AND B) OR C" do c = @condition c.block {|d| d.or {|e| e.and "a", 1 e.and "b", 2 } d.or "c", 3 } c.where.should == ["((a = ? AND b = ?) OR c = ?)", 1, 2, 3] end end
制限など
Conditionオブジェクトは@logicに"AND"/"OR"のどちらかの値を持っており、この値でブロック内の条件が接続されます。@logicの値は、最後にand/orのどちらのメソッドが呼ばれたかによって決まります。このため、1つのブロック内で、and/orを混在させることはできません。
以下のようにすべきではない、という説明が配布元のブログに載っています。
Condition.block { |c| c.and "user_id", 1 c.and "book_id", 2 c.or "company_id", 3 } # BAD # Output will be: ["user_id = ? OR book_id = ? OR company_id = ?", 1, 2, 3]
かわりにこうします。
# Do this instead: Condition.block { |c| c.and "user_id", 1 c.and { |d| d.or "book_id", 2 d.or "company_id", 3 } } # OK # Output will be: ["user_id = ? AND (book_id = ? OR company_id = ?)", 1, 2, 3]
1.0にはORでブロックをつなげないバグがありましたが、1.1で修正されました。
上で説明したとおり、インスタンスメソッドの方のblockメソッド*1は、現時点では使用しないほうがいいと思います(privateメソッドが適切な気がします)。
Condition#blockには、以下のような問題があります。
>> c = Condition.new => #<Condition:0x7f14c060 @args=[]> >> c.block {|c1| c1.and "a", 1} => ... >> c.block {|c1| c1.and "b", 2} => ... >> c.where => ["(a = ?)(b = ?)", 1, 2] # 条件を接続する AND/OR が無い。
代わりにand、orメソッドを使います。
>> c = Condition.new => #<Condition:0x7f1352c0 @args=[]> >> c.and {|c1| c1.and "a", 1} => ... >> c.and {|c1| c1.and "b", 2} => ... >> c.where => ["(a = ?) AND (b = ?)", 1, 2]
*1:クラスメソッドではありません。クラスメソッドのCondition.blockは普通に使います