odeの開発メモ日記

プログラマーやってます。今までの道のり ポケコンbasic→C(DirectX5 ネットやろうぜ)→Perl(ちょろっと)→Java→C#→Ruby→Android(Java)

class_inheritable_accessorからclass_attributeに移行する際の注意点

rails3.1でclass_inheritable_accessorがdeprecatedになりました。
warningメッセージ
class_inheritable_attribute is deprecated, please use class_attribute method instead. Notice their behavior are slightly different, so refer to class_attribute documentation first
代わりにclass_attributeを使えとのことなのですが単純置換してはだめな場合がありました。

問題点

配列等に値を追加する際に破壊的メソッドで変更すると問題がでます。(API Docに載ってます)
class_inheritable_accessorでは問題ありません。

解決方法

変数名=でコピーされた値を代入する。

サンプル

class Base
  class_attribute :setting
end

class Subclass < Base
end


Base.setting = []
Base.setting                 => []
Subclass.setting             => []

# <<で破壊的に追加すると親クラスにも影響がでる。問題!!
Subclass.setting << :foo
Base.setting                => [:foo]
Subclass.setting            => [:foo]

# += [値]で適用すると問題なし。
Base.setting = []
Subclass.setting += [:foo]  # このタイミングで=が呼ばれてSubclassに読み取り用アクセサメソッドが動的に定義される
Base.setting                => []
Subclass.setting            => [:foo]


# 次のハッシュのテストのために配列テストで定義されたSubclassのsettingメソッドを消す。
Subclass.singleton_class.class_eval do
  remove_possible_method(:setting)
end

# ハッシュの場合はmerge!の代わりにmergeを使う。肝は=メソッドを使うことにあり。=で設定することでSubclassに読み取り用アクセサメソッドが動的に定義される
Base.setting = {}
Subclass.setting.merge!(:foo => "test")
Base.setting                => {:foo => "test"}
Subclass.setting            => {:foo => "test"}

Base.setting = {}
Subclass.setting = Subclass.setting.merge(:foo => "test")
Base.setting                => {}
Subclass.setting            => {:foo => "test"}

実装の違い

  • class_inheritable_accessor
    • 値をクラスインスタンス変数のハッシュで保持。継承する際にコピーしている。
  • class_attribute
    • =メソッドで代入された際にそのクラスに読み取り用アクセサメソッドが動的に定義される。
    • あとは通常のクラス継承の仕組みで動く、Subclassで定義した場合にはそれが呼ばれ、定義してない場合はBaseクラスのメソッドが呼ばれる。
    • そのためパフォーマンスが良くなった。

実行環境

ruby (1.9.2)
rails (3.0, 3.1)

ActiveModelにある程度機能を組み込んだベースクラス

更新履歴

  • 2011/9/28 rails3.1対応
  • 2011/6/27 ActiveModelImplの継承の継承したクラス対応。
  • 2011/6/23 リリース

概要

  • ActiveModelを使うとDBに保持しないモデルが作れます(ある程度ActiveRecord互換の機能が動く)
  • ただしそのままでは簡単に使えなそげ。機能を作るのに必要な部品を提供するものっぽい
  • なので機能をある程度組み込んだベースクラスがあると便利ではないかと作りました

組み込んだ機能

  • attributes, attributes=での設定と取得
  • dirty(どの属性が変更されたか等を取得)

下記は通常のActiveModelでも簡単に実装できるもの

  • validation
  • form_for等に渡せるように対応するため互換メソッド

ActiveModelを使うシチュエーション

  • DB使わないところ
    • 検索フォームのモデルに
    • バックエンドがDBではない場合のモデルに。APIを叩く場合等

ベースクラスのコード

