faml と slim、hamlit のパフォーマンスの差

http://k0kubun.hatenablog.com/entry/2015/03/31/004021 を見て、「faml は slim と同等とか言いながら slim よりずっと遅いじゃん」と思われると悔しいので一応解説しておく。 なお、このエントリは slim 3.0.3 と hamlit 0.4.2 に基いている。

なぜこのベンチマークで faml が遅いのか、結論から言うと、この中で faml だけ自動 html エスケープが有効になっているからだ。 haml はデフォルトでは自動 html エスケープは無効であり、hamlit もそれに倣っている。

slim のベンチマークで用いている slim のビューでは == を使って明示的に html エスケープを無効化している https://github.com/slim-template/slim/blob/v3.0.3/benchmarks/view.slim 。 そのため、faml だけが自動 html エスケープを行っている。

faml が行ってる slim とのベンチマーク比較では、元々の view.slim から === に変更して html エスケープを有効にしている。 そして haml, hamlit でも escape_html: true というオプションを渡して slim のベンチマークを実行すると、travis-ci 上のある実行では以下のような結果になった。

https://travis-ci.org/eagletmt/faml/jobs/57304778

ruby benchmark/slim.rb
Calculating -------------------------------------
                Haml     1.261k i/100ms
                Faml     3.741k i/100ms
              Hamlit     4.369k i/100ms
                Slim     3.846k i/100ms
-------------------------------------------------
                Haml     14.524k (± 2.6%) i/s -     73.138k
                Faml     52.477k (± 2.3%) i/s -    265.611k
              Hamlit     64.082k (± 2.8%) i/s -    323.306k
                Slim     55.219k (± 1.7%) i/s -    276.912k
Comparison:
              Hamlit:    64082.1 i/s
                Slim:    55219.4 i/s - 1.16x slower
                Faml:    52477.4 i/s - 1.22x slower
                Haml:    14524.2 i/s - 4.41x slower

faml はたしかに slim より遅い結果になっているが、それほど大きな差は無い。 実際、このベンチマークで生成される Ruby のコードにはほとんど差が無い。差といえば、Faml::Helpers を extend してるかどうか、出力する html に改行文字が含まれるかどうか、余分な変数代入や to_s があるかどうか、くらいだ。

% bundle exec slimrb --compile benchmark/view.slim
_buf = []; _buf << ("<!DOCTYPE html><html><head><title>Simple Benchmark</title></head><body><h1>".freeze);
;
;
;
;
; _buf << (::Temple::Utils.escape_html((header)));
; _buf << ("</h1>".freeze); unless item.empty?;
; _buf << ("<ul>".freeze);
; for i in item do;
; if i[:current];
; _buf << ("<li><strong>".freeze);
; _buf << (::Temple::Utils.escape_html((i[:name])));
; _buf << ("</strong></li>".freeze); else;
; _buf << ("<li><a".freeze);
; _slim_codeattributes1 = i[:url]; case (_slim_codeattributes1); when true; _buf << (" href=\"\"".freeze); when false, nil; else; _buf << (" href=\"".freeze); _buf << (::Temple::Utils.escape_html((_slim_codeattributes1))); _buf << ("\"".freeze); end; _buf << (">".freeze); _buf << (::Temple::Utils.escape_html((i[:name])));
; _buf << ("</a></li>".freeze); end; end; _buf << ("</ul>".freeze); else;
; _buf << ("<p>The list is empty.</p>".freeze);
; end; _buf << ("</body></html>".freeze); _buf = _buf.join
% bundle exec faml compile benchmark/view.haml
_buf = []; extend ::Faml::Helpers; _buf << ("<!DOCTYPE html>\n<html>\n<head>\n<title>Simple Benchmark</title>\n</head>\n<body>\n<h1>".freeze);
;
;
;
;
;
; _faml_compiler1 = (header;
; ); _buf << (::Temple::Utils.escape_html((_faml_compiler1.to_s))); _buf << ("</h1>\n".freeze); unless item.empty?;
; _buf << ("<ul>\n".freeze);
; for i in item;
; if i[:current];
; _buf << ("<li>\n<strong>".freeze);
; _faml_compiler2 = (i[:name];
; ); _buf << (::Temple::Utils.escape_html((_faml_compiler2.to_s))); _buf << ("</strong>\n</li>\n".freeze); else;
; _buf << ("<li>\n<a".freeze);
; _faml_html1 = (i[:url]); case (_faml_html1); when true; _buf << (" href".freeze); when false, nil; else; _buf << (" href='".freeze); _buf << (::Temple::Utils.escape_html((_faml_html1))); _buf << ("'".freeze); end; _buf << (">".freeze); _faml_compiler3 = (i[:name];
; ); _buf << (::Temple::Utils.escape_html((_faml_compiler3.to_s))); _buf << ("</a>\n</li>\n".freeze); end; end; _buf << ("</ul>\n".freeze); else;
; _buf << ("<p>The list is empty.</p>\n".freeze);
; end; _buf << ("</body>\n</html>\n".freeze); _buf = _buf.join

