Scalatraで簡単なwebアプリを作ってみる①

こんにちは。JWordのO.Yです。
今回はScalaで開発された軽量webアプリケーションフレームワークであるScalatraを使って簡単なwebアプリを作ってみようと思います。
Scalatraを知らない方にさらっと概要だけ説明させていただきますとRubyのSinatraというwebアプリケーションフレームワークに似せて作られたのがこのScalatraです。
詳しくはこちらのscalatra公式サイトを見てください。

Scalatraをなんで取り上げるのかというとJWordで開発された広告系のシステムの配信プラットフォームとしてこのScalatraを使用した経験があるためです。
すでに1年くらい前の話ですが知識の整理を兼ねて今回紹介させていただくことにしました。

Scalaで有名なwebアプリケーションフレームワークというとPlayだと思います。最初はJWordでもPlayを採用しようかという話をしていたのですが、同じ業界にいる知人にScalatraのことを教えられ自分で調べたところPlayより軽量でパフォーマンスに優れてそうであること、単独で動作するPlayと違いScalatraはコアにServlet Apiを使用しておりTomcat上で動作するのでTomcatのノウハウが利用できることがわかりScalatraを採用することにした経緯があります。
(当時は気づきませんでしたが改めてScalatraの公式サイトを覗いてみるとScalatraもTomcat等のサーブレットコンテナ上ではなく単独で動作させることができるようです)

Scalaをなんで使うの?って声が聞こえてきそうですがそれはその当時くらいからScalaを採用する競合さんが増えてきていてJWordでも使ってみたいなーと思ったのが理由です。

さて、以下よりさっそく実際にscalatraで簡単なユーザー管理システムを作ってみましょう。
このシステムはユーザー情報の登録・更新(上書き)・削除・登録済みユーザー情報の一覧表示の機能を持つとします。

1.開発・実行環境
アプリ開発・実行環境は以下の通りです。
(主要なもの抜粋)

【サーバー】
Conoha 6core 8G x 2

【OS】
CentOS6.5

【HTTPサーバー】
nginx 1.0.15

【サーブレットコンテナ】
Tomcat 7.0.68

【Java】
OpenJDK 1.8.0_51-b16

【Scala】
2.10.5

【Scalatra】
2.3.0

【MySQL】
5.1.73

2.割愛箇所
誌面と時間の都合上から以下に関しては記述を割愛させていただきます。

OpenJDK8のダウンロード・インストール
MySQL5.6のダウンロード・インストール・設定
Tomcat7.0.68のダウンロード・インストール・設定

3.sbtのダウンロード・インストール・設定
scalaアプリケーションを開発するための準備を整えます。
scalaアプリケーションの開発に当たり、sbtを使用します。

sbt公式サイトから最新版である0.13を適当な場所にダウンロードし、解凍します。

/usr/local/src/にwebappディレクトリを作成しそこに上記で解凍したファイル群をコピーします。

/usr/local/src/webapp/binに移動し、sbtを起動します。この際に必要なライブラリをsbtが自動的にダウンロードしてくれます。

/usr/local/src/webapp/binにprojectディレクトリ、src/main/scalaディレクトリ、src/main/resourcesディレクトリを作成します。

projectディレクトの中にplugins.sbtを以下の内容で作成します。
使用するsbtプラグインを指定します。

格プラグインの提供する機能は以下の通りです。(分かる範囲で)

①scalateテンプレートのプリコンパイル(scalate形式をscala形式に変換)と他のscalaソースとそれを一緒にコンパイルさせる。
②③組み込み型サーブレットコンテナの制御、scalatraアプリケーションをwarファイルとしてパッケージングする際にscalaアプリケーションのクラスファイル以外で必要なファイル(静的リソース、テンプレート等)も一緒にパッケージングする。

projectディレクトリの中にbuild.scalaを以下の内容で作成します。

libraryDependenciesで指定したサードパーティのライブラリはコンパイル時に自動的にダウンロードされます。
またlibraryDependenciesに指定しなくてもサードパーティライブラリのjarをダウンロードしてきてsbtのbinディレクトリ配下にlibというディレクトリを作成し、そこに放り込んでおいてsbtに自動認識させるという手もあります。

scalatra公式サイトではgiter8を使用してscalatraと一部のプラグインのインストールをしてますが、この方法は環境によってはうまくいかない場合があるため今回は使用しません。但し、giter8経由でのインストールでも後出のscalaソースと同じようなものが自動的に生成されます。実際後出のscalaソースの内、build.scala、ScalatraBootstrap.scala、ScalatratestStack.scalaはgiter8経由でインストールした際に自動的に生成されたものを流用しています(そのため該当ソースには英語コメントがくっついてる場合があります。ご了承ください)。

