投稿日 2010年6月28日 月曜日 カテゴリ Ruby on Rails 投稿者 morimotoComments Off 

どうもお久しぶりです。

morimotoです。

今日は複雑なActiveRecordの条件句を作成する際に(もしかしたら)楽になるのではないかと思いconditionを生成するクラスを作ってみました。

  class ARCond
    module OPERATION
      EQUAL            = '='
      NOT_EQUAL        = '<>'
      IN               = 'IN'
      GREATER          = '>'
      GREATER_OR_EQUAL = '>='
      LESS             = '< '
      LESS_OR_EQUAL    = '<='
    end

    attr_reader :conditions, :parameters, :separator, :table_name

    def initialize(separator="AND", table_name=nil)
      @conditions = []
      @parameters = []
      @separator  = separator
      @table_name = table_name
    end

    # column = ?
    def equal(column, value, table=nil)
      add(column, value, OPERATION::EQUAL, table)
    end
    # column <> ?
    def not_equal(column, value, table=nil)
      add(column, value, OPERATION::NOT_EQUAL, table)
    end
    # column IN (?,?,?...)
    def in(column, value, table=nil)
      add(column, value, OPERATION::IN, table)
    end
    # column > ?
    def greater(column, value, table=nil)
      add(column, value, OPERATION::GREATER, table)
    end
    # column >= ?
    def greater_or_equal(column, value, table=nil)
      add(column, value, OPERATION::GREATER_OR_EQUAL, table)
    end
    # column < ?
    def less(column, value, table=nil)
      add(column, value, OPERATION::LESS, table)
    end
    # column <= ?
    def less_or_equal(column, value, table=nil)
      add(column, value, OPERATION::LESS_OR_EQUAL, table)
    end

    def << (other)
      cond = nil
      param = []
      if other.kind_of? ARCond
        c = other.build
        cond = c.delete_at(0)
        param = c
      elsif other.kind_of? String
        cond = other
      elsif other.kind_of? Array
        cond = other.delete_at(0)
        param = other
      end
      unless cond.blank?
        @conditions << cond
        @parameters.concat(param)
      end
      return self.build
    end

    # [conditions, param, param, param,...]
    def build
      [join_conditions].concat(@parameters)
    end

    private
    def join_conditions
      sep = " #{@separator} "
      "(#{@conditions.join(sep)})"
    end

    def column_fullname(column, table=nil)
      unless column =~ /^[\w]+\.[\w]+$/
        if table_name = table.blank? ? @table_name : table
          return "#{table_name}.#{column}"
        end
      end
      column
    end

    def add(column, value, operation, table=nil)
      parts = [column_fullname(column, table), operation]
      param = [value].flatten
      parts << ((operation == OPERATION::IN) ? "(#{param.map{'?'}.join(',')})" : "?")
      self << [parts.join(' ')].concat(param)
    end
  end

使い方の例)

# 基本スタイル
c1 = ARCond.new("AND")
c1.equal("col1", true)
  # => ["(col1 = ?)", true]
c1.not_equal("col2", nil)
  # => ["(col1 = ? AND col2 <> ?)", true, nil]
c1.in("col3", [31,32,33])
  # => ["(col1 = ? AND col2 <> ? AND col3 IN (?,?,?))", true, nil, 31, 32, 33]



# 連結文字を変更する
c2 = ARCond.new("OR")

# 指定した連結文字で結合されていく
c2.greater("col4", 100)
  # => ["(col4 > ?)", 100]
c2.less("col4", 200)
  # => ["(col4 > ? OR col4 < ?)", 100, 200]

# 配列みたいに追加していく
c1 << c2
  # => ["(col1 = ? AND col2 <> ? AND col3 IN (?,?,?) AND (col4 > ? OR col4 < ?))", true, nil, 31, 32, 33, 100, 200]



# 引数なしはANDで連結
c3 = ARCond.new()

# いつもどおりの配列の conditions もくっつけれる
c3 << ["col5 = ? AND col6 IN (?,?)", 11,22,33]
  # => ["(col5 = ? AND col6 IN (?,?))", 11, 22, 33]

# 任意文字列もくっつけれる
c3 < < "col7 > col8"
  # => ["(col5 = ? AND col6 IN (?,?) AND col7 > col8)", 11, 22, 33]


# テーブル名を指定する
c4 = ARCond.new("OR", "sample")

# カラム名の頭にテーブル名が付与される
c4.greater_or_equal("col9", 300)
  # => ["(sample.col9 >= ?)", 300]
c4.less_or_equal("col9", 400)
  # => ["(sample.col9 >= ? OR sample.col9 < = ?)", 300, 400]

# くっつけるときにテーブル名を指定する
c4.equal("col10", false, "banana")
  # => ["(sample.col9 >= ? OR sample.col9 < = ? OR banana.col10 = ?)", 300, 400, false]

c3 << c4
  # => ["(col5 = ? AND col6 IN (?,?) AND col7 > col8 AND (sample.col9 >= ? OR sample.col9 < = ? OR banana.col10 = ?))", 11, 22, 33, 300, 400, false]

