script/console の履歴機能が上手く動かなくなった...ので対応

環境

現象

  • irb は通常通り~/.irb_history に履歴をためていく
  • rails のscript/console を起動すると、~/.irb_history の内容が上書きされ、先程までirb_history にあった最新の履歴が消えてしまう

script/console の実行中も、~/.irb_history をtail すると、どんどん履歴が書き込まれていっているのだけれど、何故かscript/console を終了して、再度起動したタイミングでやはり元の内容に戻ってしまう。~/.irb_history を空にしてもscript/console を実行すると中身が復活することから、どこからかコピーされている感じ。

原因

  • 未だソースとかを追っていない
  • もしかしたらrvm 関連の可能性もあるかも。rvm 使っていないときには何も発生しなかった
    • あくまで可能性です。原因を調べていないのでなんとも言えないです

対応

# http://github.com/gmarik/dotfiles/blob/84073cf564b601c99dc4b3b7910bd91234ff94f5/.ruby/lib/gmarik/irb-1.8-history-fix.rb
# http://stackoverflow.com/questions/2065923/irb-history-not-working
require 'irb/ext/save-history'
module IRB
  # use at_exit hook instead finalizer to save history
  # as finalizer is NOT guaranteed to run
  def HistorySavingAbility.extended(obj); 
    Kernel.at_exit{ HistorySavingAbility.create_finalizer.call }
    obj.load_history #TODO: super?
    obj
  end
end if IRB::HistorySavingAbility.respond_to?(:create_finalizer)

# http://rvm.beginrescueend.com/workflow/irbrc/
# for RVM
IRB.conf[:PROMPT_MODE] = :DEFAULT

# ヒストリーを有効にする
IRB.conf[:EVAL_HISTORY] = 1000 
IRB.conf[:SAVE_HISTORY] = 100 
HISTFILE = "~/.irb_history"
MAXHISTSIZE = 100
   
begin
  if defined? Readline::HISTORY
    histfile = File::expand_path( HISTFILE )
    if File::exists?( histfile )
      lines = IO::readlines( histfile ).collect {|line| line.chomp}
      puts "Read %d saved history commands from %s." %
        [ lines.nitems, histfile ] if $DEBUG || $VERBOSE
      Readline::HISTORY.push( *lines )
    else
      puts "History file '%s' was empty or non-existant." %
        histfile if $DEBUG || $VERBOSE
    end
    
    Kernel::at_exit {
      lines = Readline::HISTORY.to_a.reverse.uniq.reverse
      lines = lines[ -MAXHISTSIZE, MAXHISTSIZE ] if lines.nitems > MAXHISTSIZE
      $stderr.puts "Saving %d history lines to %s." %

      [ lines.length, histfile ] if $VERBOSE || $DEBUG
      File::open( histfile, File::WRONLY|File::CREAT|File::TRUNC ) {|ofh|
        lines.each {|line| ofh.puts line }
      }
    }
  end
end

vim でファイルを保存した時にGoogle Chrome で開いているページをリロードする

HTML を編集している時に必須の機能。今回試した方法は、

  1. AppleScript を使う
  2. ChromeReplを使う
  3. livereload

の3つです。最終的に、(不満ながらも)AppleScript 版を使っています。

1. AppleScript を使う

http://blog.cohtan.org/2008/03/vimhtmlyacss.htmlを参考にしました。

AppleScript の設置
tell application "Google Chrome" to activate
tell application "System Events" to keystroke "r" using {command down}
  • ~/bin/terminal_focus.scpt
tell application "Terminal" to activate
vim コマンドの定義
command! -bar ChromeReload silent !osascript $HOME/bin/chrome_reload.scpt && osascript $HOME/bin/terminal_focus.scpt
command! -bar ChromeStartObserve ChromeStopObserve | autocmd BufWritePost <buffer> ChromeReload
command! -bar ChromeStopObserve autocmd! BufWritePost <buffer>

id:tyru さんにご指摘いただき、ChromeStartObserve を複数回実行しても、リロードが一回しか行われないよう修正


スクリプトを2つに分けているのは、ひとつのスクリプトで、Chrome をactivate ⇒ リロード ⇒ Terminal をactivate とすると、たまにキーストロークが実行される前にTerminal に戻ってきてしまいリロードが走らないためです。
別の解決方法がある気がするのですが・・・。

使い方
  • :ChromeReload で現在開いているページをリロード
  • :ChromeStartObserve を実行すると、以降そのバッファの内容を保存した時にChrome のリロードが実行されるようになる
  • :ChromeStopObserve は、StartObserver の実行後、保存とリロードの連携を解除したい時に実行する


vim の方は、id:ursm さんの、Firefox を自動的にリロードする Vim スクリプト - ursmの日記を参考にしました。ursm++。

