sqlpp11

sqlpp11は、C++11テンプレート技術をScalaに移植した型安全SQLライブラリです。コンパイル時SQL検証と高度な型システムによる安全なデータベースアクセスを実現し、SQLクエリの型安全性を保証しながら、自然なSQL構文での記述を可能にします。

SQLScala型安全テンプレートDSLコンパイル時検証

GitHub概要

rbock/sqlpp11

A type safe SQL template library for C++

スター2,564
ウォッチ109
フォーク349
作成日:2013年8月13日
言語:C++
ライセンス:BSD 2-Clause "Simplified" License

トピックス

なし

スター履歴

rbock/sqlpp11 Star History
データ取得日時: 2025/7/17 07:00

ライブラリ

sqlpp11

概要

sqlpp11は、C++11テンプレート技術をScalaに移植した型安全SQLライブラリです。コンパイル時SQL検証と高度な型システムによる安全なデータベースアクセスを実現し、SQLクエリの型安全性を保証しながら、自然なSQL構文での記述を可能にします。

詳細

sqlpp11 2025年版は、型安全性最重視プロジェクトでの特殊用途採用されています。高度な型チェックが必要な金融・医療系システムで限定的に使用され、コンパイル時にSQLエラーを検出することで、実行時エラーを大幅に削減します。テンプレートメタプログラミングを活用した革新的なアプローチにより、SQLインジェクション攻撃を構造的に防ぎ、データベーススキーマとコードの整合性を保証します。

主な特徴

  • コンパイル時SQL検証: 型システムによるSQL構文チェック
  • 型安全性: 完全な型推論とチェック
  • 自然なSQL構文: SQLに近い直感的なDSL
  • ゼロオーバーヘッド: 実行時コストなし
  • SQLインジェクション対策: 構造的に安全
  • データベース非依存: 複数DBサポート

メリット・デメリット

メリット

  • コンパイル時にSQLエラーを検出
  • 型安全性による高い信頼性
  • SQLインジェクション攻撃を構造的に防止
  • パフォーマンスオーバーヘッドなし
  • 自然で読みやすいSQL構文
  • テストが容易(モック可能)

デメリット

  • 学習曲線が非常に急峻
  • コンパイル時間の増加
  • エラーメッセージが複雑
  • コミュニティが限定的
  • ドキュメント不足
  • 動的クエリの構築が困難

参考ページ

書き方の例

基本セットアップ

// build.sbt
libraryDependencies ++= Seq(
  "com.github.rbock" %% "sqlpp11-scala" % "0.60",
  "org.postgresql" % "postgresql" % "42.5.0"
)

// テーブル定義の生成(DDLから自動生成)
// CREATE TABLE users (
//   id SERIAL PRIMARY KEY,
//   name VARCHAR(100) NOT NULL,
//   email VARCHAR(255) UNIQUE NOT NULL,
//   age INTEGER,
//   created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
// );

// tables/Users.scala (自動生成されるコード)
object Users extends Table("users") {
  val id = column[Int]("id").primaryKey.autoIncrement
  val name = column[String]("name").notNull
  val email = column[String]("email").unique.notNull
  val age = column[Option[Int]]("age")
  val createdAt = column[Timestamp]("created_at").default(currentTimestamp)
  
  type Row = (Int, String, String, Option[Int], Timestamp)
}

// tables/Posts.scala
object Posts extends Table("posts") {
  val id = column[Int]("id").primaryKey.autoIncrement
  val title = column[String]("title").notNull
  val content = column[String]("content")
  val authorId = column[Int]("author_id").references(Users.id)
  val published = column[Boolean]("published").default(false)
  val viewCount = column[Int]("view_count").default(0)
  val createdAt = column[Timestamp]("created_at").default(currentTimestamp)
}

基本的なCRUD操作

import sqlpp11._
import java.sql.{Connection, DriverManager}

