読者です 読者をやめる 読者になる 読者になる

SQLの条件を組み立てるRailsプラグイン Condition Builder 1.1

Ruby Rails

RailsSQLの条件を扱う、Condition Builderプラグインが便利です。

screenshot


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は普通に使います