みなさん初めまして。アニメイトラボのエンジニアtantanと申します。
この度2015年アドベントカレンダーの18日目を担当させていただくにあたり、Twitterやドワンゴでも使われたりしていて最近何かと話題の「scala」についてお話させていただきます。 qiita.com
scalaやplay frameworkの説明についてはwikipediaや他の先達にお任せするとして、わたしからは、tsql補完子を使ったmysql接続の基本についてお伝えさせていただきます。
tsql補完子について
tsql補完子とはslickが提供する機能の一つで、scala上で直接SQLを記述するために用いられます。tsql補完子には下記のような特徴があります。
- SQL内に直接変数を記述することが可能で、記述された変数はエスケープされる
- SQLインジェクション攻撃を未然に防ぐことができる
- プレースホルダに値をバインドする手間が減るためコードの記述量を減らせる
- コンパイル時にSELECT文の結果として期待される変数の型とDB上のカラム型の比較を行う
動作環境
play frameworkは公式サイトからActicatorをダウンロードして利用しました。 下記のコマンドで新規アプリケーションを作成します。
./activator new
コマンドを実行するとアプリケーション名のディレクトリが生成されます。 生成されたディレクトリにあるbuild.sbtには下記を追記しています。
"mysql" % "mysql-connector-java" % "5.1.38" "com.typesafe.slick" %% "slick" % "3.1.1" "com.typesafe.play" %% "play-slick" % "1.1.1"
次にmysql接続設定として下記をapplication.confに追記しています。
master = { driver = "slick.driver.MySQLDriver$" db { driver = "com.mysql.jdbc.Driver" url = "jdbc:mysql://localhost/scala_test?characterEncoding=utf8&useSSL=false" user = "【ユーザ】" password = "【パスワード】" } }
【ユーザ】と【パスワード】はlocalhostで起動しているmysqlで利用可能なユーザを設定してください。
mysql内のテーブル定義は下記の通りです。
desc scala_test.test; +--------------+------------------+------+------+-----------+--------+ | Field | Type | Null | Key | Default | Extra | +--------------+------------------+------+------+-----------+--------+ | id | int(11) | NO | PRI | 0 | | | title | varchar(255) | NO | | | | | description | text | NO | | NULL | | +--------------+------------------+------+------+-----------+--------+
SQLの実行処理
さて、前置きが長くなりましたが次からINSERT、UPDATE、SELECT、DELETEのそれぞれのSQLを実行していきます。
INSERT文の実行処理
まずはSQLを記述するTestDaoクラスとINSERT処理を実行するApplicationModelクラスを実装します。
【TestDao.slaca】
package models.daos import slick.driver.MySQLDriver.api._ import slick.backend.StaticDatabaseConfig case class InsertTestCols(id: Int, title: String, description: String) class TestDao { @StaticDatabaseConfig("file:conf/application.conf#master") def insert(cols: InsertTestCols): DBIO[Seq[(Any)]] = { tsql""" INSERT INTO `test` (`id`, `title`, `description`) VALUES (${cols.id}, ${cols.title}, ${cols.description}) """ } /** * 他の処理 */ }
TestDaoにはscala_test.testに対するSQLを記述しています。戻り値はDBIO型のジェネリクスにSeq型で取得カラムの型を列挙します。
SQLを見ると引数をそのまま埋め込んでいますが、内部的にエスケープされるためSQLインジェクションは発生しません。
【ApplicationModel.scala】
package models import daos._ import slick.driver._ import slick.backend._ import scala.concurrent.Await import scala.concurrent.duration.Duration class ApplicationModel extends JdbcDriver with JdbcActionComponent { // application.confからmasterというパラメータに紐づいている設定を読み込む @StaticDatabaseConfig("file:conf/application.conf#master") def insert: Unit = { val test_dao = new TestDao val dc = DatabaseConfig.forAnnotation[JdbcProfile] val db = dc.db try { // 実行SQLを取得 var execute_sql = test_dao.insert(InsertTestCols(1, "title", "description")) execute_sql = execute_sql >> test_dao.insert(InsertTestCols(2, "title2", "description2")) // トランザクション内でSQLを実行するように指定 val transaction_sql = new JdbcActionExtensionMethods(execute_sql).transactionally // クエリを実行 Await.result(db.run(transaction_sql), Duration.Inf) } catch { case e: Exception => throw e } finally { db.close } } /** * 他の処理 */ }
ApplicationModelではトランザクションを実現するJdbcActionExtensionMethods(execute_sql).transactionallyを利用するためにJdbcDriverとJdbcActionComponentを継承しています。
insert関数の上部にStaticDatabaseConfigアノテーションがありますが、これはDBの接続設定を参照しています。
SQLはAwaitを使って非同期的に実行されます。
UPDATE文、DELETE文の実行処理
UPDATE文とDELETE文の処理は次のようになります。
【TestDao.slaca】
package models.daos import slick.driver.MySQLDriver.api._ import slick.backend.StaticDatabaseConfig case class InsertTestCols(id: Int, title: String, description: String) class TestDao { @StaticDatabaseConfig("file:conf/application.conf#master") def update(id: Int, description: String): DBIO[Seq[(Any)]] = { tsql""" UPDATE `test` SET `description` = $description WHERE `id` = $id """ } @StaticDatabaseConfig("file:conf/application.conf#master") def delete(id: Int): DBIO[Seq[(Any)]] = { tsql"""DELETE FROM `test` WHERE `id` = $id""" } /** * 他の処理 */ }
INSERT文の時には、埋め込まれる変数が${cols.id}と記述されていましたが、こちらでは単純に$idと記述しています。
INSERT文の時にはInsertTestColsというケースクラスを引数としていました。そのため、メンバにアクセスするために{}が必要でした。
一方、今回は変数そのものを直接埋め込むため、$の直後に変数を記述するだけで変数の埋め込みを行えます。
【ApplicationModel.scala】
package models import daos._ import slick.driver._ import slick.backend._ import scala.concurrent.Await import scala.concurrent.duration.Duration class ApplicationModel extends JdbcDriver with JdbcActionComponent { @StaticDatabaseConfig("file:conf/application.conf#master") def update: Unit = { val test_dao = new TestDao val dc = DatabaseConfig.forAnnotation[JdbcProfile] val db = dc.db try { var execute_sql = test_dao.update(1, "アップデートテスト") val transaction_sql = new JdbcActionExtensionMethods(execute_sql).transactionally Await.result(db.run(transaction_sql), Duration.Inf) } catch { case e: Exception => throw e } finally { db.close } } @StaticDatabaseConfig("file:conf/application.conf#master") def delete: Unit = { val test_dao = new TestDao val dc = DatabaseConfig.forAnnotation[JdbcProfile] val db = dc.db try { var execute_sql = test_dao.delete(1) val transaction_sql = new JdbcActionExtensionMethods(execute_sql).transactionally Await.result(db.run(transaction_sql), Duration.Inf) } catch { case e: Exception => throw e } finally { db.close } } /** * 他の処理 */ }
INSERT文の実行時と特に変わった部分はなく、TestDaoに記述されたメソッドを呼び出してUPDATE/DELETEを実行しています。
SELECT文の実行処理
【TestDao.slaca】
package models.daos import slick.driver.MySQLDriver.api._ import slick.backend.StaticDatabaseConfig case class InsertTestCols(id: Int, title: String, description: String) class TestDao { @StaticDatabaseConfig("file:conf/application.conf#master") def selectAll: DBIO[Seq[(Int, String, String)]] = { tsql""" INSERT INTO `test` (`id`, `title`, `description`) VALUES (${cols.id}, ${cols.title}, ${cols.description}) """ } /** * 他の処理 */ }
SELECT文ではDBIOのジェネリティクス型にSeq型でSQLで取得するカラムの型を列挙指定します。こうすることでSELECT文の実行結果をSeq型として扱うことができます。
【ApplicationModel.scala】
package models import daos._ import slick.driver._ import slick.backend._ import scala.concurrent.Await import scala.concurrent.duration.Duration class ApplicationModel extends JdbcDriver with JdbcActionComponent { @StaticDatabaseConfig("file:conf/application.conf#master") def getAll: Seq[(Int, Int, String)] = { val test_dao = new TestDao var test_data = Seq[(Int, String, String)]() val dc = DatabaseConfig.forAnnotation[JdbcProfile] val db = dc.db try { var execute_sql = test_dao.selectAll test_data = Await.result(db.run(execute_sql), Duration.Inf) } catch { case e: Exception => throw e } finally { db.close } test_data } /** * 他の処理 */ }
selectAll関数の実行結果をそのままSeq型に詰めて戻り値としています。
終わりに
slickのtsql補完子を利用した一通りのDB操作を説明させていただきましたがいかがだったでしょうか。まだまだ日本語の説明が充実しているとはいいがたいですが、これからscalaに触れていく方々の一助になれば幸いです。