考察
  • メリット
    • 簡単
  • デメリット
    • OSX でしか動かない
    • activate の関係で、vim(Terminal)からフォーカスが奪われてしまい、保存直後にスムーズに編集を続けられない

2. ChromeReplを使う

blog.8-p.info: Google Chrome で「保存したらリロード」を参考にしました。

ChromeRepl のクライアントをインストール
gem install google-chrome-client
vim コマンドの定義

ChromeStartObserve、ChromeStopObserve コマンドはAppleScript の時と同じで、ChromeReload だけを書き換えてあげればOKです。

command! -bar ChromeReload silent !chrome-repl -e "chrome.tabs.getSelected(null, function (t) { chrome.tabs.executeScript(t.id, { code: 'location.reload()' }) });"
使い方
  • Chrome をオプションつきで起動
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-shell-port=9222


alias の例。

alias chrome="/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-shell-port=9222 > /dev/null 2>&1 &"
考察
  • メリット
    • フォーカスをChrome に奪われない
    • OSX でなくても動く
    • エディタ環境とブラウザ環境が別のホストにあっても平気
  • デメリット
    • OSX の場合、Chrome のデフォルトの起動オプションを指定できない(と思う)ので、Chrome を再起動する度に上記のコマンドを実行する必要があって面倒
    • リモートシェルを起動している状態で、Chromeデベロッパーツールを使うと、ページによってはリロード時に固まってしまった。デベロッパーツールを閉じるとリロードが再開されるので、どこかでブレークしているのかもしれない
    • ruby

3. livereloadを使う

Twiwt:Blog / jugyo : LiveReload & guard-livereloadを参考にしました。
が、まだきちんと動いていないので、こうすれば動くはずという手順を書きます。

livereload をインストール
gem install livereload -v 1.3

最新版の1.4 を使うと、RubyCocoa のライブラリをロードしようとしてエラーしてしまうので、Issues - RubyCocoa is not reliable, get rid of using it (osx/foundation issues etc)にあるとおり、1.3 を使います。


また、ruby < 1.8.7 を使っている場合、livereload.rb 内でEnumerable#reduce を使っている部分でエラーするので、reduce をinject に書き換えてあげます。

livereload のChrome 拡張をインストール

現在Web で配布されている拡張は1.4 向けのものなので、livereload クライアント1.3 と一緒には使えないので、mockko's livereload at v1.3 - GitHubの拡張をインストールします。

  1. livereload 1.3 をローカルにダウンロード
  2. Chrome拡張機能の画面を開く(OSX では、ウィンドウ ⇒ 拡張機能)
  3. デベロッパーモードをクリック
  4. パッケージ化されていない拡張を読み込むを選択
  5. livereload のLiveReload.chromeextension/ のディレクトリを選択し、開く
  6. インストール完了
使い方

指定したディレクトリ以下のファイルに変更があったらリロードするという仕様なので、ディレクトリを指定して起動。指定しなかった場合は、カレントディレクトリ以下のファイルを監視します。

cd path/to/yourapp
livereload


ファイルに変更があったときに、ブラウザのWebSocket をたたくサーバが起動します。次に、ブラウザ側でリロードしたいページを開き、livereload の拡張をON にします。
あとは、ファイルに変化があればブラウザーをリロードしてくれる。


本当はこうすると動くはずなのですが、現在僕の環境ですとファイルの変化が検知されずブラウザにリロードの命令が飛びません。うーん。

考察
  • メリット
    • エディタに依存しない
    • エディタの環境とブラウザの環境が別のホストにあっても平気
      • VM でサーバ起動して、ssh で作業。ブラウザはホストOS 側のを使っている場合とか
    • Safari でも使える
  • デメリット
    • ruby
    • 1.3 だと、監視対象ファイルが多すぎるとlivereload サーバを起動できない。HTML があるディレクトリだけを明示的に指定する必要がある
    • リロードしたいだけでブラウザと通信するサーバをたてるとか大袈裟な感じ