さて、このベンチマークではたしかに hamlit が faml や slim をはっきり上回っている。hamlit 0.4.2 がどのような Ruby のコードを生成しているか見てみる。

% bundle exec ruby -e 'require "hamlit"; puts Hamlit::Engine.new(escape_html: true).call(File.read("benchmark/view.haml"))'
_buf = []; _buf << ("<!DOCTYPE html>\n<html>\n<head>\n<title>Simple Benchmark</title>\n</head>\n<body>\n<h1>".freeze);
;
;
;
; _buf << (::Temple::Utils.escape_html(((header).to_s))); _buf << ("</h1>\n".freeze);
; unless item.empty?; _buf << ("<ul>\n".freeze); for i in item; if i[:current]; _buf << ("<li>\n<strong>".freeze); _buf << (::Temple::Utils.escape_html(((i[:name]).to_s))); _buf << ("</strong>\n</li>\n".freeze);
;
;
; else; _buf << ("<li>\n<a href='".freeze); _buf << (i[:url]); _buf << ("'>".freeze); _buf << (::Temple::Utils.escape_html(((i[:name]).to_s))); _buf << ("</a>\n</li>\n".freeze);
;
; end;
; end;
; _buf << ("</ul>\n".freeze);
;
; else; _buf << ("<p>The list is empty.</p>\n".freeze);
; end;
; _buf << ("</body>\n</html>\n".freeze);
;
; _buf = _buf.join

headerfor i in item のような埋め込み式の行番号がずれてしまってる問題は今回は無視するとして*1、よく見てみると i[:url] がそのまま埋め込まれている。 これには2つの問題がある。

  1. &< 等が html エスケープされない
    • これはさすがに厳しいのでは*2
  2. i[:url]nil や false だったとき、<a href='false'> のような出力になる
    • haml では、属性の値が nil または false のときは、その属性を出力しない仕様
    • ちなみに slim が生成したコードを見てわかるように、slim も同じ仕様
    • ただ、これに関しては「hamlit ではこういう仕様です」と言うこともできると思う

というわけで、この差によって slim のベンチマークにおいて hamlit が高速になっていると考えられる。

最後に恣意的に用意したマイクロベンチマークの結果を自慢しておくと、実行時に Hash のマージと attribute 全体のレンダリングを行う必要があるケースでは faml が最も高速になっている。 というか、slim は文法はともかくコンパイラは本当によくできていて、この点くらいしかぼくには勝てそうにない。 先程と同じ travis-ci 上での実行から引用すると、

ruby benchmark/rendering.rb benchmark/attribute_builder.haml benchmark/attribute_builder.slim
Calculating -------------------------------------
                Haml   693.000  i/100ms
        Faml (Array)     2.417k i/100ms
       Faml (String)     2.064k i/100ms
      Hamlit (Array)     1.896k i/100ms
     Hamlit (String)     1.712k i/100ms
        Slim (Array)     2.005k i/100ms
       Slim (String)     1.653k i/100ms