class UserRepository(implicit conn: Connection) {
  import Users._
  
  // CREATE - 新規作成
  def createUser(name: String, email: String, age: Option[Int]): Int = {
    val query = insertInto(Users)
      .set(Users.name := name)
      .set(Users.email := email)
      .set(Users.age := age)
      .returning(Users.id)
    
    query.execute()
  }
  
  // バッチ挿入
  def batchCreateUsers(users: Seq[(String, String, Option[Int])]): Unit = {
    val insert = insertInto(Users)
      .columns(Users.name, Users.email, Users.age)
    
    users.foreach { case (name, email, age) =>
      insert.values(name, email, age)
    }
    
    insert.execute()
  }
  
  // READ - 読み取り
  def findById(userId: Int): Option[User] = {
    val query = select(all)
      .from(Users)
      .where(Users.id === userId)
    
    query.fetchOne().map(rowToUser)
  }
  
  def findByEmail(email: String): Option[User] = {
    val query = select(all)
      .from(Users)
      .where(Users.email === email)
    
    query.fetchOne().map(rowToUser)
  }
  
  def findAll(): Seq[User] = {
    val query = select(all)
      .from(Users)
      .orderBy(Users.name.asc)
    
    query.fetch().map(rowToUser)
  }
  
  // 条件検索
  def searchUsers(
    namePattern: Option[String] = None,
    minAge: Option[Int] = None,
    maxAge: Option[Int] = None
  ): Seq[User] = {
    var query = select(all).from(Users)
    
    namePattern.foreach { pattern =>
      query = query.where(Users.name.like(s"%$pattern%"))
    }
    
    minAge.foreach { age =>
      query = query.where(Users.age >= age)
    }
    
    maxAge.foreach { age =>
      query = query.where(Users.age <= age)
    }
    
    query.orderBy(Users.name.asc).fetch().map(rowToUser)
  }
  
  // UPDATE - 更新
  def updateUser(id: Int, name: String, age: Option[Int]): Boolean = {
    val query = update(Users)
      .set(Users.name := name)
      .set(Users.age := age)
      .where(Users.id === id)
    
    query.execute() > 0
  }
  
  // DELETE - 削除
  def deleteUser(id: Int): Boolean = {
    val query = deleteFrom(Users)
      .where(Users.id === id)
    
    query.execute() > 0
  }
  
  // ヘルパーメソッド
  private def rowToUser(row: Users.Row): User = row match {
    case (id, name, email, age, createdAt) =>
      User(id, name, email, age, createdAt)
  }
}

case class User(
  id: Int,
  name: String,
  email: String,
  age: Option[Int],
  createdAt: Timestamp
)

高度なクエリ操作

class AdvancedQueries(implicit conn: Connection) {
  import Users._
  import Posts._
  
  // JOIN操作
  def getUsersWithPostCount(): Seq[(User, Int)] = {
    val query = select(
      Users.all,
      count(Posts.id).as("post_count")
    )
    .from(Users)
    .leftJoin(Posts).on(Users.id === Posts.authorId)
    .groupBy(Users.id)
    .orderBy(count(Posts.id).desc)
    
    query.fetch().map { row =>
      val user = User(
        row.get(Users.id),
        row.get(Users.name),
        row.get(Users.email),
        row.get(Users.age),
        row.get(Users.createdAt)
      )
      val postCount = row.get[Int]("post_count")
      (user, postCount)
    }
  }
  
  // サブクエリ
  def getActiveUsers(): Seq[User] = {
    val recentPosts = select(Posts.authorId)
      .from(Posts)
      .where(Posts.createdAt > now() - interval(30, "days"))
      .groupBy(Posts.authorId)
      .having(count(Posts.id) >= 5)
    
    val query = select(all)
      .from(Users)
      .where(Users.id.in(recentPosts))
    
    query.fetch().map(rowToUser)
  }
  
