SELECT文生成クラス

検索条件がややこしい場合に、SELECT文を作ろうとすると非常にややこしくなった経験から、SELECT文をすっきり書くためのクラスを作ってみました。使い方はこんな感じ。

# タイトルで本を検索
select = Select.new("b.id, b.isbn, b.title")
select.from("books").as("b")
select.where("b.title like '%フーコー%'")
if 著者名も指定されていた場合
  select.from.get("b").inner_join("authors").as("a").on("b.author_id = a.id")
  select.where.and("a.name like '%エーコ%'")
end
Book.find_by_sql(select.to_s)

けっこー直感的でいい感じかと思うんですけど、どうでしょう?きっとこういうのが既にあるんじゃないかなと思いつつ、見つからなかったんで勉強も兼ねて書いてみました。select.fromの戻り値の型が引数の有無で違うっていうのがちょっと気持ち悪い気もしますが直感を優先しました。

以下、テストケースとソースコードっす。
select_test.rb

# copyright akima
require 'select'

class SelectTest < Test::Unit::TestCase

  def test_introduction
    # タイトルで本を検索
    select = Select.new("b.id, b.isbn, b.title")
    select.from("books").as("b")
    select.where("b.title like '%フーコー%'")
    assert_equal "select b.id, b.isbn, b.title" + 
      " from books as b where (b.title like '%フーコー%')", select.to_s
    # 著者名も指定されていた場合
    select.from.get("b").inner_join("authors").as("a").on("b.author_id = a.id")
    select.where.and("a.name like '%エーコ%'")
    assert_equal "select b.id, b.isbn, b.title" + 
      " from books as b inner join authors as a on (b.author_id = a.id)" +
      " where ((b.title like '%フーコー%') and a.name like '%エーコ%')", select.to_s
  end

  def test_build_sql
    # booksとpartiesをjoin
    select = Select.new("b.id, b.name, p.id, p.name")
    select.from("books").as("b").inner_join("parties").as("p").on("b.author_id = p.id")
    assert_equal "select b.id, b.name, p.id, p.name" + 
      " from books as b inner join parties as p on (b.author_id = p.id)", select.to_s
    #
    # joinのonに条件を追加
    select.from.get("p").on.and("p.type_cd = 2")
    assert_equal "select b.id, b.name, p.id, p.name" + 
      " from books as b inner join parties as p on" +
      " ((b.author_id = p.id) and p.type_cd = 2)", select.to_s
    #
    # joinしているpに更にcontactsテーブルをcとしてjoin
    select.from.get("p").left_outer_join("contacts").as("c").on("c.party_id = b.id")
    select.fields += ", c.address"
    # where句に条件を追加
    select.where("c.address like '%London%'")
    assert_equal "select b.id, b.name, p.id, p.name, c.address" + 
      " from books as b inner join parties as p on" +
      " ((b.author_id = p.id) and p.type_cd = 2)" +
      " left outer join contacts as c on (c.party_id = b.id)" +
      " where (c.address like '%London%')", select.to_s
    #
    # 更にpartiesテーブルを別名companyとしてfromに追加
    select.from.add("parties").as("company")
    # where句に条件を追加
    select.where << "company.id = p.company_id"
    assert_equal "select b.id, b.name, p.id, p.name, c.address" + 
      " from books as b inner join parties as p on" +
      " ((b.author_id = p.id) and p.type_cd = 2)" +
      " left outer join contacts as c on (c.party_id = b.id)," +
      " parties as company" +
      " where (c.address like '%London%' and company.id = p.company_id)", select.to_s
    #
    # where句にorで条件を追加
    select.where.or("company.type_cd = 3")
    assert_equal "select b.id, b.name, p.id, p.name, c.address" + 
      " from books as b inner join parties as p on" +
      " ((b.author_id = p.id) and p.type_cd = 2)" +
      " left outer join contacts as c on (c.party_id = b.id)," +
      " parties as company" +
      " where ((c.address like '%London%' and company.id = p.company_id)" +
      " or company.type_cd = 3)", select.to_s
  end
end


select.rb