-------------------------------------------------
                Haml      7.767k (± 4.4%) i/s -     38.808k
        Faml (Array)     30.186k (±17.5%) i/s -    147.437k
       Faml (String)     28.314k (±10.8%) i/s -    140.352k
      Hamlit (Array)     22.381k (±20.5%) i/s -    106.176k
     Hamlit (String)     21.914k (±10.4%) i/s -    109.568k
        Slim (Array)     26.194k (± 9.7%) i/s -    130.325k
       Slim (String)     20.715k (± 5.7%) i/s -    104.139k
Comparison:
        Faml (Array):    30186.2 i/s
       Faml (String):    28313.7 i/s - 1.07x slower
        Slim (Array):    26194.2 i/s - 1.15x slower
      Hamlit (Array):    22381.0 i/s - 1.35x slower
     Hamlit (String):    21914.2 i/s - 1.38x slower
       Slim (String):    20714.7 i/s - 1.46x slower
                Haml:     7766.6 i/s - 3.89x slower

となっている。haml 遅いですね……

haml の高速なレンダリングエンジン faml を書いた

haml との互換性にはかなり気を使っているけど、一部意図的に非互換にしていたり、正確な仕様がわからず再現できていない箇所があったり、haml の奇妙な挙動が直っていたりして、完全に全く同じ動作にはなっていない。

先日 faml を本番に投入して今も動いている。faml 導入にあたって実際にはアプリケーション側のビューを一部書き換えたけど、大量のビューがある中での変更点は十分少なかったと思う。

なお、このエントリ内での「元々の haml」は haml 4.0.6 を指している。過去のバージョンは知らない。

どれくらい高速なのか

元々 faml を書き始めたきっかけは「haml と slim に文法的に大きな差があるわけではないんだし、slim と同程度高速な hamlレンダリングエンジンは書けるはず」という点だった。 なので、slim と同じくらい高速ではあるものの、slim 以上に高速というわけではない。ただ、faml は一部 C 拡張を利用しているので slim よりも若干高速なケースは存在する。

マイクロベンチは faml のリポジトリ内で rake benchmark すれば実行できる。 travis-ci の after_script で rake benchmark を実行しているので、そこで結果を見ることができる。 例: https://travis-ci.org/eagletmt/faml/jobs/57138591

なぜ高速なのか

まず slim と同じバックエンドの temple を使っている。 これはシンプルながら強力なライブラリで、temple が定めた S 式にコンパイルすると temple がそれを Ruby のコードに変換してくれ、更に S 式を変換するフィルタを間に挟むことができる。 このフィルタにより最適化を行ったり、独自の文法を導入しつつそのコンパイラを挟んだりできる。

また、属性の描画を高速化するためになるべく静的にコンパイルするようにしている。 例えば %input{checked: false} とあったらリテラルしかないので静的に <input> という文字列まで作ることができる。 これを達成するには Ruby のコードを解釈する必要があるので、そのためのライブラリとして parser を使っている。 Ruby のパーサとしては標準ライブラリの ripper や ruby_parser があるけど、僕は parser が一番使いやすいと思っている。 各 AST ノードに場所の情報がちゃんと入っていたり、その場所に書いてあったコード片を文字列として取り出せるのがいい。

属性を静的にコンパイルできなかった場合は実行時に描画することになるが、この部分で C 拡張を使っている。 これは slim には無い特徴なので、動的な属性が多く存在するようなページでは slim よりも若干高速になるかもしれない。

実際に Web アプリケーションが高速になるのか

