Slickを使ってデータベースを操作する方法を紹介
[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください
以下の記事では、JDBC, Scalikejdbc, Slickの3つの方法でデータベースからデータを抽出する方法を説明しました。
この記事では、Slickを使ってテーブルの作成、レコードの追加、更新、削除、いろいろなレコードの照会を行う方法を解説します。
注:SlickはまだScala 3に対応していません(2023年02月04日現在)。以下の記述はScala 2系用のものです。今後変わる見込みです。
Slickの基本的な使い方
Slickは、Scalaのコレクションを扱うようにデータベースを操作することができるといった特徴があります。
Slickではまずクエリーを構築し、次にアクションを取得、そしてアクションを実行して結果を取得します。
クエリーで使用する主なタイプはRep
です。
コレクションであるクエリーはQuery
です。
クエリーはテーブルに関連づいたTableQuery
から構築できます。
アクションはデータベース上で実行可能な操作で、DBIOAction
で表します。
アクションはクエリーの操作から作成します。
そして最後にアクションをデータベース上で実行します。
本記事では、以下の2つのテーブルを例にSlickを使ってデータベースを操作する方法を解説します。
Table name | Explain |
---|---|
Category | 商品をカテゴリーごとに分類するためのテーブル |
Item | 商品を管理するためのテーブル |
「Categoryテーブル」と「Itemテーブル」は以下のように1対多の関連を持ちます。
必要なライブラリを定義する
Slickを使うために必要なライブラリをbuild.sbtに追加します。
以下はバージョン3.3.3のSlickを使用する例です。
libraryDependencies ++= Seq( "com.typesafe.slick" %% "slick" % "3.3.3", "org.slf4j" % "slf4j-nop" % "1.6.4", "com.typesafe.slick" %% "slick-hikaricp" % "3.3.3" )
また、使用するデータベースへの依存性もbuild.sbtに追加します。
この記事の例では H2 Database を使用します。
H2データベースとは、Java VM上で動作する軽量データベースエンジンです。
libraryDependencies ++= Seq( "com.h2database" % "h2" % "1.4.200" )
データベースへの接続情報を設定する
使用するデータベースへの接続情報を設定します。
デーベースへの接続情報を設定する方法はいくつかありますが、ここではTypesafe Configを使用して設定する方法を説明します。
データベースへの接続先(url)と使用するJDBCドライバ(driver)を決定します。
application.confファイルを用意し上記の値を記載します。
- 定義例
h2mem = { url = "jdbc:h2:mem:test;DATABASE_TO_UPPER=false" driver = org.h2.Driver }
必要なパッケージをインポートする
SlickのDSLを使用するためのパッケージをインポートします。
この記事ではH2 Databaseを使うために以下をimport
します。
import slick.dbio.Effect import slick.jdbc.H2Profile import slick.jdbc.H2Profile.api.* import slick.sql.{FixedSqlAction, FixedSqlStreamingAction} import scala.concurrent.Future
SlickにはH2 Database以外にサポートするデータベースに対応するパッケージが用意されています。
Slickがサーポートするデータベースは、
Slickがサーポートするデータベース
を参照してください。
データベースを操作する
Slickでは、以下の手順に従ってデータベースを操作します。
まず、データベースへの操作を実行するDatabse
のインスタンスを取得します。
Database
のインスタンスはDatabase.forConfig
を使用して取得します。
以下は「データベースへの接続情報を設定する」で定義した値でデータベースに接続する例です。
val db = Database.forConfig("h2mem")
次にデータベースへの操作を定義し実行します。
// データベースへの問い合わせ手順を定義する val query = for { category <- category if(category.name === "カテゴリ5") item <- item if(item.categoryId === category.id) } yield (category.name, item.name, item.price) // 手順を実行。データベースへのアクセスは非同期で実行される。 val f = db.run(query.result) // 完了を待って結果を取得する val result = Await.result(f, Duration.Inf) result.foreach(r => println(s"category: ${r._1} item: ${r._2} price: ${r._3}"))
最後にclose
を呼び出して終了します。
db.close()
上記のコードを実行すると以下のように出力されます。
category: カテゴリ5 item: アイテム5-1 price: 1098 category: カテゴリ5 item: アイテム5-2 price: 980 category: カテゴリ5 item: アイテム5-3 price: 500 category: カテゴリ5 item: アイテム5-4 price: 2980 category: カテゴリ5 item: アイテム5-5 price: 5000
スキーマを定義する
まず最初にテーブルのスキーマを定義します。
以下がこの記事で使用する2つのテーブルのスキーマ定義です。
Table
を継承してテーブルの行を定義します。
また、TableQuery
を使用してテーブルを操作するために使用する値を定義します。
加えて以下の例では、テーブルの行をマッピングするためにケースクラスを定義しています。
// Categoryテーブルの定義 case class CategoryRow(id: Long, name: String) class CategoryTable(tag: Tag) extends Table[CategoryRow](tag, "category") { def id: Rep[Long]= column[Long]("id", O.PrimaryKey, O.AutoInc) def name: Rep[String] = column[String]("name") def * = (id, name) <> (CategoryRow.tupled, CategoryRow.unapply) } lazy val category = new TableQuery[CategoryTable](tag => new CategoryTable(tag)) // Itemテーブルの定義 case class ItemRow(id: Long, name: String, price: Int, categoryId: Long) class ItemTable(tag: Tag) extends Table[ItemRow](tag, "item") { def id: Rep[Long] = column[Long]("id", O.PrimaryKey, O.AutoInc) def name: Rep[String] = column[String]("name") def price: Rep[Int] = column[Int]("age") def categoryId: Rep[Long] = column[Long]("category_id") def * = (id, name, price, categoryId) <> (ItemRow.tupled, ItemRow.unapply) } lazy val item = new TableQuery[ItemTable](tag => new ItemTable(tag))
テーブルを作成する
先程定義したテーブルのスキーマを使用してテーブルを作成します。
val setup: DBIOAction[Unit, NoStream, Effect.Schema] = DBIO.seq( category.schema.create, item.schema.create, ) val setupFuture: Future[Unit] = db.run(setup)
以下のように記述することもできます。
val setup: FixedSqlAction[Unit, NoStream, Effect.Schema] = (category.schema ++ item.schema).create val setupFuture: Future[Unit] = db.run(setup)
テーブルにレコードを追加する
作成したテーブルにレコードを追加します。
「Categoryテーブル」にレコードを追加する例です。
「スキーマを定義する」で定義したTableQuery
の値に対して、レコードを追加していきます。
レコードを追加するには+=
メソッドを使用します。
また、Seq
を使用して複数レコードをまとめて追加するように記述できます。
+=
の場合は記載した数分のINSERT
が発行されます。
Seq
を使用するとJDBCのバッチ更新を使用してレコードを追加します。(データベースがサポートしている場合)
val addRecord: DBIOAction[Unit, NoStream, Effect.Write] = DBIO.seq( // 個々にINSERTを実行 category += CategoryRow(0, "カテゴリ1"), category += CategoryRow(0, "カテゴリ2"), // 可能であればJDBCのバッチ更新でレコードを追加する category ++= Seq( CategoryRow(0, "カテゴリ3"), CategoryRow(0, "カテゴリ4"), ), ) val addRecFuture: Future[Unit] = db.run(addRecord)
Insert後にAuto Incrementの値を取得する
次に「Itemテーブル」にレコードを追加する例を見てみます。
「itemテーブル」を追加するには「categoryテーブル」のid
が必要です。
「categoryテーブル」と「itemテーブル」は「1対多」の関連があります。
そのため、「itemテーブル」には「Categoryテーブル」のid
の値をcategoryId
に設定する必要があります。
「Categoryテーブル」の「idカラム」はAuto Incrementなので、レコードを追加する際に値を指定しません。
そのため、「Categoryテーブル」と「Itemテーブル」に同時にレコードを追加するには、以下のようにCategory追加後に取得したid
の値をItemのcategoryId
に指定します。
returning
メソッドは指定したカラムの値を返します。(以下の例の場合は「Categoryテーブル」の「idカラム」の値)
取得した値をその後のレコードの追加時に使用します。
val action: DBIOAction[(Long, Option[Int]), NoStream, Effect.Write] = for { categoryId <- category returning category.map(_.id) += CategoryRow(0, "カテゴリ5") num <- item ++= Seq( ItemRow(0, "アイテム5-1", 1098, categoryId), ItemRow(0, "アイテム5-2", 980, categoryId), ItemRow(0, "アイテム5-3", 500, categoryId), ItemRow(0, "アイテム5-4", 2980, categoryId), ItemRow(0, "アイテム5-5", 5000, categoryId), ) } yield (categoryId, num) val f: Future[(Long, Option[Int])] = db.run(action)
データを取得する
次にデータベースからレコードを取得する方法を説明します。
すべてのレコードを取得する
以下のSQLは「Itemテーブル」からすべてのレコードを取得します。
SQLSELECT * FROM item
「Itemテーブル」のすべてのレコードを取得するには以下のように記述します。
val query: Query[(Rep[String], Rep[Int]), (String, Int), Seq] = for(i <- item) yield (i.name, i.price) val f: Future[Seq[(String, Int)]] = db.run(query.result)
条件に一致するレコードを取得する
指定した条件に該当するレコードを取得する方法を説明します。
以下のSQLは「Itemテーブル」からprice
が100
より大きなレコードを取得します。
SQLSELECT * FROM item WHERE price > 100
条件を取得する方法は以下のように「すべてのレコードを取得する」で使用したfor
に条件を記述する方法とfilter
を使用する方法があります。
for
に条件を記述する方法
val query: Query[ItemTable, ItemRow, Seq] = for(i <- item if(i.price > 1000)) yield i val f: Future[Seq[ItemRow]] = db.run(query.result)
filter
を使用する方法
val query: Query[ItemTable, ItemRow, Seq] = item.filter(i => i.price > 1000) val f: Future[Seq[ItemRow]] = db.run(query.result)
ソート条件を指定する
指定したカラムの値でソートしたレコードを取得する方法を説明します。
以下のSQLは、price
の値でそれぞれ降順、昇順にソートした値を取得します。
- 降順にソート
SQLSELECT * FROM item ORDER BY price DESC
- 昇順にソート
SQLSELECT * FROM item ORDER BY price ASC
Slickでは、sortBy
を使用してソートした結果を取得できます。
以下のようにsortBy
を使用してソート対象とするカラムに対して降順(desc
)または昇順(asc
)のどちらかを指定します。
- 降順にソート
val query: Query[ItemTable, ItemRow, Seq] = item.sortBy( i => (i.price.desc, i.id.desc)) val f: Future[Seq[ItemRow]] = db.run(query.result)
- 昇順にソート
val query: Query[ItemTable, ItemRow, Seq] = item.sortBy(i => (i.price.asc, i.id.asc)) val f: Future[Seq[ItemRow]] = db.run(query.result)
最小値、最大値を取得する
集計関数を使用する例を見てみます。
SQLの集計関数に対応する関数も以下のように用意されています。
- 最大値を取得する
SQLSELECT MAX(price) FROM item
以下のように記述します。
val query: Rep[Option[Int]] = item.map(_.price).max val f: Future[Option[Int]] = db.run(query.result)
- 最小値を取得する
SQLSELECT MIN(price) FROM item
以下のように記述します。
val query: Rep[Option[Int]] = item.map(_.price).min val f: Future[Option[Int]] = db.run(query.result)
- 平均値を取得する
SQLSELECT AVG(price) FROM item
以下のように記述します。
val query: Rep[Option[Int]] = item.map(_.price).avg val f: Future[Option[Int]] = db.run(query.result)
- 合計値を取得する
SQLSELECT SUM(price) FROM item
以下のように記述します。
val query: Rep[Option[Int]] = item.map(_.price).sum val f: Future[Option[Int]] = db.run(query.result)
グルーピングをする
指定したカラムでグルーピングする方法を説明します。
以下のSQLは、categoryId
ごとのprice
の平均値を求めています。
SQLSELECT AVG(price) FROM item GROUP BY categoryId
SlickではgroupBy
を使用してグルーピングします。
以下の例ではgroupBy
を使用してcategoryId
でグルーピングした結果に対してavg
を適用しています。
val query: Query[(Rep[Long], Rep[Option[Int]]), (Long, Option[Int]), Seq] = item.groupBy(i => i.categoryId).map { case (categoryId, g) => (categoryId, g.map(_.price).avg) } val f: Future[Seq[(Long, Option[Int])]] = db.run(query.result)
複数のテーブルを結合する
複数のテーブルを結合する方法です。
以下のSQLは、「Itemテーブル」と「Categoryテーブル」を結合した結果を取得します。
SQLSELECT category.name, item.name, item.price FROM category JOIN item ON (item.categoryId = category.id)
Slickでは以下のどちらかの方法で複数のテーブルを結合します。
join
を使用する方法
val query: Query[(Rep[String], Rep[String], Rep[Int]), (String, String, Int), Seq] = item.join(category).on(_.categoryId === _.id).map { case (i, c) => (c.name, i.name, i.price) } val f: Future[Seq[(String, String, Int)]] = db.run(query.result)
for
で条件を指定する方法
val query: Query[(Rep[String], Rep[String], Rep[Int]), (String, String, Int), Seq] = for { c <- category i <- item if i.categoryId === c.id } yield (c.name, i.name, i.price) val f: Future[Seq[(String, String, Int)]] = db.run(query.result)
サブクエリを使用する
サブクエリを使用することもできます。
以下のSQLは「Itemテーブル」のcategoryId
が「Categoryテーブル」のname
が「カテゴリ5」のレコードのid
と一致するレコードを取得します。
SQLSELECT * FROM item WHERE item.categoryId in ( SELECT id FROM category WHERE name like 'f%' )
上記SQLに該当する問い合わせをSlickで記述すると以下のようになります。
val categoryIds: Query[Rep[Long], Long, Seq] = category.filter(_.name === "カテゴリ5").map(_.id) val query: Query[ItemTable, ItemRow, Seq] = item.filter(_.categoryId in categoryIds) val f: Future[Seq[ItemRow]] = db.run(query.result)
カラムの値を更新する
カラムの値を更新する方法です。
以下のSQLは、「Itemテーブル」のid
が1のレコードのprice
を200に更新します。
SQLUPDATE item SET price = 200 WHERE id = 1
上記SQLに該当する操作をSlickで記述すると以下のようになります。
まず、filterを使用して更新するレコードを絞り込む定義をします。
次に更新するカラムをmap
で抽出し、update
で値を更新します。
val action: FixedSqlAction[Int, NoStream, Effect.Write] = item.filter(_.id === 1L) .map(i => i.price) .update(200) val f: Future[Int] = db.run(action)
レコードを削除する
レコードを削除する方法です。
以下のSQLは、「Itemテーブル」からprice
が100より小さなレコードを削除します。
SQLDELETE FROM item WHERE price < 100
上記SQLに該当する操作をSlickで記述すると以下のようになります。
まず、filterを使用して更新するレコードを絞り込む定義をします。
絞り込んだ結果に対してdelete
を適用します。
val action: FixedSqlAction[Int, NoStream, Effect.Write] = item.filter(_.price === 123).delete val f: Future[Int] = db.run(action)
スキーマ定義を自動生成する
この記事では最初にテーブルのスキーマの定義をしました。
Slickでは存在するテーブルからスキーマを自動生成する方法が用意されています。
スキーマを手動で定義する作業は大変なので、テーブルをあらかじめ作成しておき、スキーマは自動生成することをおすすめします。
以下のコードを実行するとデータベースに定義されているテーブルからスキーマを自動生成します。
slick.codegen.SourceCodeGenerator.main( Array( "slick.jdbc.H2Profile", "org.h2.Driver", "jdbc:h2:mem:test", DIST, // 生成したスキーマ定義の保管先ディレクトリ PACKAGE, // スキーマ定義のパッケージ ) )