スキーマ
クエリビルダーで型安全にクエリを構築する方法を学びました。このページでは、Scalaコードでデータベーススキーマを定義し、テーブルとモデルのマッピングを行う方法を説明します。
スキーマ定義は、アプリケーションとデータベースの間の境界を明確にする重要な要素です。ldbcは、Scalaコードでスキーマを定義し、強力な型システムを活用してデータベースの構造を表現するための機能を提供しています。
この章では、Scala コードでデータベーススキーマを扱う方法、特に既存のデータベースなしでアプリケーションを書き始めるときに便利な、手動でスキーマを記述する方法について説明します。すでにデータベースにスキーマがある場合は、Code Generatorを使ってこの作業を省略することもできます。
準備
プロジェクトに以下の依存関係を設定する必要があります。
//> using dep "io.github.takapi327::ldbc-schema:0.3.0-beta11"
以下のコード例では、以下のimportを想定しています。
import ldbc.schema.*
テーブル定義の基本
ldbcでは、Table
クラスを継承してテーブル定義を作成します。これにより、Scalaのモデル(ケースクラスなど)とデータベースのテーブルを関連付けることができます。
基本的なテーブル定義
// モデル定義
case class User(
id: Long,
name: String,
age: Option[Int] // NULLを許容するカラムの場合はOptionを使用
)
// テーブル定義
class UserTable extends Table[User]("user"): // "user"はテーブル名
// カラム定義
def id: Column[Long] = column[Long]("id")
def name: Column[String] = column[String]("name")
def age: Column[Option[Int]] = column[Option[Int]]("age")
// モデルとのマッピング
override def * : Column[User] = (id *: name *: age).to[User]
上記の例では:
Table[User]
はこのテーブルがUserモデルと関連付けられることを示します"user"
はデータベース上のテーブル名です- 各カラムは
column
メソッドで定義します *
メソッドはテーブルの全カラムとモデルのマッピング方法を定義します
データ型を指定したテーブル定義
カラムにはMySQLのデータ型や属性を指定できます:
class UserTable extends Table[User]("user"):
// データ型や属性を指定したカラム定義
def id: Column[Long] = column[Long]("id", BIGINT, AUTO_INCREMENT, PRIMARY_KEY)
def name: Column[String] = column[String]("name", VARCHAR(255))
def age: Column[Option[Int]] = column[Option[Int]]("age", INT)
override def * : Column[User] = (id *: name *: age).to[User]
専用カラム定義メソッドを使用する方法
ldbcは、各データ型に特化したカラム定義メソッドも提供しています。変数名がそのままカラム名として使用されるため、よりシンプルにコードを書くことができます。
class UserTable extends Table[User]("user"):
def id: Column[Long] = bigint().autoIncrement.primaryKey
def name: Column[String] = varchar(255)
def age: Column[Option[Int]] = int().defaultNull
override def * : Column[User] = (id *: name *: age).to[User]
専用のカラム定義メソッドを使うと、そのデータ型に適した属性を設定できるようになり、よりタイプセーフなコードを書くことができます。
カラム名を明示的に指定
カラム名を明示的に指定したい場合は以下のようにします:
class UserTable extends Table[User]("user"):
def id: Column[Long] = bigint("user_id").autoIncrement.primaryKey
def name: Column[String] = varchar("user_name", 255)
def age: Column[Option[Int]] = int("user_age").defaultNull
override def * : Column[User] = (id *: name *: age).to[User]
カラム命名規則の設定
Naming
を使用して、カラムの命名規則を変更できます:
class UserTable extends Table[User]("user"):
// パスカルケースのカラム名に変換(例:userId → UserId)
given Naming = Naming.PASCAL
def userId: Column[Long] = bigint().autoIncrement.primaryKey
def userName: Column[String] = varchar(255)
def userAge: Column[Option[Int]] = int().defaultNull
override def * : Column[User] = (userId *: userName *: userAge).to[User]
利用可能な命名規則:
Naming.SNAKE
(デフォルト): スネークケース(例:user_id)Naming.CAMEL
: キャメルケース(例:userId)Naming.PASCAL
: パスカルケース(例:UserId)
数値型カラムの定義
数値型カラムでは、以下のような操作が可能です:
整数型
def id: Column[Long] = bigint().autoIncrement.primaryKey
def count: Column[Int] = int().unsigned.default(0) // 符号なしに設定、デフォルト値0
def smallValue: Column[Short] = smallint().unsigned
小数点型
def price: Column[BigDecimal] = decimal(10, 2) // 合計10桁、小数点以下2桁
def rating: Column[Double] = double(5) // 倍精度浮動小数点数
def score: Column[Float] = float(4) // 単精度浮動小数点数
文字列型カラムの定義
文字列型カラムでは、以下のような操作が可能です:
def name: Column[String] = varchar(255) // 可変長文字列(最大255文字)
def code: Column[String] = char(5) // 固定長文字列(5文字)
def description: Column[String] = text() // テキスト型
def content: Column[String] = longtext() // 長いテキスト型
// 文字セット(キャラクターセット)の指定
def japaneseText: Column[String] = text().charset(Character.utf8mb4)
// 照合順序(コレーション)の指定
def sortableText: Column[String] = varchar(255)
.charset(Character.utf8mb4)
.collate(Collate.utf8mb4_unicode_ci)
バイナリ型カラムの定義
バイナリデータを扱うカラムの定義:
def data: Column[Array[Byte]] = binary(255) // 固定長バイナリ
def flexData: Column[Array[Byte]] = varbinary(1000) // 可変長バイナリ
def largeData: Column[Array[Byte]] = blob() // バイナリラージオブジェクト
日付・時間型カラムの定義
日付・時間を扱うカラムの定義:
def birthDate: Column[LocalDate] = date() // 日付のみ
def createdAt: Column[LocalDateTime] = datetime() // 日付と時間
def updatedAt: Column[LocalDateTime] = timestamp()
.defaultCurrentTimestamp(onUpdate = true) // 作成・更新時に自動更新
def startTime: Column[LocalTime] = time() // 時間のみ
def fiscalYear: Column[Int] = year() // 年のみ
ENUM型と特殊データ型
ENUM型の使用例:
// ENUMの定義
enum UserStatus extends Enum:
case Active, Inactive, Suspended
object UserStatus extends EnumDataType[UserStatus]
// テーブル定義でENUMを使用
class UserTable extends Table[User]("user"):
// ...
def status: Column[UserStatus] = `enum`[UserStatus]("status")
その他の特殊データ型:
def isActive: Column[Boolean] = boolean() // BOOLEAN型
def uniqueId: Column[BigInt] = serial() // SERIAL型(自動増分のBIGINT UNSIGNED)
デフォルト値の設定
カラムにデフォルト値を設定する方法:
def score: Column[Int] = int().default(100) // 固定値
def updatedAt: Column[LocalDateTime] = timestamp()
.defaultCurrentTimestamp() // 現在のタイムスタンプ
def createdDate: Column[LocalDate] = date()
.defaultCurrentDate // 現在の日付
def nullableField: Column[Option[String]] = varchar(255)
.defaultNull // NULL値
主キー・外部キー・インデックス
単一カラムの主キー
def id: Column[Long] = bigint().autoIncrement.primaryKey
複合主キーの定義
class OrderItemTable extends Table[OrderItem]("order_item"):
def orderId: Column[Int] = int()
def itemId: Column[Int] = int()
def quantity: Column[Int] = int().default(1)
// 複合主キーの定義
override def keys = List(
PRIMARY_KEY(orderId *: itemId)
)
override def * : Column[OrderItem] = (orderId *: itemId *: quantity).to[OrderItem]
インデックスの定義
class UserTable extends Table[User]("user"):
// ...カラム定義...
// インデックスの定義
override def keys = List(
INDEX_KEY("idx_user_name", name), // 名前付きインデックス
UNIQUE_KEY("idx_user_email", email) // ユニークインデックス
)
インデックスタイプの指定も可能です:
override def keys = List(
INDEX_KEY(
Some("idx_name"),
Some(Index.Type.BTREE), // BツリーまたはHASHのインデックスタイプを指定可能
None,
name
)
)
外部キーの定義
外部キーを定義するには、まず参照先のテーブルのTableQueryを作成します:
// 参照先のテーブル
val userTable = TableQuery[UserTable]
// 参照元のテーブル
class ProfileTable extends Table[Profile]("profile"):
def id: Column[Long] = bigint().autoIncrement.primaryKey
def userId: Column[Long] = bigint()
// ...他のカラム...
// 外部キー定義
def fkUser = FOREIGN_KEY(
"fk_profile_user", // 外部キーの名前
userId, // 参照元のカラム
REFERENCE(userTable)(_.id) // 参照先のテーブルとカラム
.onDelete(Reference.ReferenceOption.CASCADE) // 削除時の動作
.onUpdate(Reference.ReferenceOption.RESTRICT) // 更新時の動作
)
override def keys = List(
PRIMARY_KEY(id),
fkUser // 外部キーを追加
)
参照制約オプション(ReferenceOption
):
RESTRICT
: 子レコードが存在する限り親レコードの変更を許可しないCASCADE
: 親レコードの変更に合わせて子レコードも変更するSET_NULL
: 親レコードが変更されたとき子レコードの該当カラムをNULLに設定NO_ACTION
: 制約チェックを遅延させる(基本的にはRESTRICTと同じ)SET_DEFAULT
: 親レコードが変更されたとき子レコードの該当カラムをデフォルト値に設定
制約の設定
特定の命名規則で制約を定義したい場合はCONSTRAINT
を使用できます:
override def keys = List(
CONSTRAINT(
"pk_user", // 制約名
PRIMARY_KEY(id) // 制約タイプ
),
CONSTRAINT(
"fk_user_department",
FOREIGN_KEY(departmentId, REFERENCE(departmentTable)(_.id))
)
)
モデルとの複雑なマッピング
ネストしたモデルのマッピング
case class User(
id: Long,
name: UserName, // ネストした型
contact: Contact // ネストした型
)
case class UserName(first: String, last: String)
case class Contact(email: String, phone: Option[String])
class UserTable extends Table[User]("user"):
def id: Column[Long] = bigint().autoIncrement.primaryKey
def firstName: Column[String] = varchar(50)
def lastName: Column[String] = varchar(50)
def email: Column[String] = varchar(100)
def phone: Column[Option[String]] = varchar(20).defaultNull
// ネストした値のマッピング
def userName: Column[UserName] = (firstName *: lastName).to[UserName]
def contact: Column[Contact] = (email *: phone).to[Contact]
override def * : Column[User] = (id *: userName *: contact).to[User]
この設定により、次のようなSQLが生成されます:
CREATE TABLE `user` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`firstName` VARCHAR(50) NOT NULL,
`lastName` VARCHAR(50) NOT NULL,
`email` VARCHAR(100) NOT NULL,
`phone` VARCHAR(20) NULL,
PRIMARY KEY (`id`)
)
スキーマ生成とDDLの実行
テーブル定義からDDL(Data Definition Language)を生成し、データベースにスキーマを作成します。
TableQueryの生成
val users = TableQuery[UserTable]
val profiles = TableQuery[ProfileTable]
val orders = TableQuery[OrderTable]
スキーマの生成と実行
import ldbc.dsl.*
// スキーマの組み合わせ
val schema = users.schema ++ profiles.schema ++ orders.schema
// データベース接続を使ってスキーマを適用
provider.use { conn =>
DBIO.sequence(
// テーブル作成(存在しない場合のみ)
schema.createIfNotExists,
// データ投入など他の操作...
).commit(conn)
}
DDL操作
val userSchema = users.schema
// 各種DDL操作
userSchema.create // テーブル作成
userSchema.createIfNotExists // テーブルが存在しない場合のみ作成
userSchema.drop // テーブル削除
userSchema.dropIfExists // テーブルが存在する場合のみ削除
userSchema.truncate // テーブル内のすべてのデータを削除
DDLステートメントの確認
実際に実行されるSQLを確認する方法:
// 作成クエリの確認
userSchema.create.statements.foreach(println)
// 条件付き作成クエリの確認
userSchema.createIfNotExists.statements.foreach(println)
// 削除クエリの確認
userSchema.drop.statements.foreach(println)
// 条件付き削除クエリの確認
userSchema.dropIfExists.statements.foreach(println)
// truncateクエリの確認
userSchema.truncate.statements.foreach(println)
カラム属性の設定
カラムには様々な属性を設定できます:
def id: Column[Long] = bigint()
.autoIncrement // 自動増分
.primaryKey // 主キー
.comment("ユーザーID") // コメント
def email: Column[String] = varchar(255)
.unique // ユニーク制約
.comment("メールアドレス")
def status: Column[String] = varchar(20)
.charset(Character.utf8mb4) // 文字セット
.collate(Collate.utf8mb4_unicode_ci) // 照合順序
def hiddenField: Column[String] = varchar(100)
.invisible // 不可視属性(SELECT *では取得されない)
def formatField: Column[String] = varchar(100)
.setAttributes(COLUMN_FORMAT.DYNAMIC[String]) // カラム格納フォーマット
def storageField: Column[Array[Byte]] = blob()
.setAttributes(STORAGE.DISK[Array[Byte]]) // ストレージタイプ
まとめ
ldbcのスキーマモジュールを使用することで、Scalaの型システムを活用して安全かつ表現力豊かなデータベーススキーマを定義することができます。
主な特長:
- 強力な型安全性:コンパイル時にスキーマの問題を検出
- 豊富なデータ型サポート:MySQLの全データ型をサポート
- 柔軟なモデルマッピング:単純なケースクラスからネストした複雑なモデルまで対応
- DDL生成:テーブル定義から直接SQLを生成
- 拡張性:カスタムデータ型やマッピング機能をサポート
次のステップ
これでScalaコードでスキーマを定義する方法がわかりました。手動でスキーマを定義することで、アプリケーションとデータベースの構造を密接に連携させることができます。
次はスキーマコード生成に進み、既存のSQLファイルからスキーマコードを自動生成する方法を学びましょう。