Slick

Slick(Scala Language-Integrated Connection Kit)は「Scalaのための関数型リレーショナルマッピング(FRM)ライブラリ」として、型安全で関数型プログラミングパラダイムに基づくデータベースアクセス層を提供します。従来のORMとは異なり、SQLライクなクエリを直接Scalaコード内で記述でき、コンパイル時の型チェックによりクエリの正確性を保証します。Reactive Streamsサポート、非同期処理、関数合成によるクエリ構築により、モダンなScalaアプリケーション開発において高いパフォーマンスと保守性を実現します。

FRMScalaデータベースtype-safe関数型アクセス層

GitHub概要

slick/slick

Slick (Scala Language Integrated Connection Kit) is a modern database query and access library for Scala

スター2,663
ウォッチ139
フォーク617
作成日:2009年2月20日
言語:Scala
ライセンス:BSD 2-Clause "Simplified" License

トピックス

databasescalasql

スター履歴

slick/slick Star History
データ取得日時: 2025/7/17 06:59

ライブラリ

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")
  }
}