class ActiveModelImpl
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming
  include ActiveModel::Dirty
  
  
  def initialize(attributes = {})
    @attributes = attributes_from_column_definition
    self.attributes = attributes
  end
  
  def persisted?
    false
  end
  
  def attributes=(attributes = {})
    if attributes
      attributes.each do |name, value|
        send("#{name}=", value)
      end
    end
  end
  
  def attribute_names
    @attributes.keys.sort
  end
  
  def attributes
    attrs = {}
    attribute_names.each { |name| attrs[name] = read_attribute(name) }
    attrs
  end
  
  class_attribute :column_names
  self.column_names = []
  
  def self.attr_accessor(*names)
    names.each do |attr_name|
      attr_name = attr_name.to_s
      self.column_names += [attr_name]
      generated_attribute_methods.module_eval("def #{attr_name}; read_attribute('#{attr_name}'); end", __FILE__, __LINE__)
      generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
    end
    define_attribute_methods names
  end
  
  def write_attribute(attr_name, value)
    attr_name = attr_name.to_s
    
    attribute_will_change!(attr_name) unless value == read_attribute(attr_name)
    
    @attributes[attr_name] = value
  end
  
  def read_attribute(attr_name)
    if respond_to? "_#{attr_name}"
      send "_#{attr_name}" if @attributes.has_key?(attr_name.to_s)
    else
      _read_attribute attr_name
    end
  end
  
  def _read_attribute(attr_name)
    attr_name = attr_name.to_s
    value = @attributes[attr_name]
    value
  end
  
  # 変更した部分の変更後のHashを返します。もともとあるchanged_attributesは変更した部分の変更前のHashを返すので。Railsのdirty機能にはなかったので実装。
  def changing_attributes
    changed.inject(HashWithIndifferentAccess.new){ |h, attr| h[attr] = __send__(attr); h }
  end
  
  # 変更履歴をクリアします。
  # モデルでsaveやcreate、updateがあるならそのなかでこのメソッドを呼ぶ。
  def clear_changed_attributes
    @previously_changed = changes
    @changed_attributes.clear      
  end
  
  private
  def attributes_from_column_definition
    self.class.column_names.inject({}) do |attributes, column_name|
      attributes[column_name] = nil
      attributes
    end
  end
  
end
  

利用コード

class User < ActiveModelImpl
  attr_accessor :name, :email

  validates_presence_of :name

  # 注意メモ
  # ・値を上書きする際は@email=とかは使わずにwrite_attributeを使う
end

user = User.new
user.attributes = {:name => "test", :email => "hoge@hogehoge.hoge"}
user.changed # ["name", "email"]
user.changes # {"name"=>[nil, "test"], "email"=>[nil, "hoge@hogehoge.hoge"]}
user.valid? # true
user.attributes # {"email"=>"hoge@hogehoge.hoge", "name"=>"test"}

実行環境

ruby (1.9.2)
rails (3.0, 3.1)

ハッシュの値をアクセサで呼べるようにする

アクセサにしたい理由

  • viewで[]でアクセスするのはかっこわるい。ARと同様にアクセサがいい!
  • 外部APIを叩いてJSONで返ってきたものをViewで使う場合など

実現方法

HashをStructに変換します

変換コード

    def self.hash_to_struct(hash)
      vals = *hash.values.map do |val|
        case val
          when Array
          val.map do |array_val|
            if array_val.is_a?(Hash)
              hash_to_struct(array_val)
            else
              array_val
            end
          end
          when Hash
          hash_to_struct(val)
        else
          val
        end
      end
      
      Struct.new(*hash.symbolize_keys.keys).new(*vals)
    end    

使い方

      json = <<-EOS
{
    "entry" : [
        {
            "name" : "ほげタロウ",
            "age" : "30"
        },
        {
            "name" : "ほげ子",
            "age" : "31"
        }
    ],
    "startIndex" : 1,
    "itemsPerPage" : 50,
    "totalResults" : 2
}
EOS
      
      hash = JSON.parse(json)
      obj = hash_to_struct(hash)
      puts obj.startIndex
      puts obj.itemsPerPage
      obj.entry.each do |val|
        puts val.name
        puts val.age
      end

実行環境

ruby (1.9.2)

rails2.3.8のXSSプラグインでlink_toの表示テキストがサニタイズされない問題直ってました

<%= link_to "<b>あいうえお</b>", :action => :index %>

2.3.8+XSSプラグインでbタグを適用したい場合はrawメソッドを呼ぶ

<%= link_to raw("<b>あいうえお</b>"), :action => :index %>

結果
あいうえお


インストール

(2.3.5のときのURLとは違くなってます。railsオフィシャルな場所に。)
gem install erubis
./script/plugin install http://github.com/rails/rails_xss.git


ちなみに旧2.3.5の場合はこうでした。
./script/plugin install git://github.com/NZKoz/rails_xss.git