c1 << c3
  # => ["(col1 = ? AND col2 <> ? AND col3 IN (?,?,?) AND (col4 > ? OR col4 < ?) AND (col5 = ? AND col6 IN (?,?) AND col7 > col8 AND (sample.col9 >= ? OR sample.col9 < = ? OR banana.col10 = ?)))", true, nil, 31, 32, 33, 100, 200, 11, 22, 33, 300, 400, false]


# 最終的に使いたいタイミングで build してあげたらよい
c1.build
  # => ["(col1 = ? AND col2 <> ? AND col3 IN (?,?,?) AND (col4 > ? OR col4 < ?) AND (col5 = ? AND col6 IN (?,?) AND col7 > col8 AND (sample.col9 >= ? OR sample.col9 < = ? OR banana.col10 = ?)))", true, nil, 31, 32, 33, 100, 200, 11, 22, 33, 300, 400, false]

作っておいてなんですが、テーブルが複数あったり、集計クエリが絡んできたりと、複雑になればなるほど自分でwhere句を書いたほうが早い気がします。

以上、morimoto でした。

投稿日 2009年7月6日 月曜日 カテゴリ Ruby on Rails 投稿者 sugimotoComments Off 

そんなケースはほとんどないかもしれませんが。。

先日実際にあったのでそのときのメモです。

Ruby on Rails (v2.2) による社内システムをお客様の既存システムと連携をするために、次の条件で実装しました。

  • データベースエンジンにMS SQL Server 2000を使う
  • 既存システムのデータベースとの連携をリアルタイムに行う

既存システムのデータベースへの参照、更新はリアルタイムに行う必要があったため、リンクサーバーをviewで参照することにしました。

RailsアプリケーションはWindowsサーバー上に設置することにしました。

1. まずはSQLサーバーに接続

SQLサーバーをエンジンとして使うにはSQLサーバー用のアダプタが必要なので、インストールします。

Connect To MicrosoftSQLServer From Rails On Linux Boxを参考にしてインストール

> gem install rails-sqlserver-2000-2005-adapter -s http://gems.github.com

Ruby/DBIにふくまれるADO.rb を C:\ruby\lib\ruby\site_ruby\1.8\DBD\ADO にコピーします。

database.yml の設定はいつも通りです。 adapter をMSSQL用に変更

さらに、Rails上では文字コードはUTF-8の方が何かと安心なので、environment.rb に次のように指定します。

require 'win32ole'
WIN32OLE.codepage = WIN32OLE::CP_UTF8

2. 既存システムの複合キーを解釈する

既存システムのidは当然ですが、Railsの規約通りではありませんでした。 さらに、メインのテーブルに複合キーがあり、複数カラムでユニークキーとなっていました。

とはいえ、Rails のHABTMを使いたいので、Composite Primary Keys プラグインを利用しました。

> gem install composite_primary_keys

environment.rb に次の行を追加します。

require 'composite_primary_keys'

model では次のようにして複合キーを指定しました。

set_primary_keys :col1, :col2
has_one :status, :foreign_key => ['col1', 'col2']

3. リンクサーバーに接続

これで設定はOKかなと思ったんですが、実際に動かしてみると2つの問題がありました。

1つ目、更新できない。。

致命的です。更新しようとするとエラーが発生してしまいました。 調べてみると、分散クエリと分散トランザクションに説明がありました。

長い、、、ですが、問題になっているのはここ。

上記の規則は、入れ子になったトランザクションをサポートしないプロバイダに対する制限事項として、分散トランザクションでの更新操作は、XACT_ABORT が ON の場合のみ行えることを示しています。

リンクサーバーによる接続は「入れ子になったトランザクションをサポートしないプロバイダ」ということ。 RailsではRailsの機能でリクエストごとにトランザクションが発生してしまうので、リンクサーバによる接続時に自動的に発生するローカルトランザクションと入れ子になってしまうようです。

回避する方法として、「SET XACT_ABORT ON」にして、トランザクションが入れ子になることを許容するようにしました。

Railsがトランザクションを開始する前に実行するために、environment.rb に次のコードを入れて、接続直後にSQLを実行するようにしました。

# リンクサーバーに対応するために接続後に XACT_ABORT ON にする
module ActiveRecord
  module ConnectionAdapters
    class SQLServerAdapter < AbstractAdapter
      alias :initialize_original :initialize

      def initialize(connection, logger, connection_options=nil)
        initialize_original(connection, logger, connection_options)
        @connection.execute("SET XACT_ABORT ON")
      end
    end
  end
end

2つ目、リンクサーバーへの接続が大量に発生

もうひとつ、アプリケーションを使っているとリンクサーバーへの接続が大量に発生してしまうということがありました。 実は、原因はわからなかったのですが、この接続がリンク先のリソースをロックする状況が発生していました。

対策として、productionモードでもRailsに接続を保持させないようにすることにしました。

environment.rb に以下を追加

require 'action_controller/dispatcher'
ActionController::Dispatcher.after_dispatch do
  ActiveRecord::Base.clear_reloadable_connections!
end

さて、正直なところ、これが正解かわかりませんが。試行錯誤の末、社内システムは無事に稼動しています。