4.nginxインストール・設定
インストールはyumでちゃちゃっと済ませてください。
標準のリポジトリからインストールできると思います。
インストールが済んだら次は/etc/nginx/conf.d/にscalatratest.confを以下の内容で作成します。

その他の設定はデフォルトでも構いませんがお好みで自由にいじってください。
ちなみに今回開発するscalatraアプリケーションのサイトドメインはscalatratest.jword.jpとしてます。
ここは適宜別のものを用意するなり、Windows、Macのhostsファイルを編集してscalatratest.jword.jpとwebサーバーのグローバルIPを紐付けします。
nginxを使用する場合、UNIXドメインソケットでscalatraアプリケーションに接続することはできません。

nginxを起動します。

5.scalatraアプリケーションの開発
前述したユーザー管理システムをscalaを使って開発していきましょう。
まずはscalatraアプリの初期処理部分です。
scalatraではScalatraBootstrapの名前のクラスを定義し、その中に初期処理とURIとアプリケーションの紐付けを記述することになっています。
ScalatraBootstrap.scalaを以下の内容で作成し、/usr/local/src/webapp/bin/src/main/scalaに保存します。

次にScalatraServletトレイトとScalateSupportトレイトをミックスインしたScalatratestStack.scalaを作成します。
これはgiter8でインストールした場合には自動生成されるソースの一つです。
ScalatraServletをミックスインすることによりScalatraのwebアプリケーションフレームワークとしての機能を使用できるようになります。(後出のbefore,afterフィルター、get,postハンドラ等)
ScalateSupportトレイトはscalateテンプレート関連の機能(テンプレート呼び出し、テンプレートへの変数アサイン、テンプレート表示)を提供します。

それでは以下の内容でScalatratestStack.scalaを作成し、/usr/local/src/webapp/bin/src/main/scala/scalatratestに保存します。
ScalatraBootstrap.scala以外はパッケージ名であるscalatratestディレクトリの配下に保存します。

上記部分はテンプレートの保存場所を変更したい時以外には特にいじる必要もないと思います。

次にslickをORMとして使用するためにモデル部分のscalaソースを作成しますが、これはslickに元から付属している自動生成機能を使用します。しかしこの自動生成機能を使用するとしてもそれ用のバッチを開発する必要があります。バッチと言ってもとても小さいものです。table_gen.scalaを以下の内容で作成します。

ここまで来たらsbtを起動し、sbtのコマンドライン上でcompileコマンドを実行します。(多分通るはずです。)
compileコマンドが正常終了したら次はconsoleコマンドを実行します。そうするとscalaのコマンドラインツールが起動します。

scalaコマンドラインツールが起動したら以下のようにコマンドを入力します。

2行目はscalaコマンドラインツールが出力したメッセージですので入力する必要はありません。上記が完了したらsbtを終了し/usr/local/src/webapp/bin/src/main/scala/scalatratestに移動するとTables.scalaが作成されていると思います。これがslickのORMで使用するモデル部分のscalaソースとなります。

これで下準備が整いました。それでは以下からアプリケーションのコアロジック(コントローラー・モデル・ビュー)部分を作っていきます。まずコントローラー部分です。
以下の内容でScalatratestApp.scalaを作成し、/usr/local/src/webapp/bin/src/main/scala/scalatetestに保存します。

この箇所はアプリケーションのコアに当たる部分なのですこし詳しく説明します。
まず

ですが、before,afterハンドラは各リクエストハンドラ内の処理が実行される前後の処理を定義することができるpreフィルター、postフィルターです。
今回特に実行させたい処理がなかったので、後述する組み込み型Tomcatによるテスト時にどのタイミングで実行されるかがわかるようにprintln文だけを入れてあります。
request.getPathInfo()でアクセスの合ったURIを表示しています。request変数の実体はorg.apache.catalina.connector.RequestFacade(サーブレットコンテナがTomcat以外の場合には、使用するサーブレットコンテナ固有のjavax.servlet.http.HttpServletRequestをインプリメントしたクラスインスタンスになります)インスタンスとなっておりjavax.servlet.http.HttpServletRequestインターフェイスで定義されているメソッドを呼び出すことが可能です。

notFoundハンドラには読んで字のごとく404 Not Found時の処理を定義します。

次に削除処理部分です。