2.3.8で旧verのプラグインをつい買うと
undefined method `html_safe!' for "":String
というエラーになります。

参考記事

2.3.5時代のlink_toのサニタイズされない問題を書いてる記事
http://d.hatena.ne.jp/h1mesuke/20100226/p1


xssプラグインについての記事
http://d.hatena.ne.jp/saekik/20091209/1260339089

ActiveRecordのDBコネクションの接続切れと再接続について。reconnectオプションは危険だなーとかも

ActiveRecordは基本ずっと接続をはりっぱなしにします。

なので長時間接続をはりっぱなしにするため
タイムアウト等で接続がきれると問題がでます。
MySQLでDBの接続がきれるタイミング

状況別

  • Railsの場合
    • HTTPリクエストのあるたびに接続が切れてないかを確認します。切れてる場合は再接続するようになっているので問題なさそうです。
  • バッチ、デーモン等
    • ActiveRecordを使った場合はずっとつなぎっぱなしになるため、途中で接続が切れるとエラーになります。

reconnectオプションは危険

じゃあバッチ等の場合自動で再接続してくれればと思うと
MySqlのクライアントライブラリレベルでリコネクトの概念がありました。
railsで使うにはdatabase.ymlに:reconnect = trueで指定できます。
が!!
railsの実装ではデフォルトはOFFになっています。
なんでOFFかなとおもったら下記のページを見たら納得しました。
http://dev.mysql.com/doc/refman/5.1/ja/mysql-reconnect.html
変数を使ったりした場合等の接続セッションに依存するような場合のコードを書くと、途中で接続がきれて変数等がリセットされてるのにきづかずに次の変数等に依存したsqlを実行してしまう可能性があるからです。
トランザクション周りも危険っぽいですね。ロールバックしてるのに気づかず自動コミットに戻って実行されちゃうとかかなー 怖い怖い。
なのでこの自動再接続は使わないほうがよいかと。

バッチ等の場合での対策

切れる可能性は

まぁー普通はLANなのでそんな接続は切れないだろうという前提で気にしないでエラーになってよいかと思います。
但し、バッチによっては重い処理等をすることもあるでしょうから
MySQLでDBの接続がきれるタイミングに書いたように
重い集計処理をしてからインサートする場合等や、SQL自体がゲロ重な場合等々で接続が切れる可能性があります(特にSLB経由のDBアクセスの場合)。
あんまりないとは思いますがVPN経由でDB接続してて回線が不安定で瞬断がよくおきるとかもあるかもしれません。

対策
  • VPN経由でDB接続の場合等の瞬断
    • このような場合にはrescueで拾ってやりなおしにするのが良いかもしれません。
    • 注意点としてやり直しする前にActiveRecord::Base.clear_active_connections!を呼ぶ必要があります。
    • このメソッドを呼ぶことで接続しっぱなしのコネクションを一旦コネクションプールに返却して次のコネクション使用時に再接続してもらえます。
  • 重い集計処理をしてからインサートする場合
    • インサート直前でActiveRecord::Base.clear_active_connections!を呼ぶと直ると思います。
  • SQL自体がゲロ重な場合
    • SLBやDBのtimeoutを伸ばすしかないでしょう。

Railsでリクエストのたびに再接続される部分のソースおっかけ

rails初期化時にリクエスト毎に実行されるrackのミドルウェアとしてActiveRecord::ConnectionAdapters::ConnectionManagementを追加している。
ConnectionManagementの中ではActiveRecord::Base.clear_active_connections!が呼ばれるのでコネクションが再接続してもらえる。

# rails-2.3.8\lib\initializer.rb
    def initialize_database_middleware
      if configuration.frameworks.include?(:active_record)
        if configuration.frameworks.include?(:action_controller) &&
            ActionController::Base.session_store.name == 'ActiveRecord::SessionStore'
          configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::ConnectionAdapters::ConnectionManagement
          configuration.middleware.insert_before :"ActiveRecord::SessionStore", ActiveRecord::QueryCache
        else
          configuration.middleware.use ActiveRecord::ConnectionAdapters::ConnectionManagement
          configuration.middleware.use ActiveRecord::QueryCache
        end
      end
    end
# activerecord-2.3.8\lib\active_record\connection_adapters\abstract\connection_pool.rb
    class ConnectionManagement
      def initialize(app)
        @app = app
      end

      def call(env)
        @app.call(env)
      ensure
        # Don't return connection (and peform implicit rollback) if
        # this request is a part of integration test
        unless env.key?("rack.test")
          ActiveRecord::Base.clear_active_connections!
        end
      end
    end


ActiveRecord::Base.clear_active_connections!が呼ばれるとコネクションがコネクションプールに返却(checkin)される。
コネクション自体はプールされてて接続は維持されたままである。あくまで現スレッドで使うプールからもらってたコネクションをチェックインするだけ。それがコネクションプーリングってもんだ。

# activerecord-2.3.8\lib\active_record\connection_adapters\abstract\connection_pool.rb
      def clear_active_connections!
        @connection_pools.each_value {|pool| pool.release_connection }
      end

      def release_connection
        conn = @reserved_connections.delete(current_connection_id)
        checkin conn if conn
      end


じゃあなんでコネクションプーリングに返却されると再接続されるかというと
コネクションプーリングから接続をもらう際(checkout)に再接続がされるためである。

      def connection
        if conn = @reserved_connections[current_connection_id]
          conn
        else
          @reserved_connections[current_connection_id] = checkout
        end
      end


(少し脱線:ちなみに@reserved_connectionsはチェックアウトしたコネクションをキャッシュしておくhashっぽい。current_connection_idはThreadIDみたいなものなのでスレッドごとに接続がはられてキャッシュされる仕組み(マルチスレッド対応のため)。)

      def current_connection_id #:nodoc:
        Thread.current.object_id
      end


ちょい省略するがcheckoutを辿ってくとcheckout_and_verifyが呼ばれる。ここでコネクションに対してverifyが呼ばれる。

      def checkout_and_verify(c)
        c.verify!
        c.run_callbacks :checkout
        @checked_out << c
        c
      end


verifyの中でactive?を呼んで接続されているかを確認し、切れてる場合はreconnectしている。

# activerecord-2.3.8\lib\active_record\connection_adapters\abstract_adapter.rb
      def verify!(*ignored)
        reconnect! unless active?
      end


なおabstract_adapter.rbで定義されてるactive?は

      def active?
        @active != false
      end
      def disconnect!
        @active = false
      end

というチープなものだが

mysql_adapterできちんと実装されなおしている。

# activerecord-2.3.8\lib\active_record\connection_adapters\mysql_adapter.rb
      def active?
        if @connection.respond_to?(:stat)
          @connection.stat
        else
          @connection.query 'select 1'
        end

        # mysql-ruby doesn't raise an exception when stat fails.
        if @connection.respond_to?(:errno)
          @connection.errno.zero?
        else
          true
        end
      rescue Mysql::Error
        false
      end

実行環境

ruby (1.8.6)
rails (2.3.8)

MySQLでDBの接続がきれるタイミング

接続がきれるタイミング(環境によってはもっとあるでしょうが)

  • MySQLのtimeout
    • my.cnfのwait_timeout デフォルト値は8時間
  • SLBを経由してDB接続している場合はSLBのtimeout
    • ipvsadm --set tcp tcpfin udp (set connection timeout values) デフォルト値は5,6分

timeout自体の判定

  • 送受信されるデータがなくなってから。
  • 接続をはりっぱなしにしていても常にsqlを発行していれば問題なし。

問題がでそうな状況

  • 接続を張りっぱなしにしておいてバッチ等で重い集計処理をしてからインサートする場合等や、SQL自体がゲロ重な場合等々。 このような状況で通信が発生しなくなるとtimeout値を越して接続が切れます。

切れた状態でSQLを発行すると

  • MySQL server has gone away

  • Lost connection to MySQL server during query

というようなエラーになります。

composite_primary_keysのバグ修正で思ったこと。

見たくもないレガシーDBにアクセスする用事があるんですよこれが。(あー触りたくない)
railsでは使うことのない複合プライマリキーが使われてたので
これに対応するためにプラグインのcomposite_primary_keysを使いました。
ただちょいとバグがありで、rails2.1から追加されたdirtyでの差分アップデートに対応してませんでした。なのでモンキーパッチを書いて修正しました。



複合プライマリキーに対応するためdb更新時のメソッドActiveRecord::Base#updateを再定義してるんですが問題が2点


1・差分更新に必要な引数をとってなかった。
恐らくrais1の時代から変えてなかったのかもしれません。


2・update_without_callbacksを再定義していたんですが、その前にalias_method_chainでupdate_without_dirtyやupdate_without_lockがあるのでそれらの機能が使えなってる様子。
とりあえず調べたところではupdate_without_lockが最初のaliasっぽいのでそちらを再定義するように変更。


この2番を対応してて気づいたのがalias_method_chainを複数回した時の一番最初のオリジナルのメソッドを再定義したいときってどうやれば綺麗に実装できるの?と思いました。aliasの元をたどれる仕組みがあったらいいなと思いました。



修正パッチ

module CompositePrimaryKeys
  module ActiveRecord #:nodoc:
    module Base #:nodoc:
      module CompositeInstanceMethods
        remove_method :update_without_callbacks
        def update_without_lock(attribute_names = @attributes.keys)
          quoted_attributes = attributes_with_quotes(false, false, attribute_names)
          return 0 if quoted_attributes.empty?

          where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair|
            "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
          end
          where_clause = where_clause_terms.join(" AND ")
          connection.update(
            "UPDATE #{self.class.quoted_table_name} " +
            "SET #{quoted_comma_pair_list(connection, quoted_attributes)} " +
            "WHERE #{where_clause}",
            "#{self.class.name} Update"
          )
          return true
        end
      end
    end
  end
end

オリジナル

        def update_without_callbacks
          where_clause_terms = [self.class.primary_key, quoted_id].transpose.map do |pair| 
            "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
          end
          where_clause = where_clause_terms.join(" AND ")
          connection.update(
            "UPDATE #{self.class.quoted_table_name} " +
            "SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
            "WHERE #{where_clause}",
            "#{self.class.name} Update"
          )
          return true
        end

実行環境

ruby (1.8.6)
rails (2.3.5)
composite_primary_keys (2.3.5.1)