2013年7月5日金曜日

trigger ベースの履歴

railsを使って色々なアプリを作っているが、どのアプリでもmysqlで全てのテーブルの変更履歴を取るようにしている。なにかの不具合でデータがおかしくなった場合などに調査、復元がしやすいためだ。業務アプリで履歴データが無い、というのは今となっては考えられない。

履歴を実現するために、rails には vestal_versions や paper_trails など変更履歴を自動で記録してくれる便利な gem がある。これらを使うことで save のタイミングで逐一履歴を保存してくれるようになる。便利。しかし、これらのgemには一つ問題がある。これらは通常 before_save や after_save で実装されているため、save メソッドを使わずに sql を直接実行による更新をしてしまうと,
その変更の履歴が抜け落ちてしまう。これでは  mysql console からの操作が非常にやりにくい。

理想としては、言葉通り「全て」の変更履歴を残せる仕組みが良い。rails から保存しようが、mysql から直接 update かけようが、必ず履歴が残るようにしたい。そのような仕組みを実現するには、履歴を作成する機能がデータベースレベルで処理されることが必要となる。となるとデータベースの標準機能として存在しているのが最も好ましいのだが、残念ながらそのような機能を持つデータベースは見たことが無い。少なくとも mysql にはない。が、mysql の trigger を使って履歴を作成する機能を実装できる。それをざっくり説明する。

履歴のデータ構造としては acts_as_versioned などと同じ形式で、元テーブルと同じ構造+history_id(primary key) を持つテーブルを履歴対象テーブルごとに用意して、元テーブルに追加・変更・削除があるたびに履歴テーブルにデータを追記されていくようにする。

ではその履歴の追記をどうするかだが、mysql ではテーブルに対して insert, update, delete が行われたときに trigger を実行することができる。これを使って、insert, update, delete それぞれの処理時に履歴テーブルへ最新のデータがコピーされるような trigger を作成する。こうすることでDBレベルで履歴の記録が可能となる。(drop table, truncate table では履歴は作られない)

後は migration 時にこの trigger が自動で作成されるようにすればよい。create_table, change_table の実行時に trigger を作成するようにパッチを当てるだけでよい。

だた、この方法にも問題点はいくつかある。まず mysql の制限で、ひとつのテーブルに、ひとつの種別(insert, update, delete)に対して一つしか trigger を作ることができない。したがってこの履歴の仕組みを使った時点でtriggerはほかの用途には実質使えなくなる。

次に、常に履歴作成が走ってしまうというのも問題ではある。履歴が走る時と走らない時が全くコントロールできなくなる。1万行の update を実行すると同時に 1万行の履歴 insert が走る。ただこれについては全て mysql の中で自動で実行され、ruby のコードを介さないため、それほど大きな問題になっているという認識ではない。

また、これはなぜなのかよくわからないが、mysql の create trigger は異常に時間がかかる。数秒単位でかかるため、rake db:migrate でテーブルを最初から作るというときには数分単位で待たされてしまう。

最後に、これはもっとも深刻だが、当然ながらすべての履歴を保持するということはデータ量が莫大になるということになる。データ量x平均更新回数が履歴のサイズになるため、更新の頻繁なテーブルは履歴が爆発的に膨らんでしまう。そのようなものについては定期的にアーカイブが必要になり、そうした場合アプリの中で履歴を参照してロジックを実装してしまうとアーカイブもできず身動きが取れなくなる。

そういうデメリットはあるものの、履歴があるのと無いのとではトラブル時の安心感がまったく違う。間違ってデータを変更削除してしまった場合でも簡単に復旧できるので重宝する。もはや履歴が無いというのは考えられない。

2013年2月27日水曜日

Ruby 2.0 の refinement で DCI?

ruby 2.0 では refinement という機能が追加されています。これってDCIの実装に使えるのかも?というのが今回のテーマです。仕様が難しいので、実際動かしてみないと判断はできませんが。

注意:refinement は2013/02/27 時点でexperimentalですので、この記事が三日後ゴミになっている可能性はありますので注意してくださいね。

refinementとは?

まず簡単に refinement について説明します。refinement とは、モジュールの適用範囲を特定のファイル内のみに制限するための仕組みです。おっともうこの時点でなんか使えそうな気がしてきました。

http://jp.rubyist.net/magazine/?0041-200Special-refinement

からのサンプルコードを拝借すると、

# rationalize.rb
module Rationalize
  refine Fixnum do
    def /(other)
      quo(other)
    end
  end
end

という感じで定義します。通常の module と違うのは refine というブロックの存在です。ここで指定されたクラスにそのブロック内のメソッドをまとめて追加するぞ、という意味になります。もちろん複数のクラスを指定して、一気に refine することができます。

で、これをどう使うかというと、

using Rationalize
p 1 / 2 #=> (1/2)

使う側はこのような感じ。ファイルでこのように using で refinement を指定すると、そのファイルの中でのみ、refine 対象のクラスにメソッドが追加される、という動きになります。

DCI的に解釈

