スキーマコード生成

この章では、LDBCのテーブル定義をSQLファイルから自動生成する方法について説明します。

プロジェクトに以下の依存関係を設定する必要があります。

addSbtPlugin("io.github.takapi327" % "ldbc-plugin" % "0.3.0-beta8")

生成

プロジェクトに対してプラグインを有効にします。

lazy val root = (project in file("."))
  .enablePlugins(Ldbc)

解析対象のSQLファイルを配列で指定します。

Compile / parseFiles := List(baseDirectory.value / "test.sql")

プラグインを有効にすることで設定できるキーの一覧

キー 詳細
parseFiles 解析対象のSQLファイルのリスト
parseDirectories 解析対象のSQLファイルをディレクトリ単位で指定する
excludeFiles 解析から除外するファイル名のリスト
customYamlFiles Scala型やカラムのデータ型をカスタマイズするためのyamlファイルのリスト
classNameFormat クラス名の書式を指定する値
propertyNameFormat Scalaモデルのプロパティ名の形式を指定する値
ldbcPackage 生成されるファイルのパッケージ名を指定する値

解析対象のSQLファイルの先頭には必ずデータベースのCreate文もしくはUse文を定義する必要があります。LDBCはファイルの解析を1ファイルずつ行い、テーブル定義を生成しデータベースモデルにテーブルのリストを格納させます。 そのためテーブルがどのデータベースに所属しているかを教えてあげる必要があるからです。

CREATE DATABASE `location`;

USE `location`;

DROP TABLE IF EXISTS `country`;
CREATE TABLE country (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `name` VARCHAR(255) NOT NULL,
  `code` INT NOT NULL
);

解析対象のSQLファイルにはデータベースのCreate/Use文もしくはテーブル定義のCreate/Drop文のみ記載するようにしなければいけません。

生成コード

sbtプロジェクトを起動してコンパイルを実行すると、解析対象のSQLファイルを元に生成されたモデルクラスと、テーブル定義がsbtプロジェクトのtarget配下に生成されます。

sbt compile

上記SQLファイルから生成されるコードは以下のようなものになります。

package ldbc.generated.location

import ldbc.core.*

case class Country(
  id: Long,
  name: String,
  code: Int
)

object Country:
  val table = Table[Country]("country")(
    column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY),
    column("name", VARCHAR(255)),
    column("code", INT)
  )

Compileでコードを生成した場合、その生成されたファイルはキャッシュされるので、SQLファイルを変更していない場合再度生成されることはありません。SQLファイルを変更した場合もしくは、cleanコマンドを実行してキャッシュを削除した場合はCompileを実行すると再度コードが生成されます。 キャッシュを利用せず再度コード生成を行いたい場合は、generateBySchemaコマンドを実行してください。このコマンドはキャッシュを使用せず常にコード生成を行います。

sbt generateBySchema

カスタマイズ

SQLファイルから生成されるコードの型を別のものに変換したい時があるかもしれません。その場合はcustomYamlFilesにカスタマイズを行うymlファイルを渡してあげることで行うことができます。

Compile / customYamlFiles := List(
  baseDirectory.value / "custom.yml"
)

ymlファイルの形式は以下のようなものである必要があります。

database:
  name: '{データベース名}'
  tables:
    - name: '{テーブル名}'
      columns: # Optional
        - name: '{カラム名}'
          type: '{変更したいScalaの型}'
      class: # Optional
        extends:
          - '{モデルクラスに継承させたいtraitなどのpackageパス}' // package.trait.name
      object: # Optional
        extends:
          - '{オブジェクトに継承させたいtraitなどのpackageパス}'
    - name: '{テーブル名}'
      ...

databaseは解析対象のSQLファイルに記載されているデータベース名である必要があります。またテーブル名は解析対象のSQLファイルに記載されているデータベースに所属しているテーブル名である必要があります。

columnsには型を変更したいカラム名と変更したいScalaの型を文字列で記載を行います。columnsには複数の値を設定できますが、nameに記載されたカラム名が対象のテーブルに含まれいてなければなりません。 また、変換を行うScalaの型はカラムのData型がサポートしている型である必要があります。もしサポート対象外の型を指定したい場合は、objectに対して暗黙の型変換を行う設定を持ったtraitやabstract classなどを渡してあげる必要があります。

Data型がサポートしている型に関してはこちらを、サポート対象外の型を設定する方法はこちらを参照してください。

Int型をユーザー独自の型であるCountryCodeに変換する場合は、以下のようなCustomMappingtraitを実装します。

trait CountryCode:
  val code: Int
object Japan extends CountryCode:
  override val code: Int = 1

trait CustomMapping: // 任意の名前
  given Conversion[INT[Int], CountryCode] = DataType.mappingp[INT[Int], CountryCode]

カスタマイズを行うためのymlファイルに実装を行なったCustomMappingtraitを設定し、対象のカラムの型をCountryCodeに変換してあげます。

database:
  name: 'location'
  tables:
    - name: 'country'
      columns:
        - name: 'code'
          type: 'Country.CountryCode' // CustomMappingをCountryオブジェクトにミックスインさせるのでそこから取得できるように記載
      object:
        extends:
          - '{package.name.}CustomMapping'

上記設定で生成されるコードは以下のようになり、ユーザー独自の型でモデルとテーブル定義を生成できるようになります。

case class Country(
  id: Long,
  name: String,
  code: Country.CountryCode
)

object Country extends /*{package.name.}*/CustomMapping:
  val table = Table[Country]("country")(
    column("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY),
    column("name", VARCHAR(255)),
    column("code", INT)
  )

データベースモデルに関してもSQLファイルから自動生成が行われています。

package ldbc.generated.location

import ldbc.core.*

case class LocationDatabase(
  schemaMeta: Option[String] = None,
  catalog: Option[String] = Some("def"),
  host: String = "127.0.0.1",
  port: Int = 3306
) extends Database:

  override val databaseType: Database.Type = Database.Type.MySQL

  override val name: String = "location"

  override val schema: String = "location"

  override val character: Option[Character] = None

  override val collate: Option[Collate] = None

  override val tables = Set(
    Country.table
  )