sqlpp11
sqlpp11は、C++11テンプレート技術をScalaに移植した型安全SQLライブラリです。コンパイル時SQL検証と高度な型システムによる安全なデータベースアクセスを実現し、SQLクエリの型安全性を保証しながら、自然なSQL構文での記述を可能にします。
GitHub概要
スター2,564
ウォッチ109
フォーク349
作成日:2013年8月13日
言語:C++
ライセンス:BSD 2-Clause "Simplified" License
トピックス
なし
スター履歴
データ取得日時: 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 >= ?
// パラメータは自動的にバインド
}
}