以上、vim でファイルを保存した時にChrome のページをリロードする3つの方法でした。僕の環境ですと、AppleScript 版以外の2つは、動作に問題がありますので、AppleScript 版を使っています(´ω`;)
ChromeRepl、livereload は使い勝手がよいので、いずれこれらに移行したらまたBlog を書きたいと思います。

Use FasterCSV or Ruby 1.9's CSV library

Rubyベストプラクティス -プロフェッショナルによるコードとテクニック

Rubyベストプラクティス -プロフェッショナルによるコードとテクニック


を読んでいたら、標準CSV ライブラリの紹介が載っていた。知らなかったのだけれど、CSV ライブラリは1.9 系でFasterCSV に置き換えられたとのこと。
1.8 系以前のCSV ライブラリはそこまで使いやすいって感じじゃなかった。けれど、FasterCSV、1.9 系のCSV ライブラリを使うことで、CSV の操作をずっと簡単に行えることができるようになる!

CSV 形式のファイルを読み込む

例えば、ヘッダつきのCSV ファイルを読み込むなら、次のように書く必要があった。

require 'csv'

csv = CSV::Reader.parse(DATA)

header = nil

csv.inject([]) do |result, line|
  unless header
    header = line
  else
    result.push Hash[*header.zip(line).flatten]
  end

  result
end

__END__
id,name,age
1,kai,19
2,hong,20
3,bob,


これが、FasterCSV(または1.9 系のCSV) を使うと次のように書けるようになる。(DATA の内容は先のDATA の内容と同じ)

require 'rubygems'
require 'fastercsv'
require 'ruby-debug'

csv = FasterCSV.new(DATA, :headers => true, :header_converters => :symbol)
csv.first[:id]                                     # => "1"
csv.first[:age]                                    # => "20"


また、属性値を変換するConverter も指定することができる。ライブラリには日付と数値の属性値を変換するConverter が含まれていて、その他自分で作成したConverter も簡単に組み込める。
例えば、数値に変換できる値を変換するには、Converter として:integer を指定する。

csv = FasterCSV.new(DATA, :headers => true, :header_converters => :symbol, :converters => :integer)
csv.first[:id]                                     # => 1
csv.first[:age]                                    # => 20

CSV 形式のファイルを書き出す

書き出す場合も、以前のCSV ライブラリだと使いにくい部分があって、例えば以下のように使えるラッパーをつくってた。

User = Struct.new(:id, :name, :age)
elements = [
  User.new(1, 'kai', 19),
  User.new(2, 'hong', 20)
]

FileUtility.to_csv(elements, :header => %w[id name age]) do |e|
  [
    e.id,
    e.name,
    e.age
  ]
end

# => "id,name,age\n1,kai,19\2,hong,20\n"


この場合も、FasterCSV なら次のように書ける。

FasterCSV.generate(:headers => %w[id name age], :write_headers => true){|csv|
  elements.inject(csv){|c, e| 
    c << e
    c 
  }
}

# => "id,name,age\n1,kai,19\2,hong,20\n"


1.8 系のruby を使っているなら、fastercsv を使えばCSV 周りの処理はかなり楽になる丶(´▽`)ノ

Web アプリを書いていてよく使うデザインパターン(Composite パターン)

  • DB に日々のブックマーク数を保存している
  • 指定した期間のブックマークの日別の数を表示
  • ヘッダには指定した期間のブックマークの合計を表示

というような場合を想定。
このような場合は、DB から取ってきた結果をComposite パターンを使った箱に入れると便利。


