こんにちは。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を適当な場所にダウンロードし、解凍します。
1 2 3 |
cd /var/tmp #<=場所はご自由に wget https://dl.bintray.com/sbt/native-packages/sbt/0.13.11/sbt-0.13.11.tgz tar xfvz sbt-0.13.11.tgz |
/usr/local/src/にwebappディレクトリを作成しそこに上記で解凍したファイル群をコピーします。
1 2 3 |
cd sbt mkdir /usr/local/src/webapp cp -a * /usr/local/src/webapp |
/usr/local/src/webapp/binに移動し、sbtを起動します。この際に必要なライブラリをsbtが自動的にダウンロードしてくれます。
1 2 |
cd /usr/local/src/webapp/bin ./sbt |
/usr/local/src/webapp/binにprojectディレクトリ、src/main/scalaディレクトリ、src/main/resourcesディレクトリを作成します。
1 |
mkdir -p src/main/scala project |
projectディレクトの中にplugins.sbtを以下の内容で作成します。
使用するsbtプラグインを指定します。
1 2 3 |
addSbtPlugin("com.mojolly.scalate" % "xsbt-scalate-generator" % "0.5.0") #① addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5") #② addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "0.9.0") #③ |
格プラグインの提供する機能は以下の通りです。(分かる範囲で)
①scalateテンプレートのプリコンパイル(scalate形式をscala形式に変換)と他のscalaソースとそれを一緒にコンパイルさせる。
②③組み込み型サーブレットコンテナの制御、scalatraアプリケーションをwarファイルとしてパッケージングする際にscalaアプリケーションのクラスファイル以外で必要なファイル(静的リソース、テンプレート等)も一緒にパッケージングする。
projectディレクトリの中にbuild.scalaを以下の内容で作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
import sbt._ //sbtライブラリ(必須) import Keys._ //sbtライブラリ(必須) import org.scalatra.sbt._ //以下プラグインライブラリ import org.scalatra.sbt.PluginKeys._ import com.mojolly.scalate.ScalatePlugin._ import ScalateKeys._ import com.earldouglas.xsbtwebplugin.WebPlugin object ScalatratestBuild extends Build { val Organization = "scalatratest" //パッケージ名 val Name = "Scalatratest" //アプリケーション名 val Version = "0.1.0" //アプリケーションバージョン val ScalaVersion = "2.10.5" val ScalatraVersion = "2.3.0" val TomcatVersion = "7.0.57" lazy val project = Project ( "scalatratest", //プロジェクト名 file("."), //プロジェクトルート settings = ScalatraPlugin.scalatraWithJRebel ++ scalateSettings ++ WebPlugin.webSettings ++ Seq( //独自設定と各プラグインごとの設定をマージ organization := Organization, name := Name, version := Version, scalaVersion := ScalaVersion, resolvers += Classpaths.typesafeReleases,//以下ライブラリの取得先(scalaコンパイラーとコアはここからダウンロード) resolvers += "Spy" at "http://files.couchbase.com/maven2/", resolvers += "bintray/non" at "http://dl.bintray.com/non/maven", libraryDependencies ++= Seq( //使用するサードパーティライブラリ "org.scalatra" %% "scalatra" % ScalatraVersion, //scalatraコアライブラリ "org.scalatra" %% "scalatra-scalate" % ScalatraVersion, //scalatra用scalateライブラリ "org.apache.tomcat.embed" % "tomcat-embed-core" % "7.0.57" % "container",//以下組み込み型Tomcat "org.apache.tomcat.embed" % "tomcat-embed-logging-juli" % "7.0.57" % "container", "org.apache.tomcat.embed" % "tomcat-embed-jasper" % "7.0.57" % "container", "javax.servlet" % "javax.servlet-api" % "3.1.0" % "provided",//サーブレットapi "com.typesafe.slick" %% "slick" % "2.1.0" ,//以下slick(scalaで開発されたORM) "com.typesafe.slick" %% "slick-codegen" % "2.1.0-RC3", "c3p0" % "c3p0" % "0.9.1.2",//データベースコネクションプール生成用ライブラリ "mysql" % "mysql-connector-java" % "5.1.28" ), scalateTemplateConfig in Compile <<= (sourceDirectory in Compile){ base => //scalate関連設定 Seq( TemplateConfig( base / "webapp" / "WEB-INF" / "templates", //テンプレートルート Seq.empty, /* default imports should be added here */ Seq( Binding("context", "_root_.org.scalatra.scalate.ScalatraRenderContext", importMembers = true, isImplicit = true) ), /* add extra bindings here */ Some("templates") ) ) } ) ) } |
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を以下の内容で作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
server { listen 80; server_name scalatratest.jword.jp; error_log /var/log/nginx/scalatratest.error.log; access_log /var/log/nginx/scalatratest.access.log; location / { proxy_pass http://localhost:8080/; //scalatraアプリケーションへのエントリーポイントURLをリバースプロキシとして設定 proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; #クライアントの IP アドレス proxy_set_header X-Forwarded-Host $host; # オリジナルのホスト名。クライアントが Host リクエストヘッダで渡す。 proxy_set_header X-Forwarded-Server $host; # プロキシサーバのホスト名 proxy_set_header X-Real-IP $remote_addr; #クライアントのリモートIP } error_page 404 /404.html; location = /404.html { root /usr/share/nginx/html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } |
その他の設定はデフォルトでも構いませんがお好みで自由にいじってください。
ちなみに今回開発するscalatraアプリケーションのサイトドメインはscalatratest.jword.jpとしてます。
ここは適宜別のものを用意するなり、Windows、Macのhostsファイルを編集してscalatratest.jword.jpとwebサーバーのグローバルIPを紐付けします。
nginxを使用する場合、UNIXドメインソケットでscalatraアプリケーションに接続することはできません。
nginxを起動します。
1 |
service nginx start |
5.scalatraアプリケーションの開発
前述したユーザー管理システムをscalaを使って開発していきましょう。
まずはscalatraアプリの初期処理部分です。
scalatraではScalatraBootstrapの名前のクラスを定義し、その中に初期処理とURIとアプリケーションの紐付けを記述することになっています。
ScalatraBootstrap.scalaを以下の内容で作成し、/usr/local/src/webapp/bin/src/main/scalaに保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import scalatratest._ import org.scalatra._ import javax.servlet.ServletContext import com.mchange.v2.c3p0.ComboPooledDataSource import scala.slick.jdbc.JdbcBackend.Database class ScalatraBootstrap extends LifeCycle { val cpds = new ComboPooledDataSource //コネクションプール作成。後述する/usr/local/src/webapp/bin/src/main/resources/c3p0.propertiesの設定を元にコネクションプールを生成 override def init(context: ServletContext) { val db:Database = Database.forDataSource(cpds) //コネクションプールとslickを紐付け context.mount(ScalatratestApp(db), "/*") //アプリケーションとURIの紐付け } private def closeDbConnection() { cpds.close } override def destroy(context: ServletContext) { super.destroy(context) closeDbConnection } } |
次に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ディレクトリの配下に保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
package scalatratest import org.scalatra._ import scalate.ScalateSupport import org.fusesource.scalate.{ TemplateEngine, Binding } import org.fusesource.scalate.layout.DefaultLayoutStrategy import javax.servlet.http.HttpServletRequest import collection.mutable trait ScalatratestStack extends ScalatraServlet with ScalateSupport { /* wire up the precompiled templates */ override protected def defaultTemplatePath: List[String] = List("/WEB-INF/templates/views") override protected def createTemplateEngine(config: ConfigT) = { val engine = super.createTemplateEngine(config) engine.layoutStrategy = new DefaultLayoutStrategy(engine, TemplateEngine.templateTypes.map("/WEB-INF/templates/layouts/default." + _): _*) engine.packagePrefix = "templates" engine } /* end wiring up the precompiled templates */ override protected def templateAttributes(implicit request: HttpServletRequest): mutable.Map[String, Any] = { super.templateAttributes ++ mutable.Map.empty // Add extra attributes here, they need bindings in the build file } notFound { // remove content type in case it was set through an action contentType = null // Try to render a ScalateTemplate if no route matched findTemplate(requestPath) map { path => contentType = "text/html" layoutTemplate(path) } orElse serveStaticResource() getOrElse resourceNotFound() } } |
上記部分はテンプレートの保存場所を変更したい時以外には特にいじる必要もないと思います。
次にslickをORMとして使用するためにモデル部分のscalaソースを作成しますが、これはslickに元から付属している自動生成機能を使用します。しかしこの自動生成機能を使用するとしてもそれ用のバッチを開発する必要があります。バッチと言ってもとても小さいものです。table_gen.scalaを以下の内容で作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package scalatratest import scala.slick._ object code_gen { def main(args:Array[String]) = { val slickDriver = "scala.slick.driver.MySQLDriver" val jdbcDriver = "com.mysql.jdbc.Driver" val url = "jdbc:mysql://[MySQLサーバーホスト名またはIP]/scalatratest" val outputFolder = "/usr/local/src/webapp/bin/src/main/scala/scalatratest" val pkg = "scalatratest" val user = "[MySQLログインID]" val passwd = "[MySQLログインパスワード]" scala.slick.codegen.SourceCodeGenerator.main( Array(slickDriver, jdbcDriver, url, outputFolder, pkg , user ,passwd) ) } } |
ここまで来たらsbtを起動し、sbtのコマンドライン上でcompileコマンドを実行します。(多分通るはずです。)
compileコマンドが正常終了したら次はconsoleコマンドを実行します。そうするとscalaのコマンドラインツールが起動します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
> console [info] Generating /usr/local/src/webapp/bin/target/scala-2.10/resource_managed/main/rebel.xml. [info] Compiling Templates in Template Directory: /usr/local/src/webapp/bin/src/main/webapp/WEB-INF/templates SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. [info] Compiling 1 Scala source to /usr/local/src/webapp/bin/target/scala-2.10/classes... [warn] there were 1 feature warning(s); re-run with -feature for details [warn] one warning found [info] Starting scala interpreter... [info] Welcome to Scala version 2.10.5 (OpenJDK 64-Bit Server VM, Java 1.8.0_51). Type in expressions to have them evaluated. Type :help for more information. scala> |
scalaコマンドラインツールが起動したら以下のようにコマンドを入力します。
1 2 3 |
scala> import scalatratest.code_gen._ import scalatratest.code_gen._ scala> main(Array("")) |
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に保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
package scalatratest import org.scalatra._ import scala.collection.JavaConversions._ import scala.slick.driver.MySQLDriver.simple._ case class ScalatratestApp(db:Database) extends Scalatratest trait Scalatratest extends ScalatratestStack with FlashMapSupport{ val db:Database //preフィルター before() { println("before:" + request.getPathInfo()) } //postフィルター after() { println("after:" + request.getPathInfo()) } notFound { <div style="color:red">該当ページは存在しません。</div> } //ユーザー情報削除 post("/delete") { println("post-delete") /** リクエストパラメータ抽出 **/ val deleteUserId:String = params.getOrElse("delete_user_id" , "").toString /** モデルインスタンス生成 **/ val model:ScalatratestModel = new ScalatratestModel(db) /** 対象データ削除 **/ model.deleteUser(deleteUserId) /** 削除完了メッセージ設定 **/ flash("info_msg") = "ユーザーID:" + deleteUserId + "のユーザー情報を削除しました" /** indexページにリダイレクト**/ redirect("/") } //ユーザー情報登録 post("/register") { println("post-register") /** リクエストパラメータ抽出**/ val firstName:String = params.getOrElse("first_name", "") val lastName:String = params.getOrElse("last_name" , "") val age:String = params.getOrElse("age" , "") val userId:String = params.getOrElse("user_id" , "") /** モデルインスタンス生成**/ val model:ScalatratestModel = new ScalatratestModel(db) /** パラメータバリデーション **/ if (Validator.isValidFirstName(firstName) == false || Validator.isValidLastName(lastName) == false || Validator.isValidAge(age) == false || Validator.isValidUserId(userId) == false) { /** 入力項目エラーあり。**/ flash("error_msg") = "入力値にエラーがあります" flash += ("first_name" -> firstName) flash += ("last_name" -> lastName) flash += ("age" -> age) flash += ("user_id" -> userId) redirect("/") } else { /** DBへの登録処理 **/ model.upsertUser(params) /** 登録処理完了メッセージ設定 **/ flash("info_msg") = "新しいユーザー情報を登録しました" /** indexページにリダイレクト**/ redirect("/"); } } //メインページ get("/") { println("get-/") /** モデルインスタンス生成 **/ val model:ScalatratestModel = new ScalatratestModel(db) /** 登録済みユーザー情報取得 **/ val uList:List[(Int,String,String,Short)] = model.getUserList() /** コンテンツタイプ設定 **/ contentType = "text/html; charset=utf8" //テンプレート呼び出し ssp("/index" , "layout" -> "" , "firstName" -> flash.getOrElse("first_name" , "") , "lastName" -> flash.getOrElse("last_name" , "") , "age" -> flash.getOrElse("age" , "") , "userId" -> flash.getOrElse("user_id" , ""), "errorMsg" -> flash.getOrElse("error_msg" , ""), "infoMsg" -> flash.getOrElse("info_msg" , ""), "userList" -> uList ) } } |
この箇所はアプリケーションのコアに当たる部分なのですこし詳しく説明します。
まず
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//preフィルター before() { println("before:" + request.getPathInfo()) } //postフィルター after() { println("after:" + request.getPathInfo()) } notFound { <div style="color: red;">該当ページは存在しません。</div> } |
ですが、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時の処理を定義します。
次に削除処理部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//ユーザー情報削除 post("/delete") { println("post-delete") /** リクエストパラメータ抽出 **/ val deleteUserId:String = params.getOrElse("delete_user_id" , "").toString /** モデルインスタンス生成 **/ val model:ScalatratestModel = new ScalatratestModel(db) /** 対象データ削除 **/ model.deleteUser(deleteUserId) /** 削除完了メッセージ設定 **/ flash("info_msg") = "ユーザーID:" + deleteUserId + "のユーザー情報を削除しました" /** indexページにリダイレクト**/ redirect("/") } |
削除処理はpostリクエストで処理するのでpostリクエストハンドラを使い、引数として削除処理用のURIを指定します。
処理内容は削除したいユーザー情報を指すユーザー情報IDが設定されているリクエストパラメータであるdelete_user_idの値を取得し、ScalatratestModelに定義されているユーザー情報削除処理内で実際の削除処理を行い、その後、メインページにリダイレクトするというものになっています。
最初のprintlnはどのリクエストハンドラを実行したかを確認するためだけに入れてるので特別な意味はありません。後述の他のリクエストハンドラにも同じような処理が入っています。
ここでparamsという変数が出てきますがソースを見ればわかるようにリクエストパラメータを保持しています。実体はscala.collection.imutable.Mapとなっているので上記の方法以外にもparams(“[リクエストパラメータ名]”)でも値を取得することができます。
ScalatratestModelは登録・削除・更新・一覧取得処理が定義されたこのアプリケーションのモデル部分となります。こちらについては後述します。
flashはFlashMapSupportトレイトをミックスインすることによって使用可能となる内容保存後の1リクエストのみ内容を保持可能なフラッシュデータです。ここでは削除処理完了メッセージをリダイレクト先のメイン画面に表示するために使用しています。
redirectは見ての通り他のページへリダイレクトする機能を提供します。
次はユーザー情報登録処理部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
//ユーザー情報登録 post("/register") { println("post-register") /** リクエストパラメータ抽出**/ val firstName:String = params.getOrElse("first_name", "") val lastName:String = params.getOrElse("last_name" , "") val age:String = params.getOrElse("age" , "") val userId:String = params.getOrElse("user_id" , "") /** モデルインスタンス生成**/ val model:ScalatratestModel = new ScalatratestModel(db) /** パラメータバリデーション **/ if (Validator.isValidFirstName(firstName) == false || Validator.isValidLastName(lastName) == false || Validator.isValidAge(age) == false || Validator.isValidUserId(userId) == false) { /** 入力項目エラーあり。**/ flash("error_msg") = "入力値にエラーがあります" flash += ("first_name" -> firstName) flash += ("last_name" -> lastName) flash += ("age" -> age) flash += ("user_id" -> userId) redirect("/") } else { /** DBへの登録処理 **/ model.upsertUser(params) /** 登録処理完了メッセージ設定 **/ flash("info_msg") = "新しいユーザー情報を登録しました" /** indexページにリダイレクト**/ redirect("/"); } } |
上から順に見ていきます。
1 2 3 4 |
val firstName:String = params.getOrElse("first_name", "") val lastName:String = params.getOrElse("last_name" , "") val age:String = params.getOrElse("age" , "") val userId:String = params.getOrElse("user_id" , "") |
上記はリクエストパラメータを取得している部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
if (Validator.isValidFirstName(firstName) == false || Validator.isValidLastName(lastName) == false || Validator.isValidAge(age) == false || Validator.isValidUserId(userId) == false) { /** 入力項目エラーあり。**/ flash("error_msg") = "入力値にエラーがあります" flash += ("first_name" -> firstName) flash += ("last_name" -> lastName) flash += ("age" -> age) flash += ("user_id" -> userId) redirect("/") } else { /** DBへの登録処理 **/ model.upsertUser(params) /** 登録処理完了メッセージ設定 **/ flash("info_msg") = "新しいユーザー情報を登録しました" /** indexページにリダイレクト**/ redirect("/"); } |
上記はリクエストパラメータのバリデーションを行い、すべてのリクエストパラメータが正常値であればユーザー情報登録処理を実行後にメイン画面にリダイレクト、そうでなければメイン画面に戻るという処理です。
ここでリクエストパラメータのバリデーションを行うためにValidatorインスタンスのメソッドを使用しています。これは自作のものなのでScalatraには含まれません。
Validator.scalaを以下の内容で作成し/usr/local/src/webapp/bin/src/main/scala/scalatetestに保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package scalatratest object Validator { def isValidFirstName(value:String) = { if (value.length > 0) true else false } def isValidLastName(value:String) = { if (value.length > 0) true else false } def isValidAge(value:String) = { if (value.length > 0 && value.matches("""^[0-9]+$""")) true else false } def isValidUserId(value:String) = { if (value.length > 0 && value.matches("""^[0-9]+$""")) true else false } } |
処理自体はとても単純で各リクエストパラメータ毎にデータ長と文字種をチェックするという処理になっています。
次にメインページ部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
//メインページ get("/") { println("get-/") /** モデルインスタンス生成 **/ val model:ScalatratestModel = new ScalatratestModel(db) /** 登録済みユーザー情報取得 **/ val uList:List[(Int,String,String,Short)] = model.getUserList() /** コンテンツタイプ設定 **/ contentType = "text/html; charset=utf8" /** テンプレート呼び出し、変数アサイン、表示 **/ ssp("/index" , "layout" -> "" , "firstName" -> flash.getOrElse("first_name" , "") , "lastName" -> flash.getOrElse("last_name" , "") , "age" -> flash.getOrElse("age" , "") , "userId" -> flash.getOrElse("user_id" , ""), "errorMsg" -> flash.getOrElse("error_msg" , ""), "infoMsg" -> flash.getOrElse("info_msg" , ""), "userList" -> uList ) } |
1 2 |
/** 登録済みユーザー情報取得 **/ val uList:List[(Int,String,String,Short)] = model.getUserList() |
上記は登録済みのユーザー情報全てを取得する処理です。今回はページング等は一切考慮していないので仮に100万件あれば100万件取得します。
取得処理の詳細はScalatratestModel.scalaを作成するときに説明します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** コンテンツタイプ設定 **/ contentType = "text/html; charset=utf8" /** テンプレート呼び出し、変数アサイン、表示 **/ ssp("/index" , "layout" -> "" , "firstName" -> flash.getOrElse("first_name" , "") , "lastName" -> flash.getOrElse("last_name" , "") , "age" -> flash.getOrElse("age" , "") , "userId" -> flash.getOrElse("user_id" , ""), "errorMsg" -> flash.getOrElse("error_msg" , ""), "infoMsg" -> flash.getOrElse("info_msg" , ""), "userList" -> uList ) |
上記部分はコンテンツタイプの設定とテンプレート呼び出しの処理をしている部分です。
レイアウトテンプレートを使用する場合にはlayoutにレイアウトテンプレートの相対パス(WEB-INFからのパス)を設定するとそのレイアウトテンプレートが読み込まれます。今回はレイアウトテンプレートは使用しませんので””となっています。
/indexは呼び出し対象とするテンプレートから拡張子を除去したものになります。これはWEB-INF/templates/views/からの相対パスで指定しますが拡張子は不要です。
次にメインページのテンプレートであるindex.sspを作成します。以下の内容で/usr/local/src/webapp/bin/src/main/webapp/WEB-INF/templates/viewsに保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
<html lang="ja"> <head> <%@ val firstName:String %> <%@ val lastName:String %> <%@ val age:String %> <%@ val userId:String %> <%@ val errorMsg:String %> <%@ val infoMsg:String %> <%@ val userList:List[(Int,String,String,Short)] %> <title>test</title> <script> function submit_delete(user_id) { document.getElementById("delete_user_id").value = user_id; document.getElementById("delete_form").submit(); } </script> <meta charset="utf8"> <meta http-equiv="content-type" content="text/html" charset="utf8"> </head> <body> <h3>scalatraテストアプリ1</h3> <div> <form action="/register" method="post"> #if (errorMsg != "") <div style="color:red"> <%=errorMsg%> </div> #end #if (infoMsg != "") <div style="color:blue"> <%=infoMsg%> </div> #end <table> <tr> <th>ユーザーID</th> <td><input type="text" name="user_id" value="<%=userId%>" ></td> </tr> <tr> <th>姓</th> <td><input type="text" name="first_name" value="<%=firstName%>"></td> </tr> <tr> <th>名</th> <td><input type="text" name="last_name" value="<%=lastName%>"></td> </tr> <tr> <th>年齢</th> <td><input type="text" name="age" value="<%=age%>"></td> </tr> <tr> <th colspan="2"><button type="submit" >登録</button> </tr> </table> </form> </div> <div> <form action="/delete" id="delete_form" method="post"> <input type="hidden" name="delete_user_id" id="delete_user_id" value=""> </form> </div> <table border="0" cellpadding="2" cellspacing="2"> <tr> <th>ユーザーID</th> <th>姓</th> <th>名</th> <th>年齢</th> <th>操作</th> </tr> #if (userList.size > 0) #for (user <- userList) <tr> <td><%=user._1%></td> <td><%=user._2%></td> <td><%=user._3%></td> <td><%=user._4%></td> <td><input type="button" name="delete_btn_<%=user._1%>" value="削除" onclick="submit_delete('<%=user._1%>')"></td> </tr> #end #else <tr> <td colspan="4">データがありません</td> </tr> #end </table> </div> </body> </html> |
テンプレートはscalateフォーマットに従って記述します。scalateについてはこちらの公式サイトを見てください。上の方から少し説明しようと思います。
1 2 3 4 5 6 7 |
<%@ val firstName:String %> <%@ val lastName:String %> <%@ val age:String %> <%@ val userId:String %> <%@ val errorMsg:String %> <%@ val infoMsg:String %> <%@ val userList:List[(Int,String,String,Short)] %> |
ScalatratestAppのget(“/”)内でテンプレートの呼び出し処理があったと思いますが、その時にアサインした変数をテンプレート側の変数に代入するための定義です。
1 2 3 4 5 6 7 8 9 10 |
#if (errorMsg != "") <div style="color:red"> <%=errorMsg%> </div> #end #if (infoMsg != "") <div style="color:blue"> <%=infoMsg%> </div> #end |
フラッシュデータにエラーメッセージ、インフォメーションメッセージが設定されていた場合にそれを表示するためのものです。if文の文法はscalaに準拠しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#if (userList.size > 0) #for (user <- userList) <tr> <td><%=user._1%></td> <td><%=user._2%></td> <td><%=user._3%></td> <td><%=user._4%></td> <td><input type="button" name="delete_btn_<%=user._1%>" value="削除" onclick="submit_delete('<%=user._1%>')"></td> </tr> #end #else <tr> <td colspan="4">データがありません</td> </tr> #end |
ユーザー情報が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/に保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
package scalatratest import scala.slick.driver.MySQLDriver.simple._ import scala.slick.jdbc.JdbcBackend.Database.dynamicSession import scalatratest.Tables._ object DataConvertHelpers { /** String型にInteger型に変換するtoIntを定義 **/ implicit class StringToInt(val value:String) extends AnyVal { def toInt:Int = Integer.parseInt(value) } /** String型にShort型に変換するtoShortを定義 **/ implicit class StringToShort(val value:String) extends AnyVal { def toShort:Short = java.lang.Short.parseShort(value) } } class ScalatratestModel(db:Database) { /** ユーザーIDからユーザー情報取得 **/ def getUserByUid(userId:String):List[(Int,String,String,Short)] = { this.db.withDynSession { UserMaster.filter(p => p.userId === userId.toInt).list.map( p => { (p.userId , p.firstName , p.lastName , p.age) } ) toList } } /** 指定されたユーザーIDを持つユーザー情報の件数をカウント**/ def countUserByUid(userId:String):Int = { this.db.withDynSession { UserMaster.filter(p => p.userId === userId.toInt).length.run } } /** ユーザー情報の新規登録・更新 **/ def upsertUser(params:Map[String,Any]) = { this.db.withDynTransaction { val userId:String = params.get("user_id").get.toString val firstName:String = params.get("first_name").get.toString val lastName:String = params.get("last_name").get.toString val age:String = params.get("age").get.toString if (this.countUserByUid(userId) > 0) { UserMaster.filter(p => p.userId === userId.toInt) .map(p => (p.userId, p.firstName, p.lastName , p.age)) .update( ( userId.toInt , firstName , lastName , age.toShort ) ) } else { UserMaster.map(p => (p.userId , p.firstName , p.lastName , p.age)) .insert( ( userId.toInt , firstName, lastName , age.toShort ) ) } } } /** 全ユーザー情報取得 **/ def getUserList():List[(Int,String,String,Short)] = { this.db.withDynSession { UserMaster.list.map( p => { (p.userId , p.firstName , p.lastName , p.age) } ) } } /** 指定されたユーザーIDを持つユーザー情報を削除 **/ def deleteUser(userId:String) = { this.db.withDynTransaction { UserMaster.filter(p => p.userId === userId.toInt).delete } } } |
上の方から説明していきます。
1 2 3 4 5 6 7 8 9 10 |
object DataConvertHelpers { /** String型にInteger型に変換するtoIntを追加 **/ implicit class StringToInt(val value:String) extends AnyVal { def toInt:Int = Integer.parseInt(value) } /** String型にShort型に変換するtoShortを追加 **/ implicit class StringToShort(val value:String) extends AnyVal { def toShort:Short = java.lang.Short.parseShort(value) } } |
上記はStringクラスにメソッドtoIntとtoShortを追加するという定義になります。
implicitを付けてクラスを定義するとコンストラクタの引数のデータ型のクラスにimplicit classの中のメソッドが追加されます。またAnyValを継承するとStringToInt、StringToShortがインスタンス化されず、メモリ割り当てがなされません。詳しくはこちらの値クラスと汎用トレイトを見てください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** ユーザーIDからユーザー情報取得 **/ def getUserByUid(userId:String):List[(Int,String,String,Short)] = { this.db.withDynSession { UserMaster.filter(p => p.userId === userId.toInt).list.map( p => { (p.userId , p.firstName , p.lastName , p.age) } ) toList } } /** 指定されたユーザーIDを持つユーザー情報の件数をカウント**/ def countUserByUid(userId:String):Int = { this.db.withDynSession { UserMaster.filter(p => p.userId === userId.toInt).length.run } } |
上記はORMを使用して条件に合致したユーザー情報の取得、件数のカウントをしています。filterの中はwhere句に該当します。filterの中に上記のようにSQLライクに抽出条件を記述することができます。詳しくはslick公式サイトのCOMING FROM ORM TO SLICK¶を見てください。
this.db.withDynSessionは自動的にDBのセッションの開始と終了をしてくれるものです。基本的にSELECT系のオペレーションをする場合にはwithDynSessionを使用しておけばいいです。こちらに関して詳しくはCONNECTIONS / TRANSACTIONS¶を見てください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
/** ユーザー情報の新規登録・更新 **/ def upsertUser(params:Map[String,Any]) = { this.db.withDynTransaction { val userId:String = params.get("user_id").get.toString val firstName:String = params.get("first_name").get.toString val lastName:String = params.get("last_name").get.toString val age:String = params.get("age").get.toString if (this.countUserByUid(userId) > 0) { UserMaster.filter(p => p.userId === userId.toInt) .map(p => (p.userId, p.firstName, p.lastName , p.age)) .update( ( userId.toInt , firstName , lastName , age.toShort ) ) } else { UserMaster.map(p => (p.userId , p.firstName , p.lastName , p.age)) .insert( ( userId.toInt , firstName, lastName , age.toShort ) ) } } } |
上記は入力されたユーザー情報IDと紐付くユーザー情報が既にDBに存在する場合には上書き更新、そうでなければ入力されたユーザー情報をDBに新規登録するという処理です。mapの中には更新対象または登録したいカラムを指定します。
this.db.withDynTransactionはオートコミットのオフ、トランザクションの開始と終了、例外が発生した場合のロールバックを自動的に行ってくれます。詳しくはCONNECTIONS / TRANSACTIONS¶を見てください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** 全ユーザー情報取得 **/ def getUserList():List[(Int,String,String,Short)] = { this.db.withDynSession { UserMaster.list.map( p => { (p.userId , p.firstName , p.lastName , p.age) } ) } } /** 指定されたユーザーIDを持つユーザー情報を削除 **/ def deleteUser(userId:String) = { this.db.withDynTransaction { UserMaster.filter(p => p.userId === userId.toInt).delete } } |
上記はすべてのユーザー情報の取得と指定されたユーザー情報IDと紐付くユーザー情報の削除を行うロジックとなります。
ながーくなってしまいましたが(すいません・・・)これですべてのソースコードがようやく揃いました。
scalatraアプリケーションの開発はこれで完了です。
6.c3p0.propertiesの作成
前項でscalatraアプリケーションを開発し終えたのでコンパイルしたいところですが、その前にc3p0(コネクションプール生成用のライブラリ)の設定ファイルを用意する必要があります。
c3p0.propertiesを以下の内容で/usr/local/src/webapp/bin/src/main/resources/に保存します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
c3p0.driverClass=com.mysql.jdbc.Driver c3p0.jdbcUrl=jdbc:mysql://[MySQLサーバーのホスト名かIP]/scalatratest?useUnicode=true&characterEncoding=utf8 c3p0.user=[MySQLログインID] c3p0.password=[MySQLログインパスワード] c3p0.initialPoolSize=100 c3p0.minPoolSize=50 c3p0.acquireIncrement=10 c3p0.maxPoolSize=512 c3p0.numHelperThreads=64 c3p0.maxStatements=0 c3p0.acquireRetryDelay=10 c3p0.debugUnreturnedConnectionStackTraces=true c3p0.checkoutTimeout=3000 c3p0.maxAdministrativeTaskTime = 0 |
c3p0の設定の詳細についてはc3p0 – JDBC3 Connection and Statement Poolingを見てください。
7.コンパイル・テスト
さて、それでは5で開発したscalatraアプリケーションをコンパイルします。sbtを起動し、compileコマンドを実行します。何もエラーがない場合には以下のような表示になると思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
> compile [info] Generating /usr/local/src/webapp/bin/target/scala-2.10/resource_managed/main/rebel.xml. [info] Updating {file:/usr/local/src/webapp/bin/}scalatratest... [info] Resolving org.scala-lang#scala-reflect;2.10.0 ... [info] Done updating. [warn] There may be incompatibilities among your library dependencies. [warn] Here are some of the libraries that were evicted: [warn] * com.typesafe.slick:slick_2.10:2.1.0-RC3 -> 2.1.0 [warn] Run 'evicted' to see detailed eviction warnings [info] Compiling Templates in Template Directory: /usr/local/src/webapp/bin/src/main/webapp/WEB-INF/templates SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details. [info] Compiling 12 Scala sources to /usr/local/src/webapp/bin/target/scala-2.10/classes... [warn] there were 4 feature warning(s); re-run with -feature for details [warn] one warning found [success] Total time: 10 s, completed Apr 18, 2016 5:52:35 PM |
1 |
[info] Compiling 12 Scala sources to /usr/local/src/webapp/bin/target/scala-2.10/classes... |
の部分は未コンパイルの状態でコンパイルされてscalaソース数が表示されますが、私の結果とは異なる可能もあります。
コンパイルが終わったら次に組み込み型Tomcatを起動してテストします。sbtのコマンドラインで以下のようにコマンドを入力します。
1 |
container:start |
これでsbt上で組み込み型Tomcatが起動し、外部からのHTTPリクエストを受け付けるようになります。ユーザー情報登録・上書き更新・削除・一覧表示機能が正常動作していることを確認します。
メイン画面は以下のような感じになります。
(上記画面キャプチャーではすでに2件のユーザー情報が登録されています)
8.クラスファイルのパッケージングとTomcatの上での起動
テストが終わったらパッケージングしてTomcat(組み込み型でない)上で動かしてみましょう。
まずはパッケージングをします。sbtを起動し、packageと入力してください。そうすると/usr/local/src/webapp/bin/target/scala-2.10にscalatratest_2.10-0.1.0.warが作成されます。
1 2 3 4 |
./sbt [info] Loading project definition from /usr/local/src/webapp/bin/project [info] Set current project to Scalatratest (in build file:/usr/local/src/webapp/bin/) >package |
これをTomcatのインストールディレクトリ直下にあるwebappsディレクトリにコピーします。その後ROOT.warにリネームし、元からあるROOTディレクトリを削除します。
1 2 3 4 |
cp -a target/scala-2.10/scalatratest_2.10-0.1.0.war /usr/local/tomcat/webapps #ここではTomcatは/usr/local/tomcat/にインストールされている。 cd /usr/local/tomcat/webapps mv scalatratest_2.10-0.1.0.war ROOT.war rm -rf ROOT |
最後にTomcatを起動します。
1 2 |
cd ../bin /bin/sh startup.sh |
/usr/local/tomcat/logs/catalina.outを見て
1 |
INFO: Server startup in [起動に要した時間] ms |
と表示されていれば成功です。
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アプリケーションの開発をしてみたいと思います。