完全にアプリケーションによると思っていて、1ページ内で多くのビューをレンダリングしていて haml が支配的である場合も、そうでない場合もある。 多くの場合、スロークエリを改善したり、適切にキャッシュを導入したり、アルゴリズムを改善するほうがずっと効果がありそう。 ただ、faml は haml と高い互換性を維持しているので、アプリケーションコードを変更せずとりあえず faml に入れ替えてみるだけで、一部のページで効果があるかもしれない。この点は魅力的だと思う。

非互換な部分は何なのか

属性に Hash を渡したときの挙動

元々の haml では %span{foo: {bar: 'baz'}}<span foo-bar='baz'></span> となるが、faml では <span foo='{:bar=&gt;&quot;baz&quot;}'></span> となる。 ただし、%span{data: {bar: 'baz'}}<span data-bar='baz'></span> となる。 このように、Hash を渡したときは data 属性の場合に限り hyphenate され、それ以外は単に to_s される。

高速化のために意図的にこのような非互換性を導入している。 Hash の hyphenate は属性の描画の中では比較的重いものなので、なるべく to_s だけで済ませたい。 この挙動は data 属性のときにしか普通使わないはずだと思い、data 属性を特別扱いしている。

ちなみに、一部の属性を特別扱いするのは元の haml にも存在して、idclass が該当する。 id に Array を渡すと join("_") した値がセットされ、class に Array を渡すと join(" ") した値がセットされる。 class のほうの挙動を知ってる人は多そうだけど、id のほうは知らなかった人も結構いるんじゃないかと思う。

常時自動 html エスケープ

元々の haml では Rails プロジェクトでなければデフォルトでは自動 html エスケープが無効になっている。 たまに sinatra を使ったときにはまったりするので、faml では自動 html エスケープを常に有効化している。デフォルトを無効にする手段は提供していない。 Rails プロジェクト以外で html エスケープを無効化したい箇所では != 等の文法を使う。

ugly モードのみ

元々の haml では html をインデントするかどうかを示す ugly というフラグがあったけど、faml では常に ugly、つまりインデントを行わない。

Haml::Helpers 無し

succeed や surround といった haml 固有のヘルパメソッドが存在していたけど、faml では preserve 以外は提供していない。 preserve は使い所がまれに存在するけど、それ以外は無くても困らないと思って外した。 もし他に本当に必要なヘルパメソッドがあったら issue を立てて教えてください。

Object reference 無し

この機能を使ってる人いるんだろうか…… http://haml.info/docs/yardoc/file.REFERENCE.html#object_reference_

whitespace removal の仕様

haml には >< によって空白を取り除く文法があって、これの詳細な仕様が不明で再現しきれてない。 もし明らかに間違っているものを見つけたら issue を立てて教えてください。

元々の haml 側の挙動も怪しくて、例えば

%div<
  hello
  world
%div<
  #{'hello'}
  world

を render するとどうなるかというと、

<div>hello
world</div>
<div>helloworld</div>

になる。faml は前者の挙動が正しいと判断し、

<div>hello
world</div>
<div>hello
world</div>

となるようにしている。

haml のバグ、あるいは一貫性の無い仕様

plain フィルタに interpolation が含まれていたときの空行

whitespace removal で触れたケースに似てるけど、

:plain
  hello
  world
%br
:plain
  #{'hello'}
  world
%br

を ugly mode で render すると、元々の haml では

hello
world
<br>
hello
world

<br>

となって、謎の改行が入る。faml ではこれも haml のバグだと判断し、

hello
world
<br>
hello
world
<br>

となるようにしている。 ちなみに markdown フィルダでも同様の謎の改行が存在する。

空の plain フィルタでの空行

また、同じく :plain において、:plain の中身が空だったときに空行が入るかどうかに差がある。

%br
:plain
%br

これを元々の haml で render すると

<br>

<br>

になる一方、faml では

<br>
<br>

になる。

! または & で始まるテキスト

haml において、行頭の ! または & には意味があり、それぞれ html エスケープをしない、する、という意味を持つ。 != ではなく ! だけの場合は、その後にくるテキスト内の interpolation において html エスケープをするかしないかを制御する。