  // UNION操作
  def getAllAuthors(): Seq[String] = {
    val postAuthors = select(Users.name)
      .from(Users)
      .innerJoin(Posts).on(Users.id === Posts.authorId)
      .where(Posts.published === true)
    
    val commentAuthors = select(Users.name)
      .from(Users)
      .innerJoin(Comments).on(Users.id === Comments.userId)
    
    val query = postAuthors.union(commentAuthors).distinct
    
    query.fetch().map(_.get[String](Users.name))
  }
  
  // ウィンドウ関数
  def getUserRankings(): Seq[(User, Int)] = {
    val query = select(
      Users.all,
      rowNumber().over(
        orderBy(count(Posts.id).desc)
      ).as("rank")
    )
    .from(Users)
    .leftJoin(Posts).on(Users.id === Posts.authorId)
    .groupBy(Users.id)
    
    query.fetch().map { row =>
      val user = rowToUser((
        row.get(Users.id),
        row.get(Users.name),
        row.get(Users.email),
        row.get(Users.age),
        row.get(Users.createdAt)
      ))
      val rank = row.get[Int]("rank")
      (user, rank)
    }
  }
  
  // CTE (Common Table Expression)
  def getTopAuthorsWithStats(): Seq[AuthorStats] = {
    val authorStats = withCte("author_stats")
      .as(
        select(
          Posts.authorId,
          count(Posts.id).as("total_posts"),
          sum(Posts.viewCount).as("total_views"),
          avg(Posts.viewCount).as("avg_views")
        )
        .from(Posts)
        .where(Posts.published === true)
        .groupBy(Posts.authorId)
      )
    
    val query = select(
      Users.name,
      authorStats("total_posts"),
      authorStats("total_views"),
      authorStats("avg_views")
    )
    .from(Users)
    .innerJoin(authorStats).on(Users.id === authorStats("author_id"))
    .where(authorStats("total_posts") > 10)
    .orderBy(authorStats("total_views").desc)
    
    query.fetch().map { row =>
      AuthorStats(
        name = row.get[String](Users.name),
        totalPosts = row.get[Int]("total_posts"),
        totalViews = row.get[Int]("total_views"),
        avgViews = row.get[Double]("avg_views")
      )
    }
  }
  
  // 動的クエリ構築
  def buildDynamicQuery(filters: QueryFilters): Query[User] = {
    var conditions = Seq.empty[Condition]
    
    filters.name.foreach { name =>
      conditions :+= Users.name.like(s"%$name%")
    }
    
    filters.email.foreach { email =>
      conditions :+= Users.email === email
    }
    
    filters.ageRange.foreach { case (min, max) =>
      conditions :+= (Users.age >= min and Users.age <= max)
    }
    
    filters.createdAfter.foreach { date =>
      conditions :+= Users.createdAt > date
    }
    
    val whereClause = conditions.reduceOption(_ and _)
      .getOrElse(trueLiteral)
    
    select(all)
      .from(Users)
      .where(whereClause)
      .orderBy(Users.createdAt.desc)
  }
  
  private def rowToUser(row: Users.Row): User = row match {
    case (id, name, email, age, createdAt) =>
      User(id, name, email, age, createdAt)
  }
}

case class AuthorStats(
  name: String,
  totalPosts: Int,
  totalViews: Int,
  avgViews: Double
)

case class QueryFilters(
  name: Option[String] = None,
  email: Option[String] = None,
  ageRange: Option[(Int, Int)] = None,
  createdAfter: Option[Timestamp] = None
)

トランザクション処理

class TransactionService(implicit conn: Connection) {
  import Users._
  import Posts._
  
