アニメイトラボ開発者ブログ

株式会社アニメイトラボの開発者ブログです


アニメイトラボ開発者ブログ

developer.animatelab.com


scala on play framework - tsql補完子を使ったmysql接続のキホン

みなさん初めまして。アニメイトラボのエンジニアtantanと申します。

この度2015年アドベントカレンダーの18日目を担当させていただくにあたり、Twitterドワンゴでも使われたりしていて最近何かと話題の「scala」についてお話させていただきます。 qiita.com

scalaやplay frameworkの説明についてはwikipediaや他の先達にお任せするとして、わたしからは、tsql補完子を使ったmysql接続の基本についてお伝えさせていただきます。

tsql補完子について

tsql補完子とはslickが提供する機能の一つで、scala上で直接SQLを記述するために用いられます。tsql補完子には下記のような特徴があります。

  • SQL内に直接変数を記述することが可能で、記述された変数はエスケープされる
  • コンパイル時にSELECT文の結果として期待される変数の型とDB上のカラム型の比較を行う
    • 実際にSQLが実行される前にコードとテーブル定義の差異に気付けるためデバッグしやすい
    • テーブル定義とコード上の型の差異によるバグを防止できる

動作環境

f:id:tantan8:20151218112808p:plain

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に触れていく方々の一助になれば幸いです。