zephiransasのチラシの裏

とあるJava/Rubyプログラマのメモ代わりブログ

スクフェス・ログサーバをつくった

今日は大晦日ですね。年末ですが今年も例によって、コード書いたりプラモ作ったり、普段の連休と同じくダラダラ過ごしております。

ところで今年のラブライブ!アドベントカレンダーはチェックしましたか? 自分も20日目に劇場版ラブライブとμ’sメンバーのその後というタイトルでエントリしてます。 今年はその他にも、さまざまな視点から見た素晴らしいエントリがたくさん集まってますので、ラブライバーならぜひチェックしてみてください。

で、22日目のエントリには@hideo54さんのスクフェスのライブスコアを取得する”schfeslog”を作った話というのがあります。 これはnodeで建てたプロキシを使って、スクフェスがサーバに送信してる通信内容をみて、ライブのプレイ結果をツイートすることができるツールです。

これをみて「お、ツイートできるんなら、外部サーバにも送信できるんじゃね?」ってことで、早速コードを書いてみました。

まずはschfeslog側に外部サーバへの通信機能を実装しています。該当するPull Requestはこちら。単純にプレイデータをJSON形式にして、設定でされたサーバにPOSTするだけです。

これを受信するサーバはこちら。

送信されたプレイデータを一覧で見ることもできます。ちなみに私のプレイデータがこちら

見た目とかは、もうちょっと改善したいところです・・・

最近、ちょっとJavaの案件をやってるせいもあって、真面目にSpring Bootで書いています。こういったRESTなアプリケーションを作るにはSpring Bootはとても簡単でいいですね。

サーバ側は簡単に自分用に環境を作れるよう、Deploy to Herokuボタンも準備してますので、興味のあるかたはschfeslogと一緒に、ぜひ試してみてください。

合同勉強会2016に参加してきた

このエントリは大都会岡山アドベントカレンダー2016の18日目のエントリです。

昨日のエントリは たがみだいきさんの「RailsGirlsOkayama 2ndを2月25日26日に開催する予定です!」でした。 去年の第1回には、自分もコーチとして参加させてもらいましたが、そこから新たに岡山に女性エンジニアのコミュニティができ、それが主体となってRailsGirlsをやる、というのは素晴らしいことだと思います。きっとイベントも大成功することでしょう。

先日、毎年恒例となった合同勉強会と忘年会議が開催されました。

例年のように県内だけでなく県外からも多くのかたに参加していただき、大変よいイベントになったのではないでしょうか。

例によって写真を撮ってますのでこちらからどうぞ。

合同勉強会&忘年会議2016

個人的に今回のベストトーカー(not ベストストーカー)を上げたいのは、SamunePさんの「ゲーム会社経営ゲーム」。 とにかく高い技術力を武器に、ちょっと尻込みしちゃうような案件をやってるハナシで、とにかく「今の時代にこんな人がいるのか?!」と驚きの連続でした。その凄さをみんなもうまく言語化できずに

などなど、人のボキャブラリーを著しく低下させる作用があったようですw

さて、次は来年のオープンセミナーです。こちらも面白いセッションをたくさん用意できるよう、誠意準備中ですので、ぜひご参加ください!

Maven Wrapperを使ってプロジェクトで使うMavenのバージョンを指定する

Javaでの開発において、ライブラリのバージョン管理にMavenを用いているところはたくさんあると思います。

しかし、pom.xmlを使って各ライブラリのバージョンを管理していても、各開発者が使うMavenのバージョンを固定することはできません。

プロジェクトで使うMavenのバージョンを固定したい!そんな場合に使えるのがMaven Wrapperです。

導入方法

導入方法は至って簡単。

maven wrapperを適用したいプロジェクトに移動して、以下のコマンドを発行するだけ。

1
mvn -N io.takari:maven:wrapper

これだけで、プロジェクトに以下のファイルが追加されます。

  • mvnw - Maven Wrapper経由でmavenを実行するためのファイル
  • mvnw.cmd - mvnwのWindows版。Windowsで使う場合はこっちを使いましょう。
  • .mvnディレクトリ - maven wraperがダウンロードしてきたMavenのバイナリとかが入ってる

上記のコマンドだと実行時の最新のバージョンが使用されるので、バージョンを指定したい場合はオプションで

1
mvn -N io.takari:maven:wrapper -Dmaven=3.3.1

としてやりましょう。以降は今まで