これをDCIに使えるのか?DCIではコンテキストを定め、そのコンテキスト以下においてはモデルに対して自動でroleが付与され、そのコンテキスト下でのみ実装の追加や差し替えが行われます。そしてそのコンテキストを抜け出たらそのroleは自動的に消滅します。

すると、そのコンテキスト下で各modelに付与すべきroleを refinement として定義しておき、そのコンテキストを表すファイルの先頭で using refinement する。すると refinement が role として各modelに付与される。その refinement はそのファイル内でのみ有効なので、コンテキストを抜けると自動的に role が消えます。

仕組みとしては良さそうです。また、特定コンテキスト下の role をひとまとめに書いておいて、それを一括で漏れ無くガツンと追加できる、というのもワタシ好みな感じです。今までのナイーブな rails での問題として、特定のコンテキスト下でのみ必要なメソッドが各モデルに分散してよくわからなくなる、という問題がありました。それがコンテキストという単位で一箇所にまとめることができるのは良いと思います(もちろん、モデルに置くべきか、コンテキスト単位でまとめるべきか、というのはケースバイケースだと思いますが)。また、このコンテキストで、いったいどのmoduleがextendされているのか?というのも一目瞭然なのが良いです。最もビジネスロジックレイヤーに近い、サービスなどの実装ではこの refinement が活躍しそうな予感で一杯です。

以上

2013年2月17日日曜日

考えるという事は問いを立てるという事

「何がわからないか」がわからないをそのままにしておかない技術モドキ

これと似た内容(劣化版)を昔社内の勉強会で発表したことがあるので、自分の言葉で書いてみたいと思った。

その時の発表のタイトルは「考えるとは何か」というものだった。考えるとはどういう行為なのか?入力は何で出力は何か?何ができたら「考える」という行為が達成できたことになるのだろうか?そういう疑問に答えを出したくて「考えた」結果の発表だった。

自分が過去そうだったのだが、数学のような抽象的思考(図形的思考)が得意な人の中には、なまじっか複雑なことを頭の中で図形的に処理して答えを出せるがために、考えるという行為が体系化されていない人がいる。そういう人は問題を解く際の戦略があまり洗練されておらず、難しい問題を解くときに頭の中にとりあえずその問題だけを突っ込んで、後はうーんと色々ランダムに考えてみて、時間をかけていつか答えが出るのを待つ。動物が餌を探す時に闇雲に歩きまわるような、そういう解き方をしてしまう人がいる。ちょっと極端な書き方をしたが自分が実際そうだったし、同じような人を何人か知っている。こういう、戦略のない、図形的思考にばかり頼る考え方のことを自戒を込めて「動物的思考」と呼ぶ。こういう人にはまず「言語的思考」を覚えてもらわないといけない。

動物的思考をする人は、色々な面で欠点がある。物事を図形的な思考にのみ頼ってなんとなーく処理しているので、細かいところで色々抜け落ちが出てくる。忘れっぽい。ノートも取らない。スケジュール管理もできない。机の上が散らかっている。おっと俺の悪口はそれまでだ!最大の特徴は、日常的に物事を言葉で表現する習慣がないので、とっさの質問に答えられなかったり、うまく言葉が出て来ないことがとても多い。難しい質問をすると黙り込むし、いきなり映画の感想とか聞かれても「よかったです」みたいな小学生のような答えしかでてこなくて恥ずかしい思いをする。夜になってあーあの時こう言っておけば良かったななんて後の祭りがチャメシゴト。このブログが要所要所で具体性に書けるのもそういうことだ。

私はこの自分自信の弱点を30年近い人生を経てやっと意識することができた。そしてここから脱却するために、自分の思考をもっと言葉をよく使うように変えていこうと考えた。そのための最初のステップが考えるとは何か?という問に答えを出すことだった。

では考えるとは何か?

私の定義では、考えるとは
  1. 問を曖昧さのない言葉で表現する。
  2. その問に答えを出す
の2ステップからなる。こちらのブログとほぼ同じ事を言っていると思う。そして、重要なのは1の方であり、動物的思考をする人がほとんどしていないのも1だ。動物的思考をする人はとても大雑把な問題をそのままの規模でいきなり解こうとしてしまう。するとその問題の規模が手に負えない場合、なかなか前に進まなくなる。こういう人が最初にやらなければいけないことは、問題の曖昧さをなくすことと、問題を処理可能な小さな問題になるまで分割していくことで問を洗練させるということだ。

問を洗練させるためには、まず問いがないといけない。自分が今いったいどんな問題を解こうとしているのか、自分のやりたいこと、ゴールは一体何なのか、頭の中にぼんやりとある問いを紙に書き出す。大雑把で良い。次に適当に書いた問いには5W1Hが抜けていること多いのでそこを明確にする。さらに、使われている一つ一つの単語の意味を全て拾い上げて明確に定義する。「かっこいい写真を撮りたい」というゴールだったとすると、それがいったい誰にとっての「かっこいい」なのか。かっこいいとは一体どういうことなのか、あるいはどういう物ではないのかを明らかにするということだ。そしてその実現のために必要なこと、障害となっていることは何か?ということを箇条書きにしていく。こういう整理を繰り返すと大きな問題がだんだんと小さな問題に分割されていく。当初は途方もなく大きく、どこから手をつけて良いかわからなかった問題が、ずっと規模が小さく、ゴールも明確で、なんとか手をつけれそうな問題になっていたりする。問題の最も難しい部分がどこなのかが明確になっていたり、一つ一つの小さな問題のできるできないが判断可能になっていたりする。これが問いを洗練させることの効果だ。