削除処理はpostリクエストで処理するのでpostリクエストハンドラを使い、引数として削除処理用のURIを指定します。
処理内容は削除したいユーザー情報を指すユーザー情報IDが設定されているリクエストパラメータであるdelete_user_idの値を取得し、ScalatratestModelに定義されているユーザー情報削除処理内で実際の削除処理を行い、その後、メインページにリダイレクトするというものになっています。
最初のprintlnはどのリクエストハンドラを実行したかを確認するためだけに入れてるので特別な意味はありません。後述の他のリクエストハンドラにも同じような処理が入っています。
ここでparamsという変数が出てきますがソースを見ればわかるようにリクエストパラメータを保持しています。実体はscala.collection.imutable.Mapとなっているので上記の方法以外にもparams(“[リクエストパラメータ名]”)でも値を取得することができます。
ScalatratestModelは登録・削除・更新・一覧取得処理が定義されたこのアプリケーションのモデル部分となります。こちらについては後述します。
flashはFlashMapSupportトレイトをミックスインすることによって使用可能となる内容保存後の1リクエストのみ内容を保持可能なフラッシュデータです。ここでは削除処理完了メッセージをリダイレクト先のメイン画面に表示するために使用しています。
redirectは見ての通り他のページへリダイレクトする機能を提供します。

次はユーザー情報登録処理部分です。

上から順に見ていきます。

上記はリクエストパラメータを取得している部分です。

上記はリクエストパラメータのバリデーションを行い、すべてのリクエストパラメータが正常値であればユーザー情報登録処理を実行後にメイン画面にリダイレクト、そうでなければメイン画面に戻るという処理です。
ここでリクエストパラメータのバリデーションを行うためにValidatorインスタンスのメソッドを使用しています。これは自作のものなのでScalatraには含まれません。
Validator.scalaを以下の内容で作成し/usr/local/src/webapp/bin/src/main/scala/scalatetestに保存します。

処理自体はとても単純で各リクエストパラメータ毎にデータ長と文字種をチェックするという処理になっています。

次にメインページ部分です。

上記は登録済みのユーザー情報全てを取得する処理です。今回はページング等は一切考慮していないので仮に100万件あれば100万件取得します。
取得処理の詳細はScalatratestModel.scalaを作成するときに説明します。

上記部分はコンテンツタイプの設定とテンプレート呼び出しの処理をしている部分です。
レイアウトテンプレートを使用する場合にはlayoutにレイアウトテンプレートの相対パス(WEB-INFからのパス)を設定するとそのレイアウトテンプレートが読み込まれます。今回はレイアウトテンプレートは使用しませんので””となっています。
/indexは呼び出し対象とするテンプレートから拡張子を除去したものになります。これはWEB-INF/templates/views/からの相対パスで指定しますが拡張子は不要です。

次にメインページのテンプレートであるindex.sspを作成します。以下の内容で/usr/local/src/webapp/bin/src/main/webapp/WEB-INF/templates/viewsに保存します。

テンプレートはscalateフォーマットに従って記述します。scalateについてはこちらの公式サイトを見てください。上の方から少し説明しようと思います。

ScalatratestAppのget(“/”)内でテンプレートの呼び出し処理があったと思いますが、その時にアサインした変数をテンプレート側の変数に代入するための定義です。

フラッシュデータにエラーメッセージ、インフォメーションメッセージが設定されていた場合にそれを表示するためのものです。if文の文法はscalaに準拠しています。

ユーザー情報が1件もないときには「データがありません」と表示し、ある時には1件ずつ取り出して表示するようにしています。ちなみにscalaではタプルのある特定のインデックスが指す場所に格納されているデータを参照するときに_1という形でインデックスを指定するようになっています。またインデックスは0からではなく、1からとなります。なのでuser._1という形になっています。
タプルではなくscala.collection.imutable.Listを使用した場合にはuser(0)という形になりますが型変換の手間を省くため今回はタプルを使用しています。
for文が出てきますがfor文の文法もscalaに準拠しています。

最後にアプリケーションモデルであるScalatratestModel.scalaを作成します。以下の内容で/usr/local/src/webapp/bin/src/main/scala/scalatratest/に保存します。

上の方から説明していきます。

上記はStringクラスにメソッドtoIntとtoShortを追加するという定義になります。
implicitを付けてクラスを定義するとコンストラクタの引数のデータ型のクラスにimplicit classの中のメソッドが追加されます。またAnyValを継承するとStringToInt、StringToShortがインスタンス化されず、メモリ割り当てがなされません。詳しくはこちらの値クラスと汎用トレイトを見てください。