1
2
mvn clean
mvn package

としていたのをmvnwコマンドに置き換えるだけで

1
2
./mvnw clean
./mvnw package

固定されたバージョンをMavenを利用することができます。

.gitignoreの設定

~~ Gitなどのバージョン管理にはmvnwとmvnw.cmdのみコミット対象とし、.mvnディレクトリはコミット対象外にしましょう。 ~~

はい、これウソでしたorz

正しくは「.mvnディレクトリもコミットしましょう」です。

はい、その通りですね(真顔

Past, Present and Future

これは大都会アドベントカレンダー24日目の記事です。

昨日は@yantonaさん君は「玉野音頭」を知っているかでした。

不覚にも自分は知りませんでした・・・瀬戸大橋音頭なら・・・!

実は今年の大都会アドベントカレンダー、12日目のきよくらさんが自分とIT勉強会とのかかわり合いについて書かれていて、これにインスパイアされたので、便乗して自分が岡山のIT勉強会に関わって、岡山Javaユーザ会を開催するようになるまでを、まとめてみようと思います。

岡山でJavaの勉強会をスタートするまで

2010年

2010年、当時自分は岡山にある小さなSIerで仕事をしていました。当時やっていたのは主にJavaを使ってのWeb開発で、Apache,Seasarなどのオープンソースソフトウェアを好んで使っていました。小さな会社だったこともあり、新しい技術の導入には比較的寛容な環境だったかもしれません。

そんなとき、ネットで偶然見かけたのがオープンラボ岡山でした。勉強会についてはほとんど知らなかったし、それほど興味も無かったのですが、偶然Seasarの作者であるひがやすおさんが、岡山に来られるということだけで参加しに行きました。

この時にはSeasarの開発も一時期よりは落ち着いてきて、ひがさん自身も「これからはクラウドだ!GAEだ!Slim3だ!」と、Slim3についての話をされていたことを覚えています。

同時に年末だったこともあり、忘年会議も開催されていますが、これには参加してませんでした・・・

思えば、ここが最初のスタートです。

2011年

去年参加したオープンラボをキッカケに、2011年からは勉強会にも積極的に参加するようになりました。

このころはオープンラボ岡山は月に1度のペースで開催されていましたので、ほぼ毎月参加していたように思います。そしてそのなかで、いろいろな人から、技術に関する話を聞くうちに、勉強会に参加する面白さを感じることができるようになったと思います。

当時は勉強会に登壇する人全てが、雲の上の人のように感じていましたね。

それと同時に「人の話を聞いてるだけじゃなく、自分も話せるようになって、みんなに喜んで欲しい」という気持ちも同時に芽生え始めたように思います。

そして転機は第19回オープンラボ岡山のときです。

この回はJava特集ということで、当時は日本オラクル所属だった寺田佳央さんと、RedHatの木村貴由さんのお二人からJavaについての話を聞きました。そこで寺田さんはGlassFishの話をされたのですが、これが個人的に一番興味をそそられたので、その後も個人的にいろいろ調べ、以下のブログを書きました。

このエントリを書いたことをキッカケに、オープンラボ岡山の常連であり、隣国?である福山で「オープンラボ備後」を主催されていた、Yさんから「GlassFishの話してみない?」という依頼がありました。そして第10回オープンラボ備後で初登壇することになります。

図らずも「いつか自分も発表できるようになりたい」と思っていたことが、たった1つのブログエントリから1年たたずに実現したわけです。しかしこれは運とかではなく、岡山周辺のIT勉強会コミュニティに、いろいろな人を登壇させていこうという風土のようなものが、ちゃんと根付いていたことの現れではないかと思っています。

そしてこの登壇あと、主催のYさんが唐突に「吉田さん、岡山でJavaのコミュニティやってくれない?」という話がありました。

聞けば、以前、岡山にはJavaのコミュニティがあったのですが、主催の方の転勤とともに開店休業状態にあるということ。そこで、自分にコミュニティの主催をやってみないか?ということでした。

今回が初登壇だった自分にいきなりそんなこと・・・とは、正直思いましたが、オープンラボ備後のYさんの勧めや、オープンラボ岡山の主催Hさんからもサポートしていただける、とのことだったので、岡山でJavaのコミュニティを始めることにしました。

幸いにも、自分ひとりでのスタートではなく、いつもScalaや関数型の話でみなをポカンとさせる@razonさんや、Twitterの裏垢で下ネタをつぶやいてばかりの@ryosmsさん、そして唯一?の常識人の@o310yusukeさん、などのこころ強い(?)メンバーの助けを借りつつ、岡山Javaユーザ会をスタートさせることになります。

その他にも助けて頂いた方はたくさんいますが、誰一人欠けても今までコミュニティを継続させることはできなかったと思います。

この場を借りて、関わった全ての皆様に深く感謝したいと思います。

あなたはエンジニアの仕事、好きですか?

ところで、あなたはエンジニアの仕事、好きですか?

エンジニアでない人、今の仕事好きですか?

私はエンジニアの仕事が大好きです。

自分はプログラマを目指して大学に入ったものの挫折し、その後のフリーター生活を経て、24歳の時に故郷に戻って運良くプログラマとして就職することができました。それから約16年。何事にも中途半端だった自分が、こうやって一つの仕事を続けてこられたのは、ひとえに「この仕事が好き」だからです。

だからこそ、このエンジニアという仕事を選んだ人たちが、楽しく仕事ができるようになればいいと思い、いままで岡山のIT勉強会コミュニティに関わってきました。

そしてその想いは、今でもかわることはありません。これからも岡山のIT勉強会コミュニティを影となって支えて参ります。

the Future

そしてもう一つ。来年に新しいことをスタートしようと考えています。

それが子供向けプログラミング教室です。

自分がはじめてプログラミングをしたのは、小学校の時に触れたファミリーベーシックでした。そこで説明書のプログラム通りに、右手の人差し指1本で、おぼつかない手でコードを書き、エラーを直し、実行し、そして画面でマリオが動いた時のあの感動。

この教室を通じて、自分が幼いときに感じた「自分で考え、自分で作って、動かす」そんな感動を子供たちに感じてもらい「プログラミングって楽しい!」と感じてくれる子供が一人でも増えてくれれば、これに勝る喜びはありません。

あくまでボランティアベースの教室になるので、塾のように授業料をもらうこともないです。なので成果を保証することもしません。子供たちがやりたいことを最優先に。やりたいことが見つからなければ、見つける手伝いから。

そして子供たちが「楽しかった、また来たい!」と思えるように。そんな空間を作れたらいいなと思っています。

既存のIT勉強会のコミュニティは、現役のエンジニアたちを支え、子供向けプログラミング教室が、未来のエンジニアを支える。

そしてこの両輪を回すことを、今後の自分の仕事として続けていけたら、と思っています。

合同勉強会でCrystalの話をしてきた

これはCrystalアドベントカレンダー2015の12/9のエントリです。

昨日はpine613さんの「Crystal-JP の活動紹介と、今後の活動について」でした。

12/5に岡山県立大学にて行われた、合同勉強会 in 大都会岡山 -2015 Winter-にて、Crystalの紹介のセッションを行いました。

スライドはこちら。

主に伝えたかった内容としては

  • CrystalはRubyistには簡単に使えます
  • Crystalは型があります
  • コンパイルして高速に実行できます

といった3点を主題としました。

時間配分がいい感じだったので、実際にRubyでの実行速度と、Crystalでの実行速度の違いをデモすることができたのは、良かったのではないでしょうか。

途中うっかり「Rubyには型がないので云々」といったおかげで、現場の型警察のみなさんに

となったのは迂闊でしたw

岡山の大きな勉強会で使う懇親会場に「座・スタジアム」というちょっと珍しい会場があります。

screen

ここでLTしたいがために、東京、大阪からやってくる人もいるくらい、懇親会に最適な会場です。次はオープンセミナー岡山2016という勉強会がありますので、ぜひ県外の皆様にもお越しいただきたいですね。

CrystalをHerokuで動かしてみた

最近ちょっと話題のcrystal。これをHerokuで動かしてみました。

Herokuの準備

まずはHerokuにアプリケーションを作成します。

Herokuでは当然crystalをサポートしていませんので、crystalのコンパイラを自前でインストールする必要があります。 Herokuにはこういったことを実現するために、buildpackという仕組みが用意されています。

crystal用のbuildpackは既にあるので、今回はこれを利用します。

heroku createする際に上記のbuildpackを指定しておきます。

1
heroku create --buildpack https://github.com/zamith/heroku-buildpack-crystal

ちなみに、これはあとから指定することも可能です。

1
heroku

アプリケーションの準備

次にcrystalで簡単なWebサーバを実装します。といってもcrystalの公式ページにあるサンプルに手を加えた簡単なものです。

app.crというファイル名で以下のファイルを準備します。

app.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require "http/server"
require "option_parser"

server_port = 8080
OptionParser.parse! do |opts|
  opts.on("-p PORT", "--port PORT", "define port to run server") do |port|
    server_port = port.to_i
  end
end

server = HTTP::Server.new("0.0.0.0", server_port) do |request|
  HTTP::Response.ok "text/plain", "Hello world, got #{request.path}!"
end

puts "Listening on http://0.0.0.0:#{server_port}"
server.listen

注意するところは2点。

1点目は、option_parserを使って起動時のオプションでWebサーバのポート番号を指定できるようにしています。デフォルトでは8080ポートで起動します。

Herokuの場合、サーバのポートは$PORTの値を使用しなければいけませんので、起動時にその値を渡せるようにするためです。

2点目は、以下のようにServerのインスタンス生成時に"0.0.0.0"を指定することです。

1
server = HTTP::Server.new("0.0.0.0", server_port) do |request|

こうすることで、localhost以外からでもアクセス可能にしてあります。これを指定していない場合は、Herokuでの起動時に

1
Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch

というエラーが発生します。

できたら早速、起動してみましょう。

1
crystal run app.cr

ブラウザからlocalhost:8080にアクセスし、以下の用に表示されれば、Webサーバが正しく起動しています。

screen

その他のファイルの準備

次にProcfileを以下のように準備します。

Procfile
1
web: ./app -p $PORT

起動時に$PORTをWebサーバのポートとして、指定しています。

次にProjectfileを準備します。中身は空でOKです。

1
touch Projectfile

これは本来は不要なファイルなのですが、crystalのbuildpack内でProjectfileがない場合は、crystalのアプリケーションとして認識してくれないため、空のファイルを作成しています。

Herokuへデプロイ

app.cr、Procfile、Projectfileの3つが準備できたら、Herokuにデプロイしてみましょう。

1
2
3
4
5
git init
git add .
git commit -m 'First commit'
heroku git:remote --app [APPNAME]
git push heroku master

あとは、Herokuにアクセスして、正しく動作していればOKです。

まとめ

今回作成したコードはこちらにおいてありますので、参考にしてください。

https://github.com/zephiransas/crystal-heroku

Unicorn-worker-killerが便利だった件

自分が現在関わっているプロジェクトでは、nginx + unicornの構成で運用しているのですが、この構成でサーバのメモリが足りなくなるという現象に悩まされていました。

unicornのワーカプロセスは、通常では起動したままユーザからのリクエストを処理し、再起動されることはありません。 その関係で、長時間運用していると、そのワーカプロセスがメモリをあるだけ食いつぶすような挙動になります。

こんな時に便利なのが「unicorn-worker-killer」です。

unicorn-worker-killerを使うことで、ワーカプロセスが以下の条件の場合に、自動的に再起動してくれます。

  • ワーカプロセスが指定回数のリクエストを処理した場合
  • ワーカプロセスが指定量のメモリを使用している場合

いずれの場合でもワーカプロセスの再起動は、現在のリクエストを処理した後に再起動(いわゆるgraceful restart)されます。

設定のしかた

設定はconfig.ruにて行います。

リクエストの回数基準で再起動する

config.ru
1
use Unicorn::WorkerKiller::MaxRequests, 3072, 4096

これはワーカプロセスが、3072回~4096回のいずれかの回数リクエストを処理したら再起動する設定です。

config.ru
1
use Unicorn::WorkerKiller::MaxRequests, 3072, 4096, true

とすることで、unicorn.rbのstderr_pathで指定されたパスに状況を出力することができます。

メモリの使用量を基準に再起動する

config.ru
1
use Unicorn::WorkerKiller::Oom, (192*(1024**2)), (256*(1024**2)), 16

これはワーカプロセスが16回リクエストを処理する度に、自身のメモリ使用量をチェックし、これが192M~256Mのいずれかの使用量をオーバーしていた場合に、再起動する設定です。

設定が2つある理由

リクエスト回数とメモリ使用量の設定両方とも、しきい値を範囲で指定するようになっていますが、これには理由があります。

1つのしきい値だと、各ワーカが再起動するタイミングが、ほぼ同じになるからです。同じタイミングで全てのワーカプロセスが再起動してしまうと、その間リクエストを処理することができなくなってしまうので、これは好ましくありません。

ですので、しきい値を範囲で指定し、その範囲内のいずれかの値を実際のしきい値として採用するという仕組みになっています。

なので、しきい値の範囲は狭いより、広いほうが、ベターです。

unicorn-worker-killerを試してみる

では、unicorn-worker-killerがちゃんとワーカプロセスをKillできているかを確認してみます。

シナリオとしては

  • config/unicorn.rbのworker_processesは1として、ワーカプロセスは1つだけにする
  • unicorn-worker-killerの設定は100回〜120回のリクエストを受けたタイミングで、ワーカプロセスを再起動するようにする

まずはGemfileに

Gemfile
1
gem 'unicorn-worker-killer'

と設定します。config.ruの設定は、以下のようになります。

config.ru
1
use Unicorn::WorkerKiller::MaxRequests, 100, 120, true

unicorn-worker-killerの詳細なログを出力するように設定しておきます。

この設定でunicornを起動します。すると以下ような感じでログが出力されます。

config.ru
1
2
3
4
5
I, [2015-07-29T16:38:31.589102 #29745]  INFO -- : worker=0 spawning...
I, [2015-07-29T16:38:31.591242 #29745]  INFO -- : master process ready
I, [2015-07-29T16:38:31.593001 #29752]  INFO -- : worker=0 spawned pid=29752
I, [2015-07-29T16:38:31.593581 #29752]  INFO -- : Refreshing Gem list
I, [2015-07-29T16:38:47.035570 #29752]  INFO -- : worker=0 ready

pid=29752でワーカプロセスが1つ立ち上がりました。psで確認すると

config.ru
1
zephiransas 29752 30.0  2.8 544708 235984 ?       Sl   16:38   0:15 unicorn worker[0] -c config/unicorn.rb -E production -D

のような感じです。ここでブラウザから何度かアクセスすると、unicornのログに以下のように出力されます。

config.ru
1
2
3
4
5
I, [2015-07-29T16:40:37.156111 #29752]  INFO -- : #<Unicorn::HttpServer:0x0000000343fa00>: worker (pid: 29752) has 119 left before being killed
I, [2015-07-29T16:40:37.349161 #29752]  INFO -- : #<Unicorn::HttpServer:0x0000000343fa00>: worker (pid: 29752) has 118 left before being killed
I, [2015-07-29T16:40:37.559274 #29752]  INFO -- : #<Unicorn::HttpServer:0x0000000343fa00>: worker (pid: 29752) has 117 left before being killed
I, [2015-07-29T16:40:37.649334 #29752]  INFO -- : #<Unicorn::HttpServer:0x0000000343fa00>: worker (pid: 29752) has 116 left before being killed
I, [2015-07-29T16:40:47.690545 #29752]  INFO -- : #<Unicorn::HttpServer:0x0000000343fa00>: worker (pid: 29752) has 115 left before being killed

pid=29752のワーカプロセスがあと何回リクエストを処理できるかが分かります。またリクエストするたびに1つづつ減っています。

では、引き続きブラウザからアクセスし、ワーカプロセスのメモリ使用状況を確認してみましょう。

config.ru
1
zephiransas 29752  7.1  3.2 785940 269420 ?       Sl   16:38   0:26 unicorn worker[0] -c config/unicorn.rb -E production -D

メモリ使用量が少し増えているのがわかります。次にリクエストの残り回数を使い切り、ワーカプロセスが正しく再起動されるか確認します。

ブラウザからリクエストを投げ続けると、unicornのログに以下のように出力されます。

config.ru
1
2
3
4
5
6
7
8
W, [2015-07-29T16:47:00.539055 #29752]  WARN -- : #<Unicorn::HttpServer:0x0000000343fa00>: worker (pid: 29752) exceeds max number of requests (limit: 119)
W, [2015-07-29T16:47:00.539621 #29752]  WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 29752) alive: 383 sec (trial 1)
I, [2015-07-29T16:47:03.467363 #29745]  INFO -- : reaped #<Process::Status: pid 29752 exit 0> worker=0
I, [2015-07-29T16:47:03.467928 #29745]  INFO -- : worker=0 spawning...
I, [2015-07-29T16:47:03.472507 #7549]  INFO -- : worker=0 spawned pid=7549
I, [2015-07-29T16:47:03.473377 #7549]  INFO -- : Refreshing Gem list
I, [2015-07-29T16:47:19.137831 #7549]  INFO -- : worker=0 ready
I, [2015-07-29T16:47:20.251309 #7549]  INFO -- : #<Unicorn::HttpServer:0x0000000343fa00>: worker (pid: 7549) has 101 left before being killed

1行目でpid=29752がリクエスト回数上限の119回に達したことがわかります。

2行目ワーカプロセスに対してQUITシグナルを送信しています。

その後、別のワーカプロセスがpid=7549で起動しています。試しにpid=7549のメモリ使用量を見ると

config.ru
1
zephiransas  7549  5.7  3.1 705752 261532 ?       Sl   16:47   0:18 unicorn worker[0] -c config/unicorn.rb -E production -D

となり、以前より減っていることがわかります。

Railsアプリを実運用するときには必須のgemだと思います。

論理削除とeager_loadでN+1問題が発生する件

Railsアプリにて論理削除とeager_loadを合わせて使うとN+1問題が発生することに気づいたのでメモ。

N+1問題を確認する

まずはN+1問題が起きるようなモデルを作成します。よくあるブログアプリのような、ブログのエントリがあり、それにコメントが複数あるパターンです。

1
2
3
4
5
6
7
8
9
10
11
12
class Post < ActiveRecord::Base
  attr_accessible :title,
                  :content
  has_many :comments
end

class Comment < ActiveRecord::Base
  attr_accessible :post_id,
                  :name,
                  :content
  belongs_to :post
end

適当なデータを入れた後、これに対してrails cで以下のようにレコードを取得します。

1
2
3
Post.all.each do |post|
  puts post.comments.first.name
end

すると、以下のようなSQLが発行されます。

1
2
3
4
5
6
7
8
9
Post Load (0.1ms)  SELECT "posts".* FROM "posts"
Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 12 LIMIT 1
ユーザ1
Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 13 LIMIT 1
ユーザ1
Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 14 LIMIT 1
ユーザ1
...
(以下続く

この場合は、対象となったPostの件数分、CommentsテーブルへのSQLが発行されることになります。 これがN+1問題です。

N+1問題に対処する

これを解決するには、eager_loadを使うことが一般的です。つまり

1
2
3
Post.eager_load(:comments).each do |post|
  puts post.comments.first.name
end

この場合のSQLは(一部簡略化しています)

1
2
SQL (0.2ms)  SELECT "posts".*, "comments".*
FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"

となります。Postsテーブルと一緒にCommentsテーブルを取得しているので、SQLが1回だけ発行されていることがわかります。

美しい理想の世界です。ハラショー

paranoiaを導入する

さて本題。ここでうっかり論理削除を導入してみましょう。

Railsには論理削除に関するgemは多数ありますが、現在のデファクトスタンダードはparanoiaだと思います。まずはparanoiaをGemfileに記述します。

Gemfile
1
gem 'paranoia', '~> 1.0'  # Rails3系には1.0系を使用

その後、モデルを以下のように変更します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Post < ActiveRecord::Base
  acts_as_paranoid
  attr_accessible :title,
                  :content
  has_many :comments
end

class Comment < ActiveRecord::Base
  acts_as_paranoid
  attr_accessible :post_id,
                  :name,
                  :content
  belongs_to :post
end

その後、eager_loadしてみます。

1
2
3
Post.eager_load(:comments).each do |post|
  puts post.comments.first.name
end

するとSQLは以下の様に発行されます。(一部簡略化しています)

1
2
3
SQL (0.2ms)  SELECT "posts".*, "comments".*
FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"
WHERE ("posts".deleted_at IS NULL)

PostsテーブルのWHERE条件にdeleted_at is nullが付与されているのは期待通りですが、Commentsテーブルには付与されていないので、これでは論理削除されたCommentsテーブルの内容も取得してしまいます・・・

では、以下のようにPostのcommentsにconditionsを付与するのはどうでしょう?

1
2
3
4
5
6
class Post < ActiveRecord::Base
  acts_as_paranoid
  attr_accessible :title,
                  :content
  has_many :comments, conditions: 'comments.deleted_at is null'
end

ここで同様にeager_loadするとSQLは以下の様に発行されます。

1
2
3
SQL (0.2ms)  SELECT "posts".*, "comments".*
FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" AND comments.deleted_at is null
WHERE ("posts".deleted_at IS NULL)

WHERE条件が追加されて、なんだか、いい感じにeager_loadできました。

論理削除したデータも取得したい場合

さて、ここで少々頭がおかしくなって「削除したCommentも取りたい(^q^)」という気分になったとしましょう。

そこでPostクラスにcomments_with_deletedなるアソシエーションを追加します。

1
2
3
4
5
6
7
8
9
class Post < ActiveRecord::Base
  acts_as_paranoid
  attr_accessible :title,
                  :content
  has_many :comments, conditions: 'comments.deleted_at is null'
  has_many :comments_with_deleted,
           class_name: 'Comment',
           foreign_key: :post_id
end

さて、これを使ってeager_loadしてみましょう。

1
2
3
Post.eager_load(:comments_with_deleted).each do |post|
  puts post.comments.first.name
end

すると、以下のようなSQLが発行されます。

1
2
3
4
5
6
7
8
9
10
SQL (0.2ms)  SELECT "posts".*, "comments".* FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."id" = "posts"."id" WHERE ("posts".deleted_at IS NULL)
Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 12 AND ("comments".deleted_at IS NULL) AND (comments.deleted_at is null) LIMIT 1
ユーザ2
Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 13 AND ("comments".deleted_at IS NULL) AND (comments.deleted_at is null) LIMIT 1
ユーザ1
Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 14 AND ("comments".deleted_at IS NULL) AND (comments.deleted_at is null) LIMIT 1
ユーザ1
Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 15 AND ("comments".deleted_at IS NULL) AND (comments.deleted_at is null) LIMIT 1
ユーザ1
Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 16 AND ("comments".deleted_at IS NULL) AND (comments.deleted_at is null) LIMIT 1

最初のSQLでは、条件にcomments.deleted_at is nullが付与されていないので、これは期待通りなのですが、その後、なぜかN+1問題が再発しています。

現在のところ、これを回避できる方法は見つけられていません。

  • 追記

我らのひむひむセンセイから、アドバイスを頂きました。

なるほど!やってみましょう。

1
2
3
Post.eager_load(:comments_with_deleted).each do |post|
  puts post.comments_with_deleted.first.name
end

するとSQLは

1
2
3
4
SELECT "posts".
SQL (0.2ms)  SELECT "posts".*, "comments".* FROM "posts" LEFT OUTER JOIN
ON "comments"."post_id" = "posts"."id"
WHERE ("posts".deleted_at IS NULL)

となって、意図した結果になりましたとさ。

でもこれ、実装時に意識しながら書ける自信ないですわ・・・(´・3・`)

結論

結論を社畜ちゃんにまとめていただきます。

summary

とは言っても論理武装が必要でしょうから、こちらも合わせてどうぞ。

JavaプログラマがKindle50%還元セールで買っておくべきIT技術書

Kindle Storeでセールをやってるようです。今回は50%をポイント還元するってセールらしいです。

で、こちらを見てたらRubyやPHPはあっても、Javaがなかったので、ついカッとなってJavaプログラマ向けのオススメ技術書をチョイスしました。 セールは6/1の正午までですので、お早めにどーぞ。

これは店頭で目次を見た程度です。わりと初心者向けな印象なので、これからJavaを勉強したい人にオススメです。

いわずと知れた「パーフェクトシリーズ」のJava版。広範囲に網羅されているので、手元に置いておけば長く使えると思います。初心者でも可。

これも「逆引きレシピシリーズ」のJava版。リファレンス的に使うのならコイツは鉄板。

そもそもJava EEの日本語の本は少ないのですが、最新のJava EE7に対応した本。

1つ前のバージョンであるJava EE6の本。通称「金魚本」。Java EE6と多少古くはあるがJava EE7と全然違うというわけでもないので、未だに現役で使えるはず。内容はある程度Java EE6の仕組みを理解している人がリファレンス的に使う感じだと思います。

現場からは以上です。

Project Kullaを試す

以前から気になっていたJavaのREPL、Project Kullaを動かしてみました。

REPLとはRead-eval-print loopの略で、CUIからコードを直接入力していって、その場で動作を確認できるツールです。 Rubyであればirbやpryなどが有名ですね。

Project KullaはOpenJDKにて開発されている、JavaのREPL環境をつくるプロジェクトです。 ちなみにこの機能はJDK9で、正式導入される予定になっています。

JLine2のインストール

早速REPL環境を動かしてみたいところですが、まずは前準備として、Kullaに必要なJLine2というライブラリをビルドします。

ソースコードはGitHubのリポジトリでホストされていますので

1
2
3
git clone git@github.com:jline/jline2.git
cd jline2
mvn install

ちなみにJLine2はJDK8以前でないとビルドできないので注意です。

ビルドに成功するとjline2/targetディレクトリにjline-2.13-SNAPSHOT.jarが作成されます。Kullaからは、このjarを利用します。

JDK9 EAのインストール

KullaのビルドにはJDK9が必要です。こちらからJDK9をダウンロードし、インストールします。 自分がインストールしたのは、以下のバージョン。

1
2
3
java version "1.9.0-ea"
Java(TM) SE Runtime Environment (build 1.9.0-ea-b59)
Java HotSpot(TM) 64-Bit Server VM (build 1.9.0-ea-b59, mixed mode)

その後、使用するJAVA_HOMEをJDK9に設定します。

普段、自分はJAVA_HOMEの設定にjava_homeコマンドを使用しているので、.bash_profileに

1
export JAVA_HOME=`/usr/libexec/java_home -v 1.8`

としてJDK8を使用しています。今回はJDK9を使いたいので、これを

1
export JAVA_HOME=`/usr/libexec/java_home -v 1.9`

とし

1
source ~/.bash_profile

として、JDK9を有効にします。

Kullaのビルド

いよいよKullaのソースをダウンロードしてビルドします。

1
2
hg clone http://hg.openjdk.java.net/kulla/dev ~/kulla
cd ~/kulla

次に、その他必要なソース類を取得します。

1
2
chmod 755 get_source.sh
./get_source.sh

しばらく待つと、終了します。次にビルドスクリプトを環境に合わせて修正します。

1
cd langtools/repl

scripts/compileを以下のように修正します。

scripts/compile
1
2
3
4
5
6
#!/bin/sh
JLINE2LIB=/Users/[ユーザ名]/jline2/target/jline-2.13-SNAPSHOT.jar
JAVAC_BIN_HOME=/Library/Java/JavaVirtualMachines/jdk1.9.0.jdk/Contents/Home/bin

mkdir -p build
$JAVAC_BIN_HOME/javac -Xlint:unchecked -Xdiags:verbose -cp ${JLINE2LIB} -d build src/*/*.java

1行目 - OSXの環境に合わせて"#!/bin/sh"に修正 2行目 - jline2のjarを指定 3行目 - JDK9のjavacのあるディレクトリを指定 6行目 - 先頭に"$JAVAC_BIN_HOME"を追加

修正できたら

1
scripts/compile

でビルドしましょう。なにもエラーがでなければ、成功しています。

REPLを実行する

実行前にscripts/runを以下のように修正します。

scripts/run
1
2
3
4
#!/bin/sh
JLINE2LIB=/Users/[ユーザ名]/jline2/target/jline-2.13-SNAPSHOT.jar
JAVA_BIN_HOME=/Library/Java/JavaVirtualMachines/jdk1.9.0.jdk/Contents/Home/bin/
$JAVA_BIN_HOME/java -ea -esa -cp build:${JLINE2LIB} tool.Repl "$@"

先になおしたスクリプトとほぼ同じです。

修正できたら、早速実行してみましょう。

1
scripts/run

すると、以下のようにプロンプトが表示されます。

1
2
3
4
|  Welcome to the Java REPL -- Version 0.411
|  Type /help for help

->

あとは普通にJavaのプログラムが書けます!

1
-> System.out.println("Hello!");

また、CUIなどと同じようにタブによる補完もできます。

Shift + Tab補完もなかなかステキ。

当然ですがクラスを定義することもできます。

1
2
3
4
5
6
7
8
-> class Hoge {
>> public static String fuga(){ return "FUGA!!"; }
>> }
|  Added class Hoge

-> Hoge.fuga();
|  Expression value is: "FUGA!!"
|    assigned to temporary variable $1 of type String

面白いのは、いきなりメソッド定義もできます!

1
2
3
4
5
6
-> int add(int x, int y){ return x + y;}
|  Added method add

-> add(2,3);
|  Expression value is: 5
|    assigned to temporary variable $2 of type int

普通に使えそうですね!

またTab補完周りの機能については、我らの@bitter_foxくんが実装に関わってるらしいので、補完機能が怪しかったら、ぜひレポートしてあげてください。

参考にしたリンク