Slick
Slick(Scala Language-Integrated Connection Kit)は「Scalaのための関数型リレーショナルマッピング(FRM)ライブラリ」として、型安全で関数型プログラミングパラダイムに基づくデータベースアクセス層を提供します。従来のORMとは異なり、SQLライクなクエリを直接Scalaコード内で記述でき、コンパイル時の型チェックによりクエリの正確性を保証します。Reactive Streamsサポート、非同期処理、関数合成によるクエリ構築により、モダンなScalaアプリケーション開発において高いパフォーマンスと保守性を実現します。
GitHub概要
slick/slick
Slick (Scala Language Integrated Connection Kit) is a modern database query and access library for Scala
トピックス
スター履歴
ライブラリ
Slick
概要
Slick(Scala Language-Integrated Connection Kit)は「Scalaのための関数型リレーショナルマッピング(FRM)ライブラリ」として、型安全で関数型プログラミングパラダイムに基づくデータベースアクセス層を提供します。従来のORMとは異なり、SQLライクなクエリを直接Scalaコード内で記述でき、コンパイル時の型チェックによりクエリの正確性を保証します。Reactive Streamsサポート、非同期処理、関数合成によるクエリ構築により、モダンなScalaアプリケーション開発において高いパフォーマンスと保守性を実現します。
詳細
Slick 2025年版はScala 3およびScala 2.13に完全対応し、関数型データベースアクセスのベストプラクティスを体現しています。SQL DDL生成、スキーママイグレーション、複数データベースエンジン対応(PostgreSQL、MySQL、SQLite、H2、SQL Server等)によりエンタープライズレベルの要件に対応。Future-based非同期API、Akka Streamsとの統合、Effect型(cats-effect、ZIO等)のサポートにより、リアクティブアプリケーション開発に最適化されています。Plain SQLとの混在利用も可能で、複雑なクエリが必要な場合の柔軟性を提供します。
主な特徴
- 型安全クエリ: Scalaの型システムを活用したコンパイル時クエリ検証
- 関数型アプローチ: 関数合成によるクエリ構築と再利用
- マルチDBサポート: 主要なRDBMS全般に対応
- Reactive Streams: 非同期ストリーミングクエリサポート
- DDL生成: スキーマ定義からDDLの自動生成
- Plain SQL混在: 複雑な要件に対する生SQL実行機能
メリット・デメリット
メリット
- Scalaの型システムを最大限活用した強力な型安全性
- 関数型プログラミングパラダイムとの自然な統合
- 非同期処理とReactive Streamsによる高いパフォーマンス
- コンパイル時でのクエリエラー検出による品質向上
- 複数データベースへの統一的なアクセス層提供
- Akka、Play Framework等のScalaエコシステムとの親和性
デメリット
- Scala特有の学習コストと関数型プログラミング習得の必要性
- ORM的な機能(自動関連性解決等)の不足
- 複雑なクエリでは生SQLの方が記述しやすい場合がある
- Java開発者にとってのアクセシビリティの低さ
- ドキュメントや日本語情報の限定性
- 小規模プロジェクトでは過剰な機能となる可能性
参考ページ
書き方の例
セットアップ
// build.sbt
val slickVersion = "3.5.0"
libraryDependencies ++= Seq(
"com.typesafe.slick" %% "slick" % slickVersion,
"com.typesafe.slick" %% "slick-hikaricp" % slickVersion,
"com.typesafe.slick" %% "slick-codegen" % slickVersion,
"org.postgresql" % "postgresql" % "42.7.1",
"com.h2database" % "h2" % "2.2.224",
"org.slf4j" % "slf4j-simple" % "2.0.9"
)
// Database configuration
import slick.jdbc.PostgresProfile.api._
import slick.basic.DatabaseConfig
import slick.jdbc.JdbcProfile
import scala.concurrent.ExecutionContext.Implicits.global
// データベース設定
val dbConfig = DatabaseConfig.forConfig[JdbcProfile]("mydb")
val db = dbConfig.db
// application.conf
/*
mydb {
profile = "slick.jdbc.PostgresProfile$"
db {
driver = "org.postgresql.Driver"
url = "jdbc:postgresql://localhost/mydb"
user = "postgres"
password = "password"
numThreads = 10
maxConnections = 10
}
}
*/
基本的な使い方
import slick.jdbc.PostgresProfile.api._
import scala.concurrent.Future
import java.time.LocalDateTime
// テーブル定義
case class User(id: Option[Long], name: String, email: String, age: Int, createdAt: LocalDateTime)
class Users(tag: Tag) extends Table[User](tag, "users") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name")
def email = column[String]("email")
def age = column[Int]("age")
def createdAt = column[LocalDateTime]("created_at")
// 一意制約
def emailIdx = index("idx_email", email, unique = true)
// プロジェクション(テーブルとケースクラスのマッピング)
def * = (id.?, name, email, age, createdAt).mapTo[User]
}
// テーブルクエリ
val users = TableQuery[Users]
// 投稿テーブル
case class Post(id: Option[Long], title: String, content: String, userId: Long, published: Boolean, createdAt: LocalDateTime)
class Posts(tag: Tag) extends Table[Post](tag, "posts") {
def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def title = column[String]("title")
def content = column[String]("content", O.SqlType("TEXT"))
def userId = column[Long]("user_id")
def published = column[Boolean]("published")
def createdAt = column[LocalDateTime]("created_at")
// 外部キー制約
def user = foreignKey("fk_posts_user", userId, users)(_.id.get)
def * = (id.?, title, content, userId, published, createdAt).mapTo[Post]
}
val posts = TableQuery[Posts]
// スキーマ作成
val schema = users.schema ++ posts.schema
// DDL生成とスキーマ作成
val setupAction = DBIO.seq(
schema.createIfNotExists,
// 初期データ挿入
users += User(None, "Alice", "[email protected]", 25, LocalDateTime.now()),
users += User(None, "Bob", "[email protected]", 30, LocalDateTime.now()),
users += User(None, "Charlie", "[email protected]", 35, LocalDateTime.now())
)
val setupFuture: Future[Unit] = db.run(setupAction)
クエリ実行
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import scala.util.{Success, Failure}
object UserService {
// 全ユーザー取得
def getAllUsers: Future[Seq[User]] = {
val query = users.result
db.run(query)
}
// ID指定でユーザー取得
def getUserById(id: Long): Future[Option[User]] = {
val query = users.filter(_.id === id).result.headOption
db.run(query)
}
// 名前で検索
def searchUsersByName(namePattern: String): Future[Seq[User]] = {
val query = users.filter(_.name.like(s"%$namePattern%")).result
db.run(query)
}
// 年齢範囲でフィルタ
def getUsersByAgeRange(minAge: Int, maxAge: Int): Future[Seq[User]] = {
val query = users.filter(u => u.age >= minAge && u.age <= maxAge).result
db.run(query)
}
// ユーザー作成
def createUser(name: String, email: String, age: Int): Future[User] = {
val insertAction = (users returning users.map(_.id)
into ((user, id) => user.copy(id = Some(id)))
) += User(None, name, email, age, LocalDateTime.now())
db.run(insertAction)
}
// ユーザー更新
def updateUser(id: Long, name: String, email: String, age: Int): Future[Int] = {
val updateAction = users.filter(_.id === id)
.map(u => (u.name, u.email, u.age))
.update((name, email, age))
db.run(updateAction)
}
// ユーザー削除
def deleteUser(id: Long): Future[Int] = {
val deleteAction = users.filter(_.id === id).delete
db.run(deleteAction)
}
// 複合クエリ例:アクティブユーザー統計
def getUserStatistics: Future[(Int, Double, Int, Int)] = {
val query = for {
totalUsers <- users.length.result
avgAge <- users.map(_.age).avg.result
usersWithPosts <- users.filter(_.id.in(posts.map(_.userId).distinct)).length.result
usersWithoutPosts <- users.filterNot(_.id.in(posts.map(_.userId).distinct)).length.result
} yield (totalUsers, avgAge.getOrElse(0.0), usersWithPosts, usersWithoutPosts)
db.run(query)
}
}
// 使用例
def demonstrateBasicQueries(): Unit = {
// ユーザー作成
val createUserFuture = UserService.createUser("Dave", "[email protected]", 28)
createUserFuture.onComplete {
case Success(user) =>
println(s"Created user: $user")
// 全ユーザー取得
UserService.getAllUsers.foreach { users =>
println(s"All users: $users")
}
case Failure(exception) =>
println(s"Failed to create user: $exception")
}
// 検索
UserService.searchUsersByName("A").foreach { users =>
println(s"Users with 'A' in name: $users")
}
// 年齢範囲フィルタ
UserService.getUsersByAgeRange(25, 35).foreach { users =>
println(s"Users aged 25-35: $users")
}
}
リレーションシップと結合
// Join クエリとリレーションシップ処理
object PostService {
// ユーザーと投稿の結合
def getPostsWithAuthors: Future[Seq[(Post, User)]] = {
val query = posts.join(users).on(_.userId === _.id).result
db.run(query)
}
// 左外部結合:全ユーザーと投稿(投稿がない場合もNull)
def getUsersWithOptionalPosts: Future[Seq[(User, Option[Post])]] = {
val query = users.joinLeft(posts).on(_.id === _.userId).result
db.run(query)
}
// 複雑な結合クエリ:ユーザー別投稿数
def getUserPostCounts: Future[Seq[(User, Int)]] = {
val query = users.joinLeft(posts).on(_.id === _.userId)
.groupBy(_._1.id)
.map { case (userId, group) =>
(group.map(_._1).max, group.map(_._2).length)
}
.result
db.run(query).map(_.collect {
case (Some(user), count) => (user, count)
})
}
// より効率的なユーザー別投稿数
def getUserPostCountsEfficient: Future[Seq[(String, Int)]] = {
val query = users.joinLeft(posts).on(_.id === _.userId)
.groupBy(_._1.name)
.map { case (userName, group) =>
(userName, group.length)
}
.result
db.run(query)
}
// 条件付き結合:公開済み投稿のみ
def getPublishedPostsWithAuthors: Future[Seq[(Post, User)]] = {
val query = posts.filter(_.published === true)
.join(users).on(_.userId === _.id)
.result
db.run(query)
}
// 投稿作成
def createPost(title: String, content: String, userId: Long, published: Boolean = false): Future[Post] = {
val insertAction = (posts returning posts.map(_.id)
into ((post, id) => post.copy(id = Some(id)))
) += Post(None, title, content, userId, published, LocalDateTime.now())
db.run(insertAction)
}
// ユーザーの投稿を取得
def getPostsByUser(userId: Long): Future[Seq[Post]] = {
val query = posts.filter(_.userId === userId).result
db.run(query)
}
// 人気ユーザー(投稿数順)
def getPopularUsers(limit: Int = 10): Future[Seq[(String, Int)]] = {
val query = users.join(posts).on(_.id === _.userId)
.groupBy(_._1.name)
.map { case (userName, group) => (userName, group.length) }
.sortBy(_._2.desc)
.take(limit)
.result
db.run(query)
}
}
// 使用例
def demonstrateJoins(): Unit = {
// 投稿作成
for {
user <- UserService.getUserById(1L)
_ <- user match {
case Some(u) =>
PostService.createPost("First Post", "This is my first post!", u.id.get, published = true)
case None =>
Future.failed(new RuntimeException("User not found"))
}
// 投稿とユーザーの結合クエリ
postsWithAuthors <- PostService.getPostsWithAuthors
_ = println(s"Posts with authors: $postsWithAuthors")
// ユーザー別投稿数
userPostCounts <- PostService.getUserPostCountsEfficient
_ = println(s"User post counts: $userPostCounts")
} yield ()
}
トランザクションと高度な操作
import slick.dbio.Effect
import slick.sql.FixedSqlAction
object AdvancedUserService {
// トランザクション処理
def transferPostsBetweenUsers(fromUserId: Long, toUserId: Long): Future[Unit] = {
val transaction = (for {
fromUser <- users.filter(_.id === fromUserId).result.headOption
toUser <- users.filter(_.id === toUserId).result.headOption
result <- (fromUser, toUser) match {
case (Some(_), Some(_)) =>
posts.filter(_.userId === fromUserId)
.map(_.userId)
.update(toUserId)
.map(_ => ())
case _ =>
DBIO.failed(new RuntimeException("One or both users not found"))
}
} yield result).transactionally
db.run(transaction)
}
// バッチ処理
def batchCreateUsers(usersData: Seq[(String, String, Int)]): Future[Seq[User]] = {
val insertActions = usersData.map { case (name, email, age) =>
User(None, name, email, age, LocalDateTime.now())
}
val batchInsert = (users returning users.map(_.id)
into ((user, id) => user.copy(id = Some(id)))
) ++= insertActions
db.run(batchInsert)
}
// 複雑な更新:条件付き一括更新
def updateUsersAgeByRange(ageIncrease: Int, minAge: Int, maxAge: Int): Future[Int] = {
val updateAction = users.filter(u => u.age >= minAge && u.age <= maxAge)
.map(_.age)
.update(minAge + ageIncrease) // これは簡略化。実際はより複雑な処理が必要
db.run(updateAction)
}
// カスタムSQL実行
def executeCustomQuery(minPostCount: Int): Future[Seq[(String, Int)]] = {
val query = sql"""
SELECT u.name, COUNT(p.id) as post_count
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
GROUP BY u.id, u.name
HAVING COUNT(p.id) >= $minPostCount
ORDER BY post_count DESC
""".as[(String, Int)]
db.run(query)
}
// Stream処理(大量データ対応)
def streamAllUsers: DatabasePublisher[User] = {
db.stream(users.result)
}
// 条件付き削除とクリーンアップ
def cleanupInactiveUsers(daysSinceLastPost: Int): Future[Int] = {
val cutoffDate = LocalDateTime.now().minusDays(daysSinceLastPost)
val deleteAction = for {
// 古い投稿がないユーザーのIDを取得
inactiveUserIds <- users.filterNot(_.id.in(
posts.filter(_.createdAt > cutoffDate).map(_.userId)
)).map(_.id).result
// 投稿を持たないユーザーを削除
deletedCount <- users.filter(_.id.inSet(inactiveUserIds.flatten.toSet)).delete
} yield deletedCount
db.run(deleteAction.transactionally)
}
// 統計情報取得
def getUserEngagementStats: Future[UserEngagementStats] = {
val query = for {
totalUsers <- users.length.result
totalPosts <- posts.length.result
avgPostsPerUser <- posts.groupBy(_.userId).map(_._2.length).avg.result
topUser <- users.join(posts).on(_.id === _.userId)
.groupBy(_._1.name)
.map { case (name, group) => (name, group.length) }
.sortBy(_._2.desc)
.take(1)
.result
.headOption
} yield UserEngagementStats(
totalUsers = totalUsers,
totalPosts = totalPosts,
avgPostsPerUser = avgPostsPerUser.getOrElse(0.0),
topUser = topUser.map(_._1),
topUserPostCount = topUser.map(_._2).getOrElse(0)
)
db.run(query)
}
}
// 統計データ用ケースクラス
case class UserEngagementStats(
totalUsers: Int,
totalPosts: Int,
avgPostsPerUser: Double,
topUser: Option[String],
topUserPostCount: Int
)
// 使用例
def demonstrateAdvancedOperations(): Unit = {
import scala.concurrent.duration._
// バッチユーザー作成
val newUsers = Seq(
("Eve", "[email protected]", 29),
("Frank", "[email protected]", 31),
("Grace", "[email protected]", 27)
)
val batchCreateFuture = AdvancedUserService.batchCreateUsers(newUsers)
batchCreateFuture.foreach { createdUsers =>
println(s"Batch created users: $createdUsers")
// 統計情報取得
AdvancedUserService.getUserEngagementStats.foreach { stats =>
println(s"Engagement stats: $stats")
}
}
// カスタムクエリ実行
AdvancedUserService.executeCustomQuery(1).foreach { results =>
println(s"Users with at least 1 post: $results")
}
}
エラーハンドリング
import scala.concurrent.Future
import scala.util.{Success, Failure}
import slick.jdbc.SQLActionBuilder
import java.sql.SQLException
// カスタム例外クラス
case class UserNotFoundException(userId: Long) extends RuntimeException(s"User not found: $userId")
case class DuplicateEmailException(email: String) extends RuntimeException(s"Email already exists: $email")
case class DatabaseOperationException(message: String, cause: Throwable) extends RuntimeException(message, cause)
object SafeUserService {
// 安全なユーザー作成
def createUserSafely(name: String, email: String, age: Int): Future[Either[String, User]] = {
// バリデーション
if (name.trim.isEmpty) {
return Future.successful(Left("Name cannot be empty"))
}
if (!email.contains("@")) {
return Future.successful(Left("Invalid email format"))
}
if (age < 0 || age > 150) {
return Future.successful(Left("Invalid age range"))
}
val action = for {
// 重複チェック
existingUser <- users.filter(_.email === email).result.headOption
result <- existingUser match {
case Some(_) =>
DBIO.failed(DuplicateEmailException(email))
case None =>
(users returning users.map(_.id)
into ((user, id) => user.copy(id = Some(id)))
) += User(None, name, email, age, LocalDateTime.now())
}
} yield result
db.run(action.transactionally)
.map(Right(_))
.recover {
case DuplicateEmailException(email) => Left(s"Email $email is already registered")
case ex: SQLException => Left(s"Database error: ${ex.getMessage}")
case ex: Exception => Left(s"Unexpected error: ${ex.getMessage}")
}
}
// 安全なユーザー取得
def getUserSafely(userId: Long): Future[Either[String, User]] = {
val query = users.filter(_.id === userId).result.headOption
db.run(query)
.map {
case Some(user) => Right(user)
case None => Left(s"User with ID $userId not found")
}
.recover {
case ex: SQLException => Left(s"Database error: ${ex.getMessage}")
case ex: Exception => Left(s"Unexpected error: ${ex.getMessage}")
}
}
// 安全なユーザー更新
def updateUserSafely(userId: Long, name: String, email: String, age: Int): Future[Either[String, User]] = {
// バリデーション
if (name.trim.isEmpty) {
return Future.successful(Left("Name cannot be empty"))
}
if (!email.contains("@")) {
return Future.successful(Left("Invalid email format"))
}
val action = for {
// ユーザー存在確認
existingUser <- users.filter(_.id === userId).result.headOption
result <- existingUser match {
case None =>
DBIO.failed(UserNotFoundException(userId))
case Some(user) =>
// メール重複チェック(自分以外)
for {
duplicateCheck <- users.filter(u => u.email === email && u.id =!= userId).result.headOption
updateResult <- duplicateCheck match {
case Some(_) =>
DBIO.failed(DuplicateEmailException(email))
case None =>
users.filter(_.id === userId)
.map(u => (u.name, u.email, u.age))
.update((name, email, age))
.flatMap(_ => users.filter(_.id === userId).result.head)
}
} yield updateResult
}
} yield result
db.run(action.transactionally)
.map(Right(_))
.recover {
case UserNotFoundException(id) => Left(s"User with ID $id not found")
case DuplicateEmailException(email) => Left(s"Email $email is already in use")
case ex: SQLException => Left(s"Database error: ${ex.getMessage}")
case ex: Exception => Left(s"Unexpected error: ${ex.getMessage}")
}
}
// 安全な削除(関連データチェック付き)
def deleteUserSafely(userId: Long): Future[Either[String, Boolean]] = {
val action = for {
// ユーザー存在確認
existingUser <- users.filter(_.id === userId).result.headOption
result <- existingUser match {
case None =>
DBIO.successful(false)
case Some(_) =>
// 関連投稿の確認
for {
postCount <- posts.filter(_.userId === userId).length.result
deleteResult <- if (postCount > 0) {
// 関連投稿も削除するか確認
for {
_ <- posts.filter(_.userId === userId).delete
deletedUsers <- users.filter(_.id === userId).delete
} yield deletedUsers > 0
} else {
users.filter(_.id === userId).delete.map(_ > 0)
}
} yield deleteResult
}
} yield result
db.run(action.transactionally)
.map(Right(_))
.recover {
case ex: SQLException => Left(s"Database error: ${ex.getMessage}")
case ex: Exception => Left(s"Unexpected error: ${ex.getMessage}")
}
}
// 一括操作の安全な実行
def batchOperationSafely[T](operations: Seq[DBIO[T]]): Future[Either[String, Seq[T]]] = {
val batchAction = DBIO.sequence(operations).transactionally
db.run(batchAction)
.map(Right(_))
.recover {
case ex: SQLException => Left(s"Batch operation failed: ${ex.getMessage}")
case ex: Exception => Left(s"Unexpected error in batch operation: ${ex.getMessage}")
}
}
// データベース接続テスト
def testConnection: Future[Either[String, String]] = {
val testQuery = sql"SELECT 1".as[Int]
db.run(testQuery)
.map(_ => Right("Database connection successful"))
.recover {
case ex: SQLException => Left(s"Database connection failed: ${ex.getMessage}")
case ex: Exception => Left(s"Connection test error: ${ex.getMessage}")
}
}
}
// エラーハンドリング使用例
def demonstrateErrorHandling(): Unit = {
// 安全なユーザー作成
SafeUserService.createUserSafely("John Doe", "[email protected]", 30).foreach {
case Right(user) =>
println(s"Successfully created user: $user")
// 安全な更新
SafeUserService.updateUserSafely(user.id.get, "John Smith", "[email protected]", 31).foreach {
case Right(updatedUser) => println(s"Successfully updated user: $updatedUser")
case Left(error) => println(s"Update failed: $error")
}
case Left(error) =>
println(s"User creation failed: $error")
}
// 存在しないユーザーの取得テスト
SafeUserService.getUserSafely(999L).foreach {
case Right(user) => println(s"Found user: $user")
case Left(error) => println(s"User not found: $error")
}
// 接続テスト
SafeUserService.testConnection.foreach {
case Right(message) => println(message)
case Left(error) => println(s"Connection test failed: $error")
}
}