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クラスのメソッドが呼ばれる。
- そのためパフォーマンスが良くなった。
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"}
ハッシュの値をアクセサで呼べるようにする
実現方法
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!を呼ぶと直ると思います。
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
MySQLでDBの接続がきれるタイミング
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