Slickを使ってデータベースを操作する方法を紹介

最終更新:2023年12月7日

[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 nameExplain
Category商品をカテゴリーごとに分類するためのテーブル
Item商品を管理するためのテーブル

「Categoryテーブル」と「Itemテーブル」は以下のように1対多の関連を持ちます。

Scheme

必要なライブラリを定義する

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テーブル」からすべてのレコードを取得します。

SQL
SELECT * 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テーブル」からprice100より大きなレコードを取得します。

SQL
SELECT * 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の値でそれぞれ降順、昇順にソートした値を取得します。

  • 降順にソート
SQL
SELECT * FROM item ORDER BY price DESC
  • 昇順にソート
SQL
SELECT * 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の集計関数に対応する関数も以下のように用意されています。

  • 最大値を取得する
SQL
SELECT MAX(price) FROM item

以下のように記述します。

val query: Rep[Option[Int]] = item.map(_.price).max val f: Future[Option[Int]] = db.run(query.result)
  • 最小値を取得する
SQL
SELECT MIN(price) FROM item

以下のように記述します。

val query: Rep[Option[Int]] = item.map(_.price).min val f: Future[Option[Int]] = db.run(query.result)
  • 平均値を取得する
SQL
SELECT AVG(price) FROM item

以下のように記述します。

val query: Rep[Option[Int]] = item.map(_.price).avg val f: Future[Option[Int]] = db.run(query.result)
  • 合計値を取得する
SQL
SELECT 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の平均値を求めています。

SQL
SELECT 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テーブル」を結合した結果を取得します。

SQL
SELECT 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と一致するレコードを取得します。

SQL
SELECT * 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に更新します。

SQL
UPDATE 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より小さなレコードを削除します。

SQL
DELETE 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, // スキーマ定義のパッケージ ) )

サイト内検索