%span!hello
!hello
%span! hello
! hello

を元々の haml が render すると

<span>hello</span>
!hello
<span>hello</span>
hello

となり、タグ付きの場合は ! の後に空白がなくても !haml の記号として解釈される一方、タグ無しのテキストの場合は ! の後に空白がないと haml の記号として解釈されない。

faml では

<span>hello</span>
hello
<span>hello</span>
hello

となり、どちらの場合も行頭の !haml の記号として解釈する。& の場合も同様。 !& で始まるテキストを書く場合は、行頭を == または \ でエスケープする必要がある。

まとめ

既に haml から slim に移行してしまったり、最近だとクライアントサイド JS での描画やモバイルアプリケーションがメインになってサーバサイドは単なる JSON API サーバになってるところも多そうだけど、もし haml が好きで使ってるけど遅いのが気になっている人がいれば是非 faml を試してみてほしいです。

テンプレートエンジンとバックトレース

最近 haml の別実装 を書いてみていて、コード生成部分でバックトレースのことを考える必要があることに途中で気付いて、haml や slim が生成するコード中の謎の改行の理由がわかった話。

Ruby でよく使われる HTML テンプレートエンジンとして hamlslim がある。 どちらのテンプレートエンジンも、大まかなしくみとしては、

  1. ソース言語から Ruby のコードを生成 (コンパイル)
  2. 生成した Ruby のコードを eval してメソッドに変換
  3. render の際は、適切にインスタンス変数やローカル変数等を与えてメソッド呼び出し

という実装になっている。

haml も slim も、テンプレート中に (ほぼ) 任意の Ruby の式を埋め込むことができる。 すると、テンプレートの render 中に例外が発生することも当然ありうる。 このとき、バックトレースにはちゃんと例外の発生箇所が記録されていてほしい。

テンプレートエンジンのソース言語は Ruby ではないので、例外発生箇所を正しく記録するためには、Ruby の式が埋め込まれる箇所に関してはソース言語とコンパイル後のコードの間で行番号を一致させる必要がある。 たとえば、

%div
  %ul
    - @items.each do |x|
      %li= 'item: ' + x

という haml テンプレートに対して、haml 4.0.6 は次のようなコードを生成する。

_hamlout.buffer << "<div>\n<ul>\n";

@items.each do |x|
_hamlout.buffer << "<li>#{_hamlout.format_script_false_true_false_true_false_true_true(('item: ' + x
));}</li>\n";end;_hamlout.buffer << "</ul>\n</div>\n";;_erbout

このように途中に謎の空行を挟みつつ、Ruby の式が埋め込まれている @items.each'item: ' + x に関してはソース言語と行番号が一致するようにコンパイルされている。 この対応のおかげで、主に開発中に例外が発生した際のバックトレースがテンプレート言語内であっても正確に表示できるようになっている。

C のコードを生成するようなツールだと #line ディレクティブを使って行番号をあわせていることがあるけど、Ruby にはそのようなディレクティブは無いので、コンパイルする際にがんばって行番号をあわせる必要がある。

Unicorn の graceful restart と環境変数

Unicorn の graceful restart は無停止でのデプロイを可能にして非常に便利だが、fork を用いて実装されている都合で古いプロセスから新しいプロセスに環境変数が引き継がれるため、そのことに起因するトラブルがいくつかある。

dotenv の設定が書き変わらない

設定情報を dotenv で管理している人も多いと思うけど、環境変数を使っているので罠がある。

例えば最初に .env に MEMCACHE_SERVERS=memcache-server-001:11211 と書いてあったとする。 このとき Unicorn を起動すると、dotenv によって MEMCACHE_SERVERS=memcache-server-001:11211環境変数に追加される。

