コンテンツにスキップ

Chisel

Chiselは、Scala 3で書かれたPetraDB用の型付きデータアクセスレイヤーです。ValueをScala型にマッピングし、文字列インターポレーターでインジェクション安全なSQLを構築し、テーブルのCRUDを生成します — フルORMの重さ(アイデンティティマップ、ユニットオブワーク、遅延プロキシなし)を伴いません。実行時リフレクションではなくコンパイル時導出を使用し、PetraDBが動作するあらゆる場所で動作します: JVM、Node.js(Scala.js)、Native。

Chiselはバインドパラメーターを介してエンジンのSessionと直接やり取りするため、JDBCを必要とせず、SQLテキストに値をレンダリングしません。

libraryDependencies += "io.github.edadma" %%% "petradb-chisel" % "1.5.1"

Chiselはセッションのためにエンジンに依存します。

libraryDependencies += "io.github.edadma" %%% "petradb-engine" % "1.5.5"

すべてはSessionに対して実行され、ExecutionContextを必要とします。Chiselの操作はFutureを返します。

import io.github.edadma.petradb.Session
import io.github.edadma.petradb.engine.MemoryDB
import io.github.edadma.petradb.chisel.*
import scala.concurrent.ExecutionContext.Implicits.global
given Session = new MemoryDB().connect()

セッションはすべてのChisel操作に暗黙的に渡されるため、スコープ内のgiven Sessionだけが必要です。

Chiselは、4つの型クラスを介してScalaの値とPetraDBのValueの間をマッピングします。

型クラス方向役割
Get[A]Value => A1カラムをデコード
Put[A]A => Value1値をエンコード(パラメーターをバインド)
Read[A]Row => A行全体をデコード
Write[A]A => Seq[(String, Value)]行全体をエンコード

一般的なスカラー型にはGetPutのインスタンスが提供されています。

Scala型PetraDB型
Int, Long, Short, Byte整数ファミリー
Double, Float浮動小数点
BigDecimalNUMERIC
StringTEXT
BooleanBOOLEAN
java.time.LocalDateDATE
java.time.LocalTimeTIME
java.time.LocalDateTimeTIMESTAMP
java.time.OffsetDateTimeTIMESTAMPTZ
java.time.OffsetTimeTIMETZ
java.time.DurationINTERVAL
Array[Byte]BYTEA
java.util.UUIDUUID
Option[A]NULLable ANone ⇄ SQLのNULL

型が一致しないカラムはDecodeExceptionを発生させます。

行全体をマッピングするには、ケースクラスに対してReadWriteを導出します。導出はフィールド名によって行われるため、カラムの並び替えに対して堅牢で、SELECT *と直接連携します。

case class User(id: Long, name: String, age: Int) derives Read, Write

OptionフィールドはNULLableカラムにマッピングされます。

case class Account(id: Long, nickname: Option[String]) derives Read, Write

名前付きタプルを含む任意のプロダクトが導出されます — クラスを宣言せずにアドホックなプロジェクションを行うのに便利です。

val r = Read.derived[(id: Long, name: String)]

Get/Readには.mapがあり、Put/Writeには.contramapがあり、Chiselが知らない型に適応させます。

enum Color:
case Red, Green, Blue
given Get[Color] = Get[String].map(Color.valueOf)
given Put[Color] = Put[String].contramap[Color](_.toString)

sql"…"Fragmentを構築します — SQLテキストとバインドされたパラメーターです。各インターポレートされた引数はそのPutを介してエンコードされ、パラメーターとしてバインドされるため、値がSQLテキストになることはなく、インジェクションは不可能です。

val minAge = 18
val frag = sql"select id, name, age from users where age >= $minAge"

Fragmentはエンジンの$1$2、…というプレースホルダー形式にレンダリングされます。

frag.sql // "select id, name, age from users where age >= $1"
frag.params // Seq(NumberValue(18))

Optionと事前構築されたValueもインターポレートできます — NoneはSQLのNULLをバインドします。

フラグメントはレンダリングされない状態で保存されるため、++で合成され、プレースホルダーが自動的に再番号付けされます。Fragment.constは生のテキスト(テーブル名のような信頼できる識別子用)を提供し、Fragment.paramは単一のバインド値を提供します。

val table = "users"
val q = sql"select * from " ++ Fragment.const(table) ++ sql" where age >= $minAge"

.query[A]で行デコーダーをアタッチし、結果の収集方法を選択します。

val users: Future[List[User]] =
sql"select * from users order by id".query[User].toList
val one: Future[Option[User]] =
sql"select * from users where id = ${1L}".query[User].option
val exactlyOne: Future[User] =
sql"select * from users where id = ${1L}".query[User].unique
メソッド結果備考
.toListFuture[List[A]]すべての行
.toVectorFuture[Vector[A]]すべての行
.optionFuture[Option[A]]最初の行またはNone
.uniqueFuture[A]ちょうど1行; それ以外は失敗

INSERT … RETURNING.queryを介してデコードします。

val inserted: Future[User] =
sql"insert into users (name, age) values (${"alice"}, ${30}) returning *"
.query[User].unique

queryValueで最初の行の最初のカラムを直接読み取ります — count(*)existsmaxなどに使用します。

val total: Future[Long] = sql"select count(*) from users".queryValue[Long]
val maybeMax: Future[Option[Int]] = sql"select max(age) from users".queryValueOption[Int]

クエリ以外のSQLには、.updateが影響を受けた行数を返し、.runが生のエンジン結果を返します。

val changed: Future[Int] =
sql"update users set age = ${31} where id = ${1L}".update
val raw: Future[Seq[Result]] =
sql"create table users (id serial primary key, name text, age integer)".run

Repo[A, Id]は、エンティティのRead/Writeインスタンスから単一テーブルのCRUDを生成します。カバーされていないものにはsql"…"を使用します。

case class User(id: Long, name: String, age: Int) derives Read, Write
val users = Repo[User, Long]("users") // idColumn = "id", generatedId = true

デフォルトではRepoはデータベース生成のキー(SERIAL)を想定するため、insert/insertReturningはidカラムを省略し、データベースがそれを割り当てます。エンティティを構築する際にプレースホルダーidを渡します — それはinsert時に無視されます。

val saved: Future[User] = users.insertReturning(User(0, "alice", 30))
// saved.id は生成されたキーです
users.findById(1L) // Future[Option[User]]
users.findAll // Future[List[User]]
users.count // Future[Long]
users.existsById(1L) // Future[Boolean]
users.insert(User(0, "bob", 25)) // Future[Int] — 挿入された行数
users.insertReturning(User(0, "eve", 22)) // Future[User] — 生成されたidを復元
users.update(saved.copy(age = 31)) // Future[Int] — idが一致する箇所のid以外の全カラムを設定
users.deleteById(1L) // Future[Int]
users.deleteAll // Future[Int]

主キーがアプリケーションによって提供される(生成されない)テーブルの場合、generatedId = falseを設定し、idカラムが"id"でない場合は名前を指定します。

case class Widget(sku: String, label: String) derives Read, Write
val widgets = Repo[Widget, String]("widgets", idColumn = "sku", generatedId = false)
widgets.insert(Widget("w-1", "Sprocket"))
widgets.findById("w-1")

クロスプラットフォームに関する注意

Section titled “クロスプラットフォームに関する注意”

Chiselは、単一のソースからJVM、Scala.js、Scala Native向けに公開されています。行のデコードはScala 3のインラインMirror導出を使用するため、どのプラットフォームでも実行時リフレクションはありません。

summon[Session].close()