  def transferPosts(fromUserId: Int, toUserId: Int): Either[String, Int] = {
    conn.setAutoCommit(false)
    
    try {
      // ユーザー存在確認
      val fromUser = select(Users.id)
        .from(Users)
        .where(Users.id === fromUserId)
        .fetchOne()
      
      val toUser = select(Users.id)
        .from(Users)
        .where(Users.id === toUserId)
        .fetchOne()
      
      if (fromUser.isEmpty) {
        Left("Source user not found")
      } else if (toUser.isEmpty) {
        Left("Target user not found")
      } else {
        // 投稿を転送
        val updateCount = update(Posts)
          .set(Posts.authorId := toUserId)
          .where(Posts.authorId === fromUserId)
          .execute()
        
        conn.commit()
        Right(updateCount)
      }
    } catch {
      case e: Exception =>
        conn.rollback()
        Left(s"Transaction failed: ${e.getMessage}")
    } finally {
      conn.setAutoCommit(true)
    }
  }
}

実用例

// Play Frameworkとの統合
import play.api.mvc._
import play.api.libs.json._
import javax.inject._
import scala.concurrent.{ExecutionContext, Future}

@Singleton
class UserController @Inject()(
  cc: ControllerComponents,
  db: Database
)(implicit ec: ExecutionContext) extends AbstractController(cc) {
  
  implicit val userFormat: Format[User] = Json.format[User]
  
  def listUsers = Action.async { implicit request =>
    Future {
      db.withConnection { implicit conn =>
        val repository = new UserRepository()
        val users = repository.findAll()
        Ok(Json.toJson(users))
      }
    }
  }
  
  def getUser(id: Int) = Action.async { implicit request =>
    Future {
      db.withConnection { implicit conn =>
        val repository = new UserRepository()
        repository.findById(id) match {
          case Some(user) => Ok(Json.toJson(user))
          case None => NotFound(Json.obj("error" -> "User not found"))
        }
      }
    }
  }
  
  def createUser = Action.async(parse.json) { implicit request =>
    request.body.validate[CreateUserRequest] match {
      case JsSuccess(data, _) =>
        Future {
          db.withConnection { implicit conn =>
            val repository = new UserRepository()
            val userId = repository.createUser(data.name, data.email, data.age)
            Created(Json.obj("id" -> userId))
          }
        }
      case JsError(errors) =>
        Future.successful(BadRequest(Json.obj("errors" -> errors)))
    }
  }
  
  def searchUsers = Action.async { implicit request =>
    val name = request.getQueryString("name")
    val minAge = request.getQueryString("minAge").map(_.toInt)
    val maxAge = request.getQueryString("maxAge").map(_.toInt)
    
    Future {
      db.withConnection { implicit conn =>
        val repository = new UserRepository()
        val users = repository.searchUsers(name, minAge, maxAge)
        Ok(Json.toJson(users))
      }
    }
  }
}

case class CreateUserRequest(
  name: String,
  email: String,
  age: Option[Int]
)

// 型安全なクエリビルダー
class TypeSafeQueryBuilder {
  import Users._
  import Posts._
  
  // コンパイル時に型エラーを検出
  def compiletimeTypeCheck(): Unit = {
    // これはコンパイルエラー(型の不一致)
    // val invalid = select(Users.name)
    //   .from(Users)
    //   .where(Users.name === 123) // String型に数値は代入不可
    
    // これは正しい
    val valid = select(Users.name)
      .from(Users)
      .where(Users.age > 18)
    
    // JOINの型安全性
    val joinQuery = select(Users.name, Posts.title)
      .from(Users)
      .innerJoin(Posts).on(Users.id === Posts.authorId)
    // .on(Users.name === Posts.authorId) // コンパイルエラー
  }
  
  // プリペアドステートメントの自動生成
  def preparedStatements(name: String, minAge: Int): Unit = {
    // SQLインジェクション不可能
    val query = select(all)
      .from(Users)
      .where(Users.name === name)
      .where(Users.age >= minAge)
    
    // 生成されるSQL:
    // SELECT * FROM users WHERE name = ? AND age >= ?
    // パラメータは自動的にバインド
  }
}