その後、接続先として memcache-server-002:11211 を追加したくなって .env を編集して MEMCACHE_SERVERS=memcache-server-001:11211,memcache-server-002:11211 に変える。 ここで Unicorn を graceful restart すると、古い MEMCACHE_SERVERS の値は次のプロセスにも引き継がれ、dotenv はデフォルトでは既にある環境変数を上書きしないため、新しいプロセスでも MEMCACHE_SERVERS の値は古いままで、memcache-server-002:11211 が追加されない。

Bundler のバージョンが上がらない

Bundler環境変数 RUBYLIBRUBYOPT を指定することで、Ruby の起動時に Bundler を読み込ませセットアップを行うようなしくみになっている。 RUBYLIB には初回起動時の Bundler の libdir が入っており、Unicorn を graceful restart してもこの環境変数が引き継がれるため、同じく引き継がれた RUBYOPT=-rbundler/setup によりずっと初回起動時の Bundler の libdir から bundler/setup がロードされ続ける。

解決策

どちらも before_exec でなんとかすることができそう。

dotenv については Dotenv.overload を呼ぶことで .env の内容で環境変数を上書きできる。 なので、before_exec でこれを呼ぶことによって新しい設定が使われるようにできる。 もし .env 以外の方法でセットしている環境変数があった場合、そこでセットされたものよりも .env が優先されるようになってしまうけど、まぁ .env 使ってるときにそういう環境変数は無いと思う。

before_exec do |server|
  Dotenv.overload
end

Bundler については RUBYLIB を最新の Bundler を指すように書き換えてやればいいわけで、ややトリッキーな方法だが以下のような before_exec を書くことでできそう。 もしデフォルトで RUBYLIB, RUBYOPT, GEM_HOME が存在するような環境なら、env.delete のかわりにその値をセットするようにすればいいはず。

before_exec do |server|
  env = ENV.to_hash
  %w[RUBYLIB RUBYOPT GEM_HOME].each do |key|
    env.delete(key)
  end
  rubylib = IO.popen([env, 'bundle', 'exec', 'ruby', '-e', 'puts ENV["RUBYLIB"]', unsetenv_others: true], &:read).chomp
  ENV['RUBYLIB'] = rubylib
end

追記

RUBYLIB を出力するときにわざわざ ruby を使う必要は無いという指摘があった。たしかに bundle exec env でよさそう。

before_exec do |server|
  env = ENV.to_hash
  %w[RUBYLIB RUBYOPT GEM_HOME].each do |key|
    env.delete(key)
  end
  rubylib = IO.popen([env, 'bundle', 'exec', 'env', unsetenv_others: true], &:read).slice(/^RUBYLIB=(.+)$/, 1).chomp
  ENV['RUBYLIB'] = rubylib
end

pt3_drv を使ってる人は Linux 3.18 以上に上げるときに注意

Linux で PT3 を利用している人は pt3_drv を使ってる人が多いと思うけど、Linux 3.18 から earth-pt3 という DVB 版の PT3 ドライバが入るようになって、そのままだと pt3_drv と競合して /dev/pt3video* が生えてこなくなる。

% uname -r
3.18.2-2-ARCH
% zgrep CONFIG_DVB_PT3 /proc/config.gz
CONFIG_DVB_PT3=m

DVB 版に移行してもいいと思うけど、今まで通り pt3_drv を使い続けたい場合は earth-pt3 を blacklist に入れておけばいい。

% cat /etc/modprobe.d/earth-pt3.conf
blacklist earth-pt3

2014年の思い出

しゃかいじんいちねんめ

新生活

無事ストレートで修士課程を終えて社会人になった。 それに合わせて3月に引っ越した。飲み会等でオフィスやその付近でぐだぐだしていても歩いて帰れるので便利。 部屋の広さとか立地とかそれに対する家賃とかには満足してるんだけど、浴室にやや不満があるので遠くないうちにまた引っ越すかも…… 大学で地理的に離れてしまった地元の友人の中に就職のタイミングで上京してきた人もいて、そういう人と会いやすくなったりした。

