zephiransasのチラシの裏

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

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くんが実装に関わってるらしいので、補完機能が怪しかったら、ぜひレポートしてあげてください。

参考にしたリンク

GitHubのPull requestから、CHANGELOGっぽいものを作成するgemを作った

gemを作りました。名前はoctocamです。

主な機能としては「GitHubから指定された日付期間にマージされたPull requestを抽出し、CHANGELOGっぽいMarkdownを生成する」というgemです。

定期的にリリースを行っている場合に、以前リリースされたときからどのような機能が増えたかをCHANGELOGとかに書き出しますが、そういった時に便利に使えると思います。

似たような機能を持つものはgemやnpmを探すと、結構あります。この辺りとか。ただ、いずれも

  • 日付の指定ができない。できたとしてもPull requestの作成日とか。
  • issueやcommitを含めてしまう。
  • Markdownで出力できない。
  • 認証に対応してない。

などなど、要求を満たすものではなかったので、gemの作り方を勉強がてら作ってみました。

ワークフローとして、 必ずPull requestでレビューをしてから、マージをおこなうワークフロー を採用しているところであれば、フィットするように思います。

インストール

以下のようにしてインストールします。

1
2
gem install octocam
rbenv rehash  # rbenvを使ってる人はrehash

もしプライベートなリポジトリにアクセスしたい場合は、こちらからPersonal access tokensを生成します。 あとは、生成したトークンを.bash_profileあたりから、環境変数「OCTOCAM_GITHUB_TOKEN」に設定しておきます。

1
export OCTOCAM_GITHUB_TOKEN="your-40-digit-github-token"

使い方

インストールされるとoctocamコマンドが使えるようになるので、以下のようにして実行します。

1
octocam -o zephiransas -r octocam -f 2015-01-01 -t 2015-01-31

-f,-tオプションにPull requestがマージされた日付を指定でききます。

カレントディレクトリがgitのローカルリポジトリで、かつ、originがGitHubに設定されている場合であれば-o,-rオプションは省略できます。

欲しい機能ありましたら、issueを立てて頂くか、Pull requestを投げてください。

Lambda-behaveでテストを書こう

これはJavaアドベントカレンダー2014の12/16分の記事です。

昨日はgrimroseさんの、[書評] Gradle徹底入門 でした。

明日は@com4dcさんの、はじめて触るStreamの世界 です。

自分はJavaのテストフレームワークである、lambda-behaveについて紹介します。

自分は普段はRailsでの開発を行っているのですが、現場では主にRSpecを使ってテストを記述しています。RSpecでのテストは以下のような感じです。

1
2
3
4
5
6
7
describe 'Sample' do
  context 'hogeメソッドについて' do
    it 'fugaを返すこと' do
      Sample.hoge.should == "fuga"
    end
  end
end

RSpecでは上記のようにDSLを使って、なにをテストしているかを構造的に記述することができます。 lambda-behaveを使うと、このようなDSLっぽい記述のテストを、Java8のLambdaを使って書くことができるようになります。

最初のテスト

まずはテスト対象となるメソッドを準備します。

1
2
3
4
5
public class Sample {
    public static int includeTax(Integer price) {
        return 0;
    }
}

上記のようなstaticなメソッドを準備します。includeTaxメソッドは引数を一つ取り、その税込み金額を返すメソッドとします。実にギョーミーですね!

今回はTDD的なノリで実装していきますので、ここでは中身の実装はおこないません。

それでは実際のテストを書いて行きましょう。ここでのテストシナリオは

  • includeTaxメソッドに100を渡した場合に、108が返ってくること

をテストするとします。これをlambda-behaveで書くと、以下のようになります。

1
2
3
4
5
6
7
8
9
10
import static com.insightfullogic.lambdabehave.Suite.*;

@RunWith(JunitSuiteRunner.class)
public class SampleSpec {{
    describe("includeTax", it -> {
        it.should("税込み価格が取得できること", expect ->
            expect.that(Sample.includeTax(100)).is(108)
        );
    });
}}

static importを使ってlambda-behaveのメソッドを使えるようにし、こられを使って記述していきます。

JUnitのランナーも用意されていますので、これを@RunWithで指定すれば、IDEからも簡単にテストを実行可能です。早速、テストを実行してみましょう。

screen1

includeTaxは、まだ実装をおこなっていませんので、当然このとおりテストが失敗します。

次に、includeTaxを実装してみます。

1
2
3
4
5
6
public class Sample {
    public static int includeTax(Integer price) {
        final float TAX_RATE = 0.08;
        return (int)Math.floor(price * (1 + TAX_RATE));
    }
}

再度、テストを実行すれば、テストが成功しています。

screen2

複数のテストデータでチェック

先の例では1つの値でしかテストしませんでしたが、lambda-behaveでは同時に複数の値でテストすることもできます。以下のようになります。

1
2
3
4
5
it.uses(100, 108)
  .and(200, 216)
  .toShow("税込み価格が取得できること", (expect, price, includeTax) -> {
     expect.that(Sample.includeTax(price)).is(includeTax);
  });

itのあとにuseとandをチェインして値を準備し、これを使ってtoShowメソッド内でテストを行います。toShowメソッド内ではlambdaの引数で準備した値を利用できますので、lambda内でその値をexpectするようにしています。

生成した値でチェック

lambda-behaveではランダムな値を生成する機能も準備されています。適当な数値を5つほど生成して、そのテストを行うコードは以下のようになります。

(あまり例がよくないですが・・・)

1
2
3
4
5
it.requires(5)
  .example(Generator.integersUpTo(1000))
  .toShow("税込み価格が取得できること", (expect, price) -> {
    expect.that(Sample.includeTax(price)).is((int)Math.floor(price * 1.08));
  });

requiresでランダムに準備する値の数を指定します。

exampleではどのようなテストデータを生成するかを指定します。ここではlambda-behaveで準備されたGenerator.integersUpToを使用しています。この他にも適当な文字列を生成するasciiStringsメソッドなどもあります

例外が発生することをチェック

引数にnullが渡された場合にNullPointerExceptionの発生をチェックすることも、JUnitと同様に可能です。

まず、includeTaxにnullチェックを記述します。

1
2
3
4
5
6
7
8
9
public class Sample {
    public static int includeTax(Integer price) {
        if(price == null) {
            throw new NullPointerException();
        }
        final float TAX_RATE = 0.08f;
        return (int)Math.floor(price * (1 + TAX_RATE));
    }
}

これをテストするlambda-behaveのコードは以下のようになります。

1
2
3
4
5
it.should("nullの場合ぬるぽ", expect -> {
    expect.exception(NullPointerException.class, () -> {
        Sample.includeTax(null);
    });
});

exceptionメソッドの第1引数に、発生する予定の例外を指定し、第2引数には例外が発生する処理をlambdaで記述します。

lambdaに慣れていないと少々書きづらいかもしれないですが、DSLちっくに書けるのはJavaっぽくなくてステキですよね!

こちらからは以上です。