Migration Notes (Upgrading to 0.3.x from 0.2.x)

パッケージ

パッケージ名の変更

0.2.x 0.3.x
ldbc-core ldbc-schema

新たなパッケージ

新たに2種類のパッケージが追加されました。

Module / Platform JVM Scala Native Scala.js
ldbc-connector
jdbc-connector

全てのパッケージ

Module / Platform JVM Scala Native Scala.js
ldbc-sql
ldbc-connector
jdbc-connector
ldbc-dsl
ldbc-query-builder
ldbc-schema
ldbc-schemaSpy
ldbc-codegen
ldbc-hikari
ldbc-plugin

機能変更

コネクタ切り替え機能

Scala MySQL コネクタに、JDBC と ldbc の接続切り替えのサポートが追加されました。

この変更により、開発者はプロジェクトの要件に応じて JDBC または ldbc ライブラリを使用したデータベース接続を柔軟に選択できるようになりました。これにより、開発者は異なるライブラリの機能を利用できるようになり、接続の設定や操作の柔軟性が向上します。

変更方法

まず、共通の依存関係を設定する。

libraryDependencies += "io.github.takapi327" %% "ldbc-dsl" % "0.3.0-beta8"

クロスプラットフォームプロジェクトでは(JVM、JS、ネイティブ)

libraryDependencies += "io.github.takapi327" %%% "ldbc-dsl" % "0.3.0-beta8"

使用される依存パッケージは、データベース接続が Java API を使用するコネクタを介して行われるか、または ldbc によって提供されるコネクタを介して行われるかによって異なります。

jdbcコネクタの使用

libraryDependencies += "io.github.takapi327" %% "jdbc-connector" % "0.3.0-beta8"

ldbcコネクタの使用

libraryDependencies += "io.github.takapi327" %% "ldbc-connector" % "0.3.0-beta8"

クロスプラットフォームプロジェクトでは(JVM、JS、ネイティブ)

libraryDependencies += "io.github.takapi327" %%% "ldbc-connector" % "0.3.0-beta8"

使用方法

jdbcコネクタの使用

val ds = new com.mysql.cj.jdbc.MysqlDataSource()
ds.setServerName("127.0.0.1")
ds.setPortNumber(13306)
ds.setDatabaseName("world")
ds.setUser("ldbc")
ds.setPassword("password")

val datasource = jdbc.connector.MysqlDataSource[IO](ds)

val connection: Resource[IO, Connection[IO]] =
  Resource.make(datasource.getConnection)(_.close())

ldbcコネクタの使用

val connection: Resource[IO, Connection[IO]] =
  ldbc.connector.Connection[IO](
    host     = "127.0.0.1",
    port     = 3306,
    user     = "ldbc",
    password = Some("password"),
    database = Some("ldbc"),
    ssl      = SSL.Trusted
  )

データベースへの接続処理は、それぞれの方法で確立されたコネクションを使って行うことができる。

val result: IO[(List[Int], Option[Int], Int)] = connection.use { conn =>
  (for
    result1 <- sql"SELECT 1".query[Int].to[List]
    result2 <- sql"SELECT 2".query[Int].to[Option]
    result3 <- sql"SELECT 3".query[Int].unsafe
  yield (result1, result2, result3)).readOnly(conn)
}

破壊的変更

プレーン・クエリ構築の拡張

プレーン・クエリを用いたデータベース接続メソッドによる検索対象の型の決定は、検索対象の型とそのフォーマット(リストまたはオプション)を一括して指定していた。

今回の修正ではこれを変更し、取得する型とその形式の指定を分離することで内部ロジックを共通化した。これにより、プレーン・クエリの構文はよりdoobieに近くなり、doobieのユーザは混乱することなく使用できるはずである。

before

sql"SELECT id, name, age FROM user".toList[(Long, String, Int)].readOnly(connection)
sql"SELECT id, name, age FROM user WHERE id = ${1L}".headOption[User].readOnly(connection)

after

sql"SELECT id, name, age FROM user".query[(Long, String, Int)].to[List].readOnly(connection)
sql"SELECT id, name, age FROM user WHERE id = ${1L}".query[User].to[Option].readOnly(connection)

AUTO INCREMENT値取得メソッド命名変更

更新 API で AUTO INCREMENT 列によって生成された値を変換する API updateReturningAutoGeneratedKey の名前が returning に変更されました。