仕事

最初の一ヶ月は研修受けたりほうれん草収穫したり1週間で Android アプリ作ったりしてた。 その後、Rails のバージョンアップに関連した作業をしたり、アセットまわりを改修したり、小さい Web アプリケーションを golang で書いて Docker を使って動かそうとしたりしていた。 だいたい6, 7割は Web アプリケーションを書いてて、2, 3割が puppet や itamae 書いたりトラブル・アラート対応したりといったかんじだったと思う。 Web アプリケーションを書くといっても新規に機能を追加することはあんまり無く、レガシーっぽいところに対処したり、独自のモンキーパッチでレールから外れたり gem を更新しにくくなったりしているところを何とかしたりしていた。

LT

イベントでの発表は実はこれまで一回だけあったんだけど、今年は RubyKaigi 2014 LTRails複数DB Casual Talks の2回あって、どちらも switch_point の話をした。 プレゼンに対して苦手意識があって、実際伝わりにくい発表だったと思うので、伝え方とか自慢の仕方とかをもっと意識したい。 まぁ switch_point に関しては使わずに済むならそれに越したことはない gem なので、これくらいでちょうどいいのかもしれない……

録画

PC で録画し始めてから去年までずっと使い続けてきたスクリプト群をゼロから書き直した。 これにより同じ番組であっても異なる局で重複して録画するようになり、さらに引越しの機会に BS も視聴できるようになったので、ディスクの消費スピードが高まった。 各局で録画するので、L字やテロップがあったときでも適当に補完できるようになった一方、副作用としてテロップのある回を録画する機会が増えて、あえて残すようになった。 社会人になってから時間をとりにくくなったので実際に視聴する番組は更に減った。

アニメ

いなりこんこん恋いろは、アイカツラブライブ2期、プリズマイリヤ2期、東京喰種がよかった。 あと悪魔のリドル、グリザイアの果実も楽しめた。 まだ放送中だけど、SHIROBAKO、クロスアンジュもよさそう。 社会人になったので積極的に円盤を集めていきたい。

漫画・ラノベ

2月くらいから全部 bookwalker で買うようになった (それまでは Kindle Store で買ってた)。 今年になってから読んだ作品でアニメ見てからじゃないものの中だと citrus がすごくよかった。 数年前で止まってたコップクラフトの新刊が出たのも嬉しかったけど、なんか繋ぎみたいな話だったので早く次が欲しい。

Rails複数DB Casual Talksで話した

先日行われた Rails複数DB Casual Talks というイベントで複数 DB に関連するつらい話をした https://speakerdeck.com/eagletmt/fu-shu-dbtorails 。 r/w splitting を行う switch_point については以前の記事でもう少し詳しく書いてます。

質疑のときに出ていた複数アプリケーション間のモデルの共有方法についてちょっと補足。 僕は今年の新卒入社なのでそれ以前にどうやっていたかはよく知らないけど、本体と管理画面のようにモデルを共有するくらい密に結合しているアプリケーションにおいて1つのリポジトリに入れることは、つらさはあるけど理にかなっていると思う。 ちなみに以前は git submodule で共有していたこともあったらしいです。

gem とか git submodule とかでうまく分けられている事例があったら話を聞きたい……

複数アプリケーション + 複数 DB で不必要な establish_connection をしなくする方法、つまり必要なモデルだけロードする方法は1つわかっていて、App 1 から使われるモデル、App 1 と App 2 から使われるモデル、App 2 と App 3 から使われるモデル、…… みたいに細かく Rails Engine として切り出して、それらに合わせて各アプリケーションでそれぞれ必要なものを require するというもの。 ただ、この方法だとモデルファイルが1つのリポジトリの中で色んなディレクトリに散在してしまうことになって、それはそれでイマイチ感がある。 というわけで何かいい方法ないですかねーという話を最後にしたんですが、この状況に悩んでいるのはうちだけな気はしてます。