このくらいの用途だと「sum すればいいじゃん」という話になるけれど、

  • 複数のテーブルからデータを取ってきた結果をComposite に入れて、ひとつのオブジェクトにまとめられて便利
  • Component とComposite の統一インターフェース(上の例ではCounts#count, Count#count)を持てるので、ポリモーフィズムを利用しやすい
  • 入れ物に集計機能がついているので、DB 以外にmemcached から取得した結果に対して使えたりできる

等の利点はあるかなと思います。
また、Composite パターンはruby と相性がいいかなーと思っていて、上の例のCounts に次のようなeach を実装してあげてEnumerable をinclude してあげるとか。で、Counts#each で指定した期間の日付順にCount を取得できるようにするとかできる。

class Counts
  include Enumerable

  def each
    @counts.values.each{|c| yield(c) }
  end
end

RubyKaigi2010 に行ってきた(2, 3日目)

RubyKaigi2010 に行ってきた(1日目) - Slow Danceに続き、今年のRubyKaigi に行ってきたレポートを書きたいと思います!例によって興味があった内容をまとめました。Chad Fowler の基調講演の内容は情熱プログラマー ソフトウェア開発者の幸せな生き方を読むのが一番いいと思います。
また、内容に誤りがありましたらお手数ですが指摘いただけますと助かります。

Ruby powering 9 million dining tables(Cookpad)

Cookpad CTO の橋本さん(@)によるCookpad のサービス、システム全般のお話と、id:secondlife さんが何故Cookpad に入社したかというお話を聴きました。

規模
  • 9.89 M UU / month
  • 83万レシピ
開発体制
  • ユーザにとって価値のあるものを作成するには
  • プロトタイプをどんどん作る
    • プロトタイプのうち、90% はリリースされない
    • どんどん作る環境としてruby, rails はとても適している
      • 食べみるは、ruby 初心者が一ヶ月で開発
  • Agile
    • ユーザに高速に価値を提供することができる
システム構成
  • MySQL 5.5 + Triton
    • 5.1 だったような気もする
    • スライドみようと思ったけど公開されていない
  • Ruby On Rails 2.3
  • Cent OS 5.3
LVS - Web  ---------------- LVS -- App(rails)
          |_ Cache_fs                   |_ Cache(フラグメントキャッシュ)
  • PC版とモバイル版はWeb もApp も別
  • 99% 以上のリクエストを200ms で返すことが出来ている
キャッシュ

使っているキャッシュは2種類。

  • ページキャッシュ
    • レシピページ、トップページ、カテゴリページ等で使っている
    • cache_fs
      • レンダリング結果をcache_fs に保持し、web ⇒ cache_fs とリクエストが行くようにしている(app にはいかない)
      • cache_fs はNFS マウント
      • キャッシュがなくapp に届いた時はapp からNFS を経由してキャッシュをためる
    • 実態はただのファイルなのでcron で1日1回消せばよく運用が楽
  • フラグメントキャッシュ
  • 3つのできないキャッシュ
    1. ようこそ〇〇さん等ユーザ依存のもの
    2. 広告
    3. アクセスログ
    • rails レベルでとっているため、web で返ってしまうとログに残らない
    • 対策としては、ページキャッシュ + ajax で動的部分をリクエストする
      • 以前Web+DB でニコ動もこの方法を使っていると読んだことがある
  • 検索結果のキャッシュ
    • 講演内容に検索結果もページキャッシュしているという部分があり、「クエリパターンを全部キャッシュしているの?」とか「新しいデータ作成されたらキャッシュの再生性を行うの?」とか疑問に思ったので、講演後に中の人に伺いました
    • 1日1回バッチでキャッシュを作成する
      • ログから上位x 件の検索パターンに対して検索結果のキャッシュを生成
      • 検索パターンはいうほど多く無いらしい
      • 1日1回しか作らないので、更新されても次の日までは検索結果に反映されない
      • 本当は良くないけど現在はそうやっいる
DB レプリケーションとacts_as_readonlyable
  • 更新直後はマスタからselect
    • 更新処理はpost で実行しているので、ApplicationController のbefore_filter を使って、post 以外はslave を見るようacts_as_readonlyable のフラグをセットしている
DSR とコネクションプーリングの問題

この問題についてはRails(ActiveRecord)でデータベースへのコネクションプーリングをさせなくする - ククラフトに詳細が書かれている。

  • cookpad ではDB へのアクセスもバランシングしていて、各slave の負荷が均等になるようにしている

App - LVS - DB

  • LVS ではDSR を使ってレスポンスはDB からLVS を通らずApp に返るようにしている
    • ここで問題があって、Rails 2くらいからAR がコネクションプーリングを行うようになった
    • DSR でDB から返ってきたコネクションをプーリングしてしまうと、次からLVS を通らず、一定のslave にアクセスし続けてしまいバランシングできない
  • 対策
    • MySQL のコネクションのコストってcookpad のようなWeb アプリにとっては誤差レベルなのでOFF にした
    • バランシングもされたし、MySQL 側のスレッド数も激減
      • それまでは、Passenger で複数のRails プロセスが起動していて、それぞれが5つプーリングしていたのでかなり無駄なスレッドが立ち上がっていた
マスタDBの分散
  • デュアルマスタ
    • マスター1がダウンした場合はマスター2 に自動切り替えするようにしている
      • ホットスタンバイ状態かな?
    • 切り離された元のマスタは手動で戻す
  • 他に、機能別にマスタを分割
ヘルスチェックの罠
  • とある時にやたらマスタDB の負荷が増える
  • App からマスタDB が生きているか確認している処理があって、その処理にstatus コマンドを使っていた。status コマンドは重い
    • 加えて、App がどんどん増えていくにつれマスタへの負荷が増えていた
  • 対策
    • ping でステータスチェックをするよう変更


やはり、問題が発生したら、原因分析と解決をしっかりとやっているという印象。

画像ファイル運用上の工夫
  • 画像系の規約として画像(パス)を保存しているtable にはphoto_saved_at というカラムを付けるようにしている
    • このカラムの値がNULL なら画像はなし
      • 画像を表示させる部分には全て同じヘルパを使うことでこの規約を適用している
  • 画像はCDN から配信している。キャッシュは長めで1年に設定
    • 画像が変更されたらどうするか
      • photo_saved_at のハッシュ値をパラメータとして付与して画像アクセスしている(js ファイルのキャッシュとかを防ぐ方法と同じ)
      • CDN はURL をキーにしてキャッシュしているのでこの方法でいける
バッチ処理
  • cron で処理を管理
  • 指示はHTTP を通じて行う
  • HTTP で命令が叩かれるのでworker 側には自由な言語を使うことができる
  • worker アプリはRails で実装
全文検索
  • Triton を使っている
  • MySQL のテーブルになっているので、結果を他のテーブルとJOIN することができる
  • MyISAM なのでひとつのマシンでインデクスを作成したあと、テーブルをcp で他のマシンのまくことができる
ログ
  • ログはMySQL のテーブルでとっている
    • CookPad の規模でも全然問題ない
    • 一方この量の解析をMySQL でやるのは厳しいのでHadoop を使っている
監視
    1. Web サーバのレスポンスタイムを監視。ログを流しておいて、200ms 以上かかったものを赤で表示している
    2. ステータスコードを監視して、どのくらいがリダイレクトして、どのくらいが200 を返したがとかを監視している
    3. mysql のshow full process list を監視してつまっているクエリがないかもチェック
    4. http の監視もしている。
    • etc.
オフィス
  • 以前は合宿をやっていたけど、今はオフィスが快適なので毎日合宿のようなもの
  • 壁一面がホワイトボート
質問
  • デザイナ - プログラマ間のコミュニケーションの取り方
    • デザイン専門というのはいない
    • デザイン面では公開前に必ず社長のチェックを受ける
  • ユーザにヒットするモノを作るには
    • 最初に実装されたものは大抵使いづらい。モデルユーザに使ってもらい使いやすくしていく
    • 90% の機能を捨てるという覚悟。本質に集中する
      • 公開している機能もどんどの削る
  • ミッションの共有をどうやっているか
    • 技術のための技術をしない
    • 目的ありきの技術
      • ここの認識がずれていると感じたら話しあって考える


ユーザ視点を本気でつらぬているということがヒシヒシ伝わってくる講演でした。

何故Cookpad に入ったのか(id:secondlife さん)
  • ものづくりの考え方に共感
  • エンジニア全員が技術+αを持っている
    • 話をしていてとても面白い
  • +αとは
    • コードの価値観
      • ここのコードがバグるのは絶対にあってはならないとかビジネスの視点を持っている
    • 多様な視点
    • ものづくりに真剣
  • 本気でユーザのことを思っていれば+αはついてくる
橋本さんへの質問

講演終了後にその場に来ているCookpad のエンジニアが紹介され、質問がある方は直接話かけてみて下さいという形式のフリータイムに突入。
早速橋本さんに質問。

  • 初心者エンジニアが育つにはどうすればいい?

個人的にも聞きたいこと。また、今自分がいる会社のエンジニアは大学時代にプログラムに触れていなかった人が大半のため、その人達が育ちやすい環境をどのように作ればいいと思うか、というあたりを伺ってみました。

  • 基本的にcookpad は中途しかとらない
  • 自身の経験を話すと期日までに作ると決めたら死ぬ気で作っていた
    • でも作っている時はとても楽しい
    • それこそ電車の中でも常にそのことを考えている。周りから見れば変な人だと思う
    • しかし、実際こうやっている時が一番のびた


講演内容は素晴らしいもので、何故大ホール枠じゃないんだろうと思ったほどです。
逆に、大ホールでないから、30分以上の時間を使えるし、質問タイムも長めに設けて下さったので企画部屋枠が一番だったのかもしれません。
本当に勉強になりました。ありがとうございます。

RWikiと怠惰な私の10年間

dRuby の@さんによるRWiki の活用事例、どうやって開発してきたのかの話。

仕組み
  • RWiki のプロセスを長く生かしている
    • そこにCGI を経由してアクセス
NoSQL(ネタ)
  • DB とかを使わないでログファイルに書き込む形式
    • DB とかいらない。なんでもプロセスに入れている
    • RWiki のプロセスが起動時にログファイルを読み込む
    • 基本的にデータはメモリから返して書き込み操作のみをDisk に書いている
  • メモリにはERB オブジェクトだけを保持
  • メモリで処理する
    • 全てのデータをオブジェクトにしてひとつのプロセスに配置
    • 要素が数万程度のArray やHash は普通に使える
活用事例
  • 3万5千ページと2万5千ページの二つのRWiki が1台のマシンで動いている
    • ここにはソースの変更履歴とかBTS の機能もある
  • これでも1.2 G くらいにしかプロセスがふくれない
    • Redmine が100 ページくらいで400M とかにいなって、Rwiki の方がいいと思った
問題点
  • 起動が遅い
    • 起動時にページをメモリに読みながらそのページにあるリンクも再帰的に読み込んで展開していた
    • リンクの展開にはキューを使い、同時に生成される木はひとつになるようにした
    • キャッシュとして、オブジェクトをMarshal.dump した状態でログファイルに書き込むようにした
      • ログには最低限の情報しか保存しないという方針だったけどあまりこだわらないことにした
  • 全文検索が遅い
    • 3万5千ページをscan メソッドで走査していた
      • すごいw
    • Senna を使うようにした
まとめ
  • 全てのオブジェクトをひとつのプロセスに配置するのは自然
    • 自然な設計なので変なトラブルにはまらない
  • メモリが大きくなるが100 ページとかなら全然問題ない


これで、RubyKaigi2010 のレポートは終わりです。
RubyKaigi からそろそろ1ヶ月が経ちますが、本当に良いイベントでした。昨年より多くの人と話すことができたのはシャイなRubyist界隈の自分にとって良い試みだったと思います。その甲斐あって、得られたものはとても大きかったです。
(あと、デモでto_f するところがto_i になっていてテストが落ちるという現場に遭遇したので「to_f 必要です」とツッコんだw)

最後に、RubyKaigi スタッフの皆さんを初め、スピーカー、スポンサー、参加者の全ての皆さんに感謝します。ありがとうございました。

RubyKaigi2010 に行ってきた(1日目)

今年もRubykaigi に行ってきました。とても勉強になることが多かったので興味があった内容をまとめます。
まずは1日目から。2,3日目も後に書きます!

もし間違い等ありましたら指摘いただけると助かります。


Conflicts and Resolutions in Ruby and Rails

Rails のコミッタ達を迎えたトークセッション形式。

  • Rails2 と3 の違い
    • Rails2 の上位互換でありながら様々な機能を盛り込んだ
    • Rails のそれぞれのパーツをRails の外で使えるようモジュール化した
  • Rails チームがRails3 構想時に描いたゴールは達成できたか
    • Yes. 我々コアチームはよくやったと思う
    • 最も良かったのは共通のAPI を作ったこと
  • ruby1.9 対応をする上で難しかったこと
    • encoding。常にencoding について考えなければならなくなった
      • 逆に正しくencoding を扱うプログラムを書けるようになった
  • Rails アプリは早急に1.9 に対応する必要はあるか
    • アプリについては直ぐに対応する必要はないかもしれない
    • 一方ライブラリは早急に対応する必要がある
      • 全てのライブラリがencoding 対応していないとユーザが混乱してしまう
  • Rails3 の良いところ
    • モジュール化は素晴らしい
    • Arel によってRack のように抽象層が入ったことで、ORM 層を切り替えられる仕組みができた
  • Rails3 がまだ改善すべきところ
  • Ruby2.0 について
    • より正しくなるとしても、今までのコードの大半が動かなくなるような変更はあまりして欲しくはない

jpmobile on Rails 3 の作り方

  • 開発の仕方
    • Heroku でテストしている
    • 基本的にRails のコードを読むだけ
    1. ビューのレンダリングを行っている部分を探す
    2. hook を挟める場所を探す
    3. 挟む

リアルタイムウェブができるまで

一部の内容は、個別に質問させていただいたものです。ミントいただきました( ´艸`)

  • セッション保存のフロントとしてredis を使っている
    • 個人的にはTC を使いたかったが、redis 好きがいるのでredis を使っている
    • 現在の規模ならMySQL だけでも十分いけると思う
  • redis
    • ソート処理等色々できる
    • 一方、redis はシングルスレッドでイベントループがまわっているので、重いソートを実行してしまうと、他の処理をブロックしてしまう

Head First ふつうのシステム開発

お客さんから要望を受けて、その機能をリリースするまでをライブ形式で行う。会社でプログラムを書いている身としては、他の会社の開発環境はかなり興味深く、最高の企画のひとつでした。こういうのどんどん増えるといいなぁ。

  • フェーズ
    1. 計画
    2. 朝会
    3. 開発
    4. レビュー
    5. リリース
  • 環境
    • Scrum
    • nginx + Passennger
  • 計画
    • 日頃から要件、バグをBTS に溜めてある
    • その一覧からスプリントでやることを決めておく
    • 見積りはある程度の精度で
    • チームがコミットできることを宣言する
    • 要件、仕様、受け入れ条件を明確にする
  • 要件
    • 要件(ストーリ)を最小単位に分割する
  • 開発
    • テスト書く
  • Q&A
  1. 新人がursm さんとペアプロやるのって大変じゃない?
    • 最初は速いと感じたがなれる。あと、丁寧に教えてくれるので理解できる
  2. コントローラのテストを厳密に書くか
    • 不安に思わないなら書かない。ursm さんは書かない
    • ヘルパのテストは書いた方がいいと思う(kakutani さん)
  3. 見積りはどうやってやるのか
    • 自分の質問。当初予定していなかった点が開発中に見えてきて遅れることが多いので質問
    • 見積りは絶対にひとりではしない。皆でやる
    • 見積りはクイズなので当たらない
    • 遅れたらできるだけ速く謝る
    • 恒常的に遅れているプロジェクトならチームで何が問題なのか話しあう
  4. 皆どのくらいの時間に帰るのか
    • ursm さんが大体最後。21時くらい
    • お客さんの仕事は集中力が切れる19時くらいに終える。その後社内環境改善とかしている

コミュニティナイト

ほぼ懇親会。懇親会のチケットを持っていなかった自分にとってはとても助かりました。

未踏のid:kazuk_i さんを発見。アーキテクチャの話、スケーラビリティについて伺う
  • 最近読み込みを非同期に行うためにcramp を使っている
    • 処理を完全に非同期に扱うためにはライブラリから何からネイティブスレッドに対応していないといけないので、Rails は通せない
      • 非同期処理が必要になる部分はロードバランサでリクエストをcramp に向けるようにしている
  • ライブラリの作り方について
    • 他のライブラリを読んだり、ライブラリの対象のドキュメントを詳細に読んだりとかはあまりしていない。結構適当に作っている
  • 学習の仕方
    • 基本大切。ものがどういう仕組みで動いているかを把握しておく必要はあると思う
  • 無駄な時間を使わないために
    • Cassandra が速いらしい!とか手当たり次第試すのは無駄
    • 事前に手計算したりして方向性をある程度つけておく
      • 例えばキャッシュ戦略を考えるなら、ログを分析してリクエストパターンを抽出
      • 今度扱うデータのサイズはこのくらいで、リクエスト数は、非同期化すると...
永和のid:bekkou68 さんを発見。セッション中に質問しきれなかったことについて伺う
  • プログラミングは入社前からやっていた
    • ruby を始めたのは入社3ヶ月前くらいから
  • 帰るのは6時半くらい
    • 自分の時間をもたないと何をやっているのか分からなくなる
  • 毎日昼休みに何か作っている
  • 皆プログラミング好き
  • 皆で色々な仕組みを作って社内環境を良くしている
  • プロジェクト間の情報共有はML
@ さん発見!!Rubykaigi は漢のイベントだった!
  • Blog を通じて質問させていただいたり、Blog にとても助けられているのでお礼を言う
  • とても柔和な方だった
  • Amazon CAPTCHAはとっても良い本なので、自分がMySQL を管理している立場の人は是非
    • クラッシュしたらどうするか、バックアップは、アップグレードのベストプラクティスは、無停止メンテナンスを行う方等、来るべき時に備えて予め学習しておくべき内容だと思った
    • Explain の見方の章はチューニングのポイントも紹介されていて勉強になる。JOIN のアルゴリズムを説明する図とか、何度もにらめっこしました
    • クライアントの--show-warnings オプション知らなかった。「お前は今までタイポしたshow warnings の数を覚えているか?」な自分にとってはシルバーブレッド


普段疑問に思っていることについて色々質問することができてよかった。やっぱり直接話せるのと空気を感じることができるのが現地に行く最大のメリット!
質問にお答えくださった皆様ありがとうございましたー丶(´▽`)ノ

MySQL のNULL ではまったことあれこれ

MySQL に限らず、SQL のNULL の仕様には何回か「えっ」と驚くことがあったのでメモしておこうと思います。5.1 版の日本語マニュアルがなかったものについては、4.1 のマニュアルを参照しました。

そもそもNULL は何を意味するか

NULL は未定義または、不明を意味する。「電話番号を持たない」ということを表現する場合は、NULL ではなく、空の文字列を使う。

NULL 値というものを SQL 初心者はよく混乱します。SQL 初心者は、多くの場合、NULL が空文字 "" と同じであると考えてしまいます。これは違います。たとえば、以下のステートメントは完全に別のものです。

mysql> INSERT INTO my_table (phone) VALUES (NULL);
mysql> INSERT INTO my_table (phone) VALUES ("");

どちらのステートメントも、値を phone カラムに挿入しています。しかし、最初のステートメントは NULL 値を挿入し、2 つ目は空文字を挿入しています。最初のステートメントは ``電話番号が不明'' であると考えることができ、2番目は``電話を持っていない'' と考えることができます。

MySQL :: MySQL 4.1 リファレンスマニュアル :: A.5.3 NULL 値の問題

NULL との比較演算結果は常にNULL

これは多分、誰もが一度ははまる。
MySQL :: MySQL 4.1 リファレンスマニュアル :: A.5.3 NULL 値の問題

  • =, <, > 等、値のどちらかにNULL が含まれる場合、結果は常にNULL
    • つまり、NULL = NULL はTRUE ではなく、NULL
  • 値がNULL のカラムを検索する場合は、IS NULL を使う
  • CASE val WHEN NULL はヒットしない
    • COALESCE や、CASE WHEN val IS NULL などで対応
  • うっかりJOIN の条件内で= で比較してしまったり


余談として、rails のfind_by_xxx メソッド等動的ファインダは、引数にnil を与えると、IS NULL を生成するようにできている。

NULL と論理演算子

比較演算子以上にはまる。
MySQL :: MySQL 5.1 リファレンスマニュアル :: 11.1.4 論理演算子

  • 論理演算の結果は、TRUE(1)、FALSE(0)、NULL の3つのいずれかになる
  • !NULL はNULL
  • FALSE(0) AND NULL は0、TRUE(1) AND NULL はNULL
  • FALSE(0) OR NULL はNULL、TRUE(1) OR NULL は1


単体だと「ふーん」という感じしかしないかもしれないけど、今回は以下のような例ではまった。

  • タスク管理アプリケーション
  • タスクにはタスク開始、終了予定日を設定できる
  • タスクは、「2010/03/01 - 2010/03/31」のように日付の範囲を指定して検索できる
    • 但し、指定する日付の上限、下限共に「なし」を指定することができる。

で、以下のようなSQL を構築していた。:period_from, :period_to が検索時に指定した値に置き変えらえる。

SELECT * 
FROM tasks 
WHERE !((period_from < :period_from AND period_to < :period_from) OR (:period_to < period_from AND :period_to < period_to))


上記のSQL は、指定する期間の上限、下限両方が指定された場合を想定して組んだもの。ここで、ふと思う。


「:period_from か:period_to のどちらかがNULL だったら、結果は!NULL、つまりTRUE になって、条件式の結果が必ずTRUE になってしまう気がする」


でも、ならない。先程書いた通り、!NULL はNULL だから。:period_from, :period_to の少なくとも1つがNULL だった場合、先程のSQL の結果がTRUE になることはない。FALSE かNULL になってしまう。このように複数の条件式の組合わせによって求められる結果があって、そのどこかの条件式の結果がNULL になる場合、予想外の結果になってしまうことがある。
教訓として、NULL が結果に含まれる場合は「IS NULL」等を使ってNULL を区別して扱う必要がある。NULL の性質を上手く使って…とかダメ。

NULL とインデックス

NULL とソート

MySQL :: MySQL 4.1 リファレンスマニュアル :: A.5.3 NULL 値の問題

  • ORDER BY を使用する際、降順でソートするように DESC を指定すると、NULL 値が最初または最後に表示される
    • 5.0.67 では、DESC の場合はNULL が最後に、ASC の場合は最初にヒットした

NULL と集計

MySQL :: MySQL 4.1 リファレンスマニュアル :: A.5.3 NULL 値の問題

  • COUNT()、MIN()、SUM() などの集約関数では、NULL 値は無視される
    • COUNT(*) は、個々のカラム値ではなくレコードをカウントするので値がNULL のレコードもカウントされる
  • GROUP BY を使用すると、すべての NULL 値が同じと見なされる

TIMESTAMP とNULL

MySQL :: MySQL 5.1 リファレンスマニュアル :: 10.3.1.1 TIMESTAMP MySQL 4.1での性質

  • TIMESTAMP 型はNULL を許可しない
  • TIMESTAMP 型をNULL 許可で定義すると、「NOT NULL DEFAULT CURRENT_TIMESTAMP」と定義される
    • このように定義されたTIMESTAMP 型にNULL を指定すると、現在の時刻がセットされる
    • ADD COLUM で上記のTIMESTAMP 型を追加した場合、それまでに作成されたレコードのカラム値は「0000-00-00 00:00:00」となる


ちなみに、4.1 のマニュアルには以下のように書かれている。

TIMESTAMP 型カラムでは、他のカラム型とは異なる方法で NULL 値が処理される。TIMESTAMP 型カラムには、NULL は格納できない。カラムに NULL を挿入すると、カラムの値として現在の日時が設定される。TIMESTAMP 型のカラムはこのように動作するため、NULL 属性と NOT NULL 属性は通常どおりには適用されず、それらを指定しても無視される。

その一方で、TIMESTAMP 型のカラムを MySQL クライアントにとって使用しやすくするために、サーバは、(実際には、TIMESTAMP 型カラムには NULL 値は格納されないにもかかわらず)TIMESTAMP 型カラムについて、NULL 値の割り当てが可能(true)と報告する。DESCRIBE tbl_name を使用してテーブルに関する記述を取得すると、これを確認できる。

注意: TIMESTAMP 型のカラムに、値として 0 を設定するのは、NULL を設定するのとは異なる。0 は TIMESTAMP 型の有効な値である。

MySQL :: MySQL 4.1 リファレンスマニュアル :: 6.5.3 CREATE TABLE 構文

追記

id:sugibuchi さんが、ブコメNULL撲滅委員会の記事を紹介して下さいました。
NULL の何が問題か、NULL を回避するにはどうしたらいいか等が書かれていて参考になります。記事が若干前のものなので、古い情報もあります。
例えば、IS NULL、IS NOT NULL ではインデックスが使われないと書かれているのですが、少なくともMySQL 4.1.22ではIS NULL、IS NOT NULL でもインデックスが使われることを確認しました。EXPLAIN にもインデックスを使う旨が表示され、パフォーマンスも実際に向上します。
参考になる部分はNULL の回避策。

  • 数値カラムはNULL の変わりに0 を使うとSUM 等に影響がなくて大体上手くいく
  • 日付等は最大値、最小値を使う('0001-01-01', '9999-12-31')といい


「'0001-01-01'」なんて使えるんですね。2038 年問題とかに影響されるのかと思っていました。
NULL オブジェクトパターン的発想ですね。勉強になりました!