これはMySQLの特徴で、MySQLはデータ挿入時にAUTO INCREMENTで生成された値を返しますが、他のRDBは動作が異なり、AUTO INCREMENTで生成された値以外の値を返すことがあります。 API 名は、将来の拡張を考慮して、限定的な API 名をより拡張しやすくするために早い段階で変更されました。

before

sql"INSERT INTO `table`(`id`, `c1`) VALUES ($None, ${ "column 1" })".updateReturningAutoGeneratedKey[Long]

after

sql"INSERT INTO `table`(`id`, `c1`) VALUES ($None, ${ "column 1" })".returning[Long]

クエリビルダーの構築方法

以前まではクエリビルダーはテーブルスキーマを構築しなければ使用することができませんでした。

今回の更新で、より簡易的にクエリビルダーを使用できるように変更を行いました。

before

まずモデルに対応したテーブルスキーマを作成し、

case class User(
  id: Long,
  name: String,
  age: Option[Int],
)

val userTable = Table[User]("user")(                 // CREATE TABLE `user` (
  column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY), //   `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
  column("name", VARCHAR(255)),                      //   `name` VARCHAR(255) NOT NULL,
  column("age", INT.UNSIGNED.DEFAULT(None)),         //   `age` INT unsigned DEFAULT NULL
)

次にテーブルスキーマを使用してTableQueryの構築を行います。

val tableQuery = TableQuery[IO, User](userTable)

最後にクエリ構築を行っていました。

val result: IO[List[User]] = connection.use { conn =>
  tableQuery.selectAll.toList[User].readOnly(conn)
  // "SELECT `id`, `name`, `age` FROM user"
}

after

今回の変更によって、モデルを構築し

import ldbc.query.builder.Table

case class User(
  id: Long,
  name: String,
  age: Option[Int],
) derives Table

次にTableを初期化を行います。

import ldbc.query.builder.Table

val userTable = Table[User]

最後にクエリ構築を行うことで利用可能となります。

val result: IO[List[User]] = connection.use { conn =>
  userTable.selectAll.query.to[List].readOnly(conn)
  // "SELECT `id`, `name`, `age` FROM user"
}

カスタムデータ型のサポート

ユーザー定義のデータ型を使用する際は、ResultSetReaderParameterを使用してカスタムデータ型をサポートしていました。

今回の更新で、ResultSetReaderParameterを使用してカスタムデータ型をサポートする方法が変更されました。

Encoder

クエリ文字列に動的に埋め込むために、ParameterからEncoderに変更。

これにより、ユーザはEffect Typeを受け取るための冗長な処理を記述する必要がなくなり、よりシンプルな実装とカスタムデータ型のパラメータとしての使用が可能になります。

enum Status(val code: Int, val name: String):
  case Active   extends Status(1, "Active")
  case InActive extends Status(2, "InActive")

Before

given Parameter[Status] with
  override def bind[F[_]](
    statement: PreparedStatement[F],
    index: Int,
    status: Status
  ): F[Unit] = statement.setInt(index, status.code)

After

given Encoder[Status] with
  override def encode(status: Status): Int = status.done

Encoderのエンコード処理では、PreparedStatementで扱えるScala型しか返すことができません。

現在、以下のタイプがサポートされている。

Scala Type Methods called in PreparedStatement
Boolean setBoolean
Byte setByte
Short setShort
Int setInt
Long setLong
Float setFloat
Double setDouble
BigDecimal setBigDecimal
String setString
Array[Byte] setBytes
java.time.LocalDate setDate
java.time.LocalTime setTime
java.time.LocalDateTime setTimestamp
None setNull

Decoder

ResultSetからデータを取得する処理をResultSetReaderからDecoderに変更。

これにより、ユーザーは取得したレコードをネストした階層データに変換できる。

case class City(id: Int, name: String, countryCode: String)
case class Country(code: String, name: String)
case class CityWithCountry(city: City, country: Country)

sql"SELECT city.Id, city.Name, city.CountryCode, country.Code, country.Name FROM city JOIN country ON city.CountryCode = country.Code".query[CityWithCountry]

Using Query Builder

case class City(id: Int, name: String, countryCode: String) derives Table
case class Country(code: String, name: String) derives Table

val city = Table[City]
val country = Table[Country]

city.join(country).join((city, country) => city.countryCode === country.code)
  .select((city, country) => (city.name, country.name))
  .query // (String, String)
  .to[Option]
  

city.join(country).join((city, country) => city.countryCode === country.code)
  .selectAll
  .query // (City, Country)
  .to[Option]