# copyright akima
class Select
  
  attr_accessor :fields
  attr_accessor :from
  attr_accessor :where
  attr_accessor :group_by
  attr_accessor :having
  attr_accessor :order_by
  
  def initialize( fields = nil )
    @fields = fields
    @from = FromPhase.new
    @where = Conditions.new
    @group_by = nil
    @having = Conditions.new
    @order_by = nil
  end

  def to_s
    build
  end
  
  def build
    s = "select " + (Helper.empty?(@fields) ? "*" : @fields)
    s += " from " + @from.to_s
    s += " where " + @where.to_s unless @where.empty?
    s += " group by " + @group_by.to_s unless Helper.empty?( @group_by )
    s += " having " + @having.to_s unless @having.empty?
    s += " order by " + @order_by.to_s unless Helper.empty?( @order_by )
    return s
  end
  
  def from( table = nil) 
    (table) ? @from.add(table) : @from
  end
  
  def where( condition = nil) 
    (condition) ? @where.add(condition) : @where
  end
  
  def having( condition = nil) 
    (condition) ? @having.add(condition) : @having
  end
  
  class Helper
    def self.empty?( s )
      return true unless s
      return s.empty?
    end
  end

  class Conditions
    attr_accessor :items
    attr_accessor :bracket
    attr_accessor :connector
    
    def initialize( connector = 'and', bracket = true )
      @connector = connector
      @bracket = bracket
      @items = Array.new
    end
    
    def empty?
      @items.empty?
    end
    
    def add( item )
      @items << item
      return self
    end
    
    def and( item )
      connect("and", item)
    end
    
    def or( item )
      connect("or", item)
    end
    
    def connect( connector, item )
      s = self.to_s
      @items = Array.new
      @items << s unless s.empty?
      @items << item unless item.empty?
      @connector = connector
      return self
    end
    
    def <<( item )
      add(item)
    end
    
    def checkValue( value )
      return (value) && (value != "")
    end
    
    def addCompare(left, operator, right)
      return self unless checkValue( right )
      return self unless checkValue( left )
      if "in" == operator.downcase
        return add("#{left} in (#{right.to_a.join(',')})")
      end
      r = (right.is_a? String) ? "'#{right}'" : "#{right.to_s}" 
      return add("#{left.to_s} #{operator.to_s} #{r}")
    end
    
    def addEqual( left, right )
      operator = (right.is_a? Array) ? "in" : "="
      return addCompare(left, operator, right)
    end
    
    def to_s
      return "" if @items.empty?
      result = @items.join(" " + @connector + " ")
      result = "(#{result})" if @bracket
      return result
    end
  end
  
  class FromPhase
    def initialize
      @tables = Array.new
    end
    
    def empty?
      @tables.empty?
    end
    
    def <<(table)
      add(table)
    end
    
    def add(table)
      table = TableUnit.new(table) unless table.is_a? TableUnit
      @tables << table
      return table
    end
    
    def get(name)
      @tables.each{|table| 
        result = table.get(name)
        return result if result
      }
      return nil
    end
    
    def to_s
      return "" if empty?
      return @tables.join(", ")
    end
  end
  
  class FromUnit 
    attr_accessor :name
    attr_accessor :table
    attr_accessor :joins
    
    def initialize(table)
      @table = table
      @name = nil
      @joins = Array.new
    end
    
    def get( n )
      return self if n == @name
      @joins.each{|join| 
        result = join.get(n)
        return result if result
      }
      return nil
    end
    
    def as(name)
      @name = name
      return self
    end
    
    def to_s
      (@joins.empty?) ? "" : @joins.join(" ")
    end
    
    def join(table, join_type = "inner join")
      result = JoinUnit.new(table, join_type)
      @joins << result
      return result
    end
    def inner_join(table)
      join(table, "inner join")
    end
    def left_outer_join(table)
      join(table, "left outer join")
    end
  end
  
  class TableUnit < FromUnit
    def initialize(table)
      super(table)
    end
    
    def to_s
      subJoins = super;
      result = @table + (!Helper.empty?(name) ? (" as #{name}") : "" ) + 
        (subJoins.empty? ? "" : (" " + subJoins))
    end
  end
  
  class JoinUnit < FromUnit
    attr_accessor :join_type
    attr_accessor :conditions
    def initialize(table, join_type)
      super(table)
      @join_type = join_type
      @conditions = Conditions.new
    end
    
    def on( condition = nil )
      condition.to_a.each{ |c| @conditions << c } if condition
      return @conditions 
    end

    def to_s
      subJoins = super;
      result = @join_type + " " + @table + (!Helper.empty?(name) ? (" as #{name}") : "" ) + 
        ( @conditions.empty? ? "" : " on " + @conditions.to_s ) +
        (subJoins.empty? ? "" : (" " + subJoins))
    end
  end
end

まだRubyのコードの書き方に自信がないので、何かアドバイスなどがあったらコメント下さい。よろしくお願いします > Ruby使いの方