上記はORMを使用して条件に合致したユーザー情報の取得、件数のカウントをしています。filterの中はwhere句に該当します。filterの中に上記のようにSQLライクに抽出条件を記述することができます。詳しくはslick公式サイトのCOMING FROM ORM TO SLICK¶を見てください。
this.db.withDynSessionは自動的にDBのセッションの開始と終了をしてくれるものです。基本的にSELECT系のオペレーションをする場合にはwithDynSessionを使用しておけばいいです。こちらに関して詳しくはCONNECTIONS / TRANSACTIONS¶を見てください。

上記は入力されたユーザー情報IDと紐付くユーザー情報が既にDBに存在する場合には上書き更新、そうでなければ入力されたユーザー情報をDBに新規登録するという処理です。mapの中には更新対象または登録したいカラムを指定します。
this.db.withDynTransactionはオートコミットのオフ、トランザクションの開始と終了、例外が発生した場合のロールバックを自動的に行ってくれます。詳しくはCONNECTIONS / TRANSACTIONS¶を見てください。

上記はすべてのユーザー情報の取得と指定されたユーザー情報IDと紐付くユーザー情報の削除を行うロジックとなります。

ながーくなってしまいましたが(すいません・・・)これですべてのソースコードがようやく揃いました。
scalatraアプリケーションの開発はこれで完了です。

6.c3p0.propertiesの作成
前項でscalatraアプリケーションを開発し終えたのでコンパイルしたいところですが、その前にc3p0(コネクションプール生成用のライブラリ)の設定ファイルを用意する必要があります。
c3p0.propertiesを以下の内容で/usr/local/src/webapp/bin/src/main/resources/に保存します。

c3p0の設定の詳細についてはc3p0 – JDBC3 Connection and Statement Poolingを見てください。

7.コンパイル・テスト
さて、それでは5で開発したscalatraアプリケーションをコンパイルします。sbtを起動し、compileコマンドを実行します。何もエラーがない場合には以下のような表示になると思います。

の部分は未コンパイルの状態でコンパイルされてscalaソース数が表示されますが、私の結果とは異なる可能もあります。
コンパイルが終わったら次に組み込み型Tomcatを起動してテストします。sbtのコマンドラインで以下のようにコマンドを入力します。

これでsbt上で組み込み型Tomcatが起動し、外部からのHTTPリクエストを受け付けるようになります。ユーザー情報登録・上書き更新・削除・一覧表示機能が正常動作していることを確認します。
メイン画面は以下のような感じになります。

scalatratest-app-main
(上記画面キャプチャーではすでに2件のユーザー情報が登録されています)

8.クラスファイルのパッケージングとTomcatの上での起動
テストが終わったらパッケージングしてTomcat(組み込み型でない)上で動かしてみましょう。
まずはパッケージングをします。sbtを起動し、packageと入力してください。そうすると/usr/local/src/webapp/bin/target/scala-2.10にscalatratest_2.10-0.1.0.warが作成されます。

これをTomcatのインストールディレクトリ直下にあるwebappsディレクトリにコピーします。その後ROOT.warにリネームし、元からあるROOTディレクトリを削除します。

最後にTomcatを起動します。

/usr/local/tomcat/logs/catalina.outを見て

と表示されていれば成功です。
7の時と同じように機能確認して正常動作するようなら完了となります。

9.最後に
いかがでしたか?JavaやPHPのwebアプリケーションフレームよりも簡単にwebアプリが作れました。(大規模なwebアプリケーションやパフォーマンスが求められるとなるとその限りではないかもですが)
元よりJavaで開発している方から見ると「Javaで作った方がええ!!」って言うかもしれませんがPHPらーから見ると簡単にそしてPHPのwebアプリケーションフレームワークを使用するより確実にパフォーマンスアップが見込めます。PHP7.0で拡張性も汎用性も考慮せずフルスクラッチで同じwebアプリケーションを開発した場合、ひょっとしたらPHP7.0の方がハイパフォーマンスかもしれませんが、1リクエスト当たりの処理コストが高い場合(様々データから複雑な演算をする必要がある場合等)には確実にscalatraを使用することをお勧めします。但しあまりに1リクエスト当たりの処理コストが高い場合にはscala、というかakkaがサポートしているremote akkaとakka microkernelを組み合わせ負荷分散環境を構築する必要があります。独立したいくつかのロジックに処理を分解できない場合にはあまり効果は見込めませんがそうでないなら大幅なパフォーマンスアップが見込めます。次回はそのremote akkaとakka microkernelをscalatraと組み合わせた高負荷でもオッケーな負荷分散環境の構築とその上で動作するscalatraアプリケーションの開発をしてみたいと思います。