重要なことは、この問いを洗練させるという行為は、ある程度習熟すればかなり機械的な処理として実行できるということだ。もともと解こうとしていた途方もない問題に比べると極めて難易度の低い手続きのはずだ。簡単な整理をするだけで問題がずっと簡単になるのだから、やらない手はない。

このように、考えるという行為をはじめるときに紙の上で問題を整理していく作業をするようになってから、自分自信では物事を考えることの質も速度も向上したと感じている。こういう風に物事を整理して考える方法をもっと若い時に身に着けていれば人生変わっただろうとも思わなくはない。なので全国の動物系男子、動物系女子にはぜひとも自分の頭の中を紙にダンプして整理するという方法を身に着けてもらいたいなぁと思ってネットの片隅にこんな記事を書いた。

さらに補足として言っておきたいことが少しある。この「考える」の定義に従うならば、「一緒に考える」というのは考えている問いを誤解ない形で共有するということである、と再定義できる。また、森先生のブログにあった「考えたんですがまとまらなくて」という場合には、どこまで問いをブレイクダウンしたか、分割した中で一番むずかしい問いはどこか、その途中結果を共有するということになるだろう。「思い悩む」という状態は問いを明確にせずに「考え」ようとしてしまった結果ランダムな思考の迷路に入り込んでいる場合がある。他にもこの定義にしたがって捉え直すことのできるものがあるかもしれない。こういうことができるので言葉を自分なりに再定義するのは中々楽しい。

最後に、動物的思考をする人たちの名誉のために補足しておくと、この種の人は、その人がこれまで培ってきた多数のパターン認識という強い武器を持っている(というか、パターン認識能力に優れているから抽象的思考に強いのだと思う)。だから、パターン認識にすっぽりハマる分野では人並み以上の性能を出すことができているはずだ。ただ、それ以外の分野では抜けていることが多いという話。

2013年2月4日月曜日

view table を使って 1+N 問題回避

今回はtipsの紹介的な話。

検索結果で表示する内容が簡単な association だけでは取ってこれないような場合の解決方法について考える。

例えば、ユーザの検索結果にユーザの投稿数を表示する、という場合を考える。rails にはcounter cache columnという仕組みがあるためこの例に限っては別の解決法があるが、今回はそれを使わないことにする。

通常これを実装するためのコードは
@user = User.all

- @users.each do |u|
  %tr
     = u.posts.count

のようになるだろう。ここで明らかな問題は 1-N 問題が発生すること。posts.count を通るたびに

select count(*) from posts where user_id = 1

のような sql が実行される。u.posts.any?{|p| p.comments.present? } みたいなのだとさらに増える。

一つの解決法は当然ながら includes を使うことだ。
@user = User.includes( :posts => :comments )
としておけば 1-N 問題が解決される。しかしこれは数を数えるという目的のために comment オブジェクトを大量に作るという点で良くない。rails では残念なことに ActiveRecord の生成は大きなパフォーマンスの低下を招く。

別の方法としては、別途 sql で count した結果を inject してやるというものがある。
def inject_count_comments( posts )
  sql = "select post_id, count(*) from comments where post_id in (?) "
  restult = Comment.connection.execute [sql, posts.map(&:id)]
  posts_by_id = posts.index_by(&:id)
  result.each do |row|
     post = posts_by_id[""]
  end
end

これを毎回書くのは面倒なので、与えられた sql を実行して、model にセットするという汎用のメソッドは作ればすっきりする。model にセットするときの属性名は sql のカラム名をそのまま取るようにすればよい。

呼び出し側としては
posts = Post.some_condition.all
Post.inject_count_commitments( posts)
という二段構成になる点が残念。

さらに別な解決方法として、mysql の view を作るという方法もある。やや大げさすぎる感じもするので、これに値するほど複雑な sql のみ使っている。

最初の例の場合でいうと、以下のような view を作る。

create view posts_counts as
   select user_id, count(*) as count from posts group by user_id;

そして、この view を user の子モデルとして定義する。

class PostsCount < ActiveRecord::Base
end

class User < ActiveRecord::Base
  has_one :posts_count
end

@users = User.includes(:posts_count).all

ただこのやりかたは、Post がコメントをいくつ持っているか?という、明らかに Post の属性の一つであるものが独立したモデルとなってしまっているのがあまりよろしくなく、多様すると app/models や mysql の show tables が混雑してくるので限られた場合のみ使うほうが良いとは思う。コード上はすっきりするが。