White Box技術部

WEB開発のあれこれ(と何か)

prismaのschemaファイルの書き方(MySQL用)

フルNext.jsアプリケーション作成の一環で、DBマイグレーションツールにprismaを使ってみたので、書き方のメモです。

いきなりですが、prismaは細かくDDL文を制御できず、思ったテーブルを作れなかったので、細かくテーブル定義を設定したい場合はTypeORMの方がいいかもしれません。

準備

プロジェクトにprismaをインストールし、初期ファイル(prisma/schema.prisma)を作成します。

$ yarn add -D prisma

$ yarn run prisma init

schema.prismaの書き方

公式ドキュメントの書き方を参考に、スキーマをschema.prismaに記載していきます。

記載する際の注意点・考慮点は以下です。

  • providerをmysqlに変更する
  • @db.Timestampではなく@db.Timestamp(0)のように書かないとマイグレーション出力時にエラーになる
  • @updatedAtをつけてもON UPDATE CURRENT_TIMESTAMPがつかない
    • つけたい場合はマイグレーションファイルを作成してから、手動で追記するしかないっぽい(追記して実行したら反映はされる
  • model名をキャメルケースで書いても、DDLでスネークケースのテーブル名に変換されるわけではない
    • スネークケースのテーブル名にしたければmodel名をそうしておく
  • @relationのnameはmodel間のマッチングキーに利用する
  • サポートされていない型(例えばgeometry)を使う場合は、Unsupported("geometry")のように書く
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Area {
  id          Int      @id @default(autoincrement())
  name        String   @db.VarChar(32)
  description String?  @db.Text
  created_at  DateTime @default(now()) @db.Timestamp(0)
  updated_at  DateTime @default(now()) @updatedAt @db.Timestamp(0)
  shop        Shop[]   @relation("shop_fk_1")
}

model Shop {
  id           Int                      @id @default(autoincrement())
  name         String                   @db.VarChar(128)
  area_id      Int
  area         Area                     @relation(name: "shop_fk_1", fields: [area_id], references: [id])
  address      String?                  @db.VarChar(128)
  tel          String?                  @db.VarChar(128)
  access       String?                  @db.Text
  localton     Unsupported("geometry")?
  url          String                   @db.Text
  created_at   DateTime                 @default(now()) @db.Timestamp(0)
  updated_at   DateTime                 @default(now()) @updatedAt @db.Timestamp(0)
  shop_article Shop_Article[]           @relation("shop_article_fk_1")
}

model Shop_Article {
  id                 Int                  @id @default(autoincrement())
  status             ArticleStatus?
  published_at       DateTime             @default(now()) @db.Timestamp(0)
  title              String?              @db.Text
  image_url          String?              @db.Text
  shop_id            Int?
  shop               Shop?                @relation(name: "shop_article_fk_1", fields: [shop_id], references: [id])
  created_at         DateTime             @default(now()) @db.Timestamp(0)
  updated_at         DateTime             @default(now()) @updatedAt @db.Timestamp(0)
  shop_article_image Shop_Article_Image[] @relation("shop_article_image_fk_1")

  @@index([shop_id])
  @@index(fields: [published_at], name: "idx_shop_article_published_at")
}

enum ArticleStatus {
  draft
  published
}

model Shop_Article_Image {
  shop_article_id Int
  shop_article    Shop_Article @relation(name: "shop_article_image_fk_1", fields: [shop_article_id], references: [id])
  order           Int
  image_url       String       @db.Text
  created_at      DateTime     @default(now()) @db.Timestamp(0)
  updated_at      DateTime     @default(now()) @updatedAt @db.Timestamp(0)

  @@id([shop_article_id, order])
}

※例は既存アプリケーションの書き直しなのでtimestamp型ですが、今から作るなら範囲を考えてdatetime型を使ってください。

TIMESTAMP[(fsp)]

タイムスタンプです。範囲は '1970-01-01 00:00:01.000000' UTC から '2038-01-19 03:14:07.999999' UTC です。TIMESTAMP 値は、エポック ('1970-01-01 00:00:00' UTC) からの秒数として格納されます。TIMESTAMP は、'1970-01-01 00:00:00' という値を表すことはできません。これは、エポックからの秒数が 0 であることと同等で、0 という値は '0000-00-00 00:00:00'、つまり「ゼロ」の TIMESTAMP 値を表すために予約されているからです。
MySQL 5.6.4 以降では、小数秒の精度を指定するために 0 から 6 の範囲でオプションの fsp 値を指定できます。0 の値は、小数部がないことを表します。省略した場合、デフォルトの精度は 0 です。
MySQL :: MySQL 5.6 リファレンスマニュアル :: 11.1.2 日付と時間型の概要

migration.sql

上記のschema.prismaから出力されたマイグレーションファイルは以下のようになりました。

-- CreateTable
CREATE TABLE `Area` (
    `id` INTEGER NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(32) NOT NULL,
    `description` TEXT,
    `created_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
    `updated_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),

    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `Shop` (
    `id` INTEGER NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(128) NOT NULL,
    `area_id` INTEGER NOT NULL,
    `address` VARCHAR(128),
    `tel` VARCHAR(128),
    `access` TEXT,
    `localton` geometry,
    `url` TEXT NOT NULL,
    `created_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
    `updated_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),

    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `Shop_Article` (
    `id` INTEGER NOT NULL AUTO_INCREMENT,
    `status` ENUM('draft', 'published'),
    `published_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
    `title` TEXT,
    `image_url` TEXT,
    `shop_id` INTEGER,
    `created_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
    `updated_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),

    INDEX `Shop_Article.shop_id_index`(`shop_id`),
    INDEX `idx_shop_article_published_at`(`published_at`),
    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `Shop_Article_Image` (
    `shop_article_id` INTEGER NOT NULL,
    `order` INTEGER NOT NULL,
    `image_url` TEXT NOT NULL,
    `created_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),
    `updated_at` TIMESTAMP(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0),

    PRIMARY KEY (`shop_article_id`, `order`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `Shop` ADD FOREIGN KEY (`area_id`) REFERENCES `Area`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `Shop_Article` ADD FOREIGN KEY (`shop_id`) REFERENCES `Shop`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `Shop_Article_Image` ADD FOREIGN KEY (`shop_article_id`) REFERENCES `Shop_Article`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

外部キー制約でON DELETE CASCADEを付けたくない場合は、手動で消さないとダメそうです。

また、DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;あたりは接続しているMySQLの設定から自動取得しているようでした。

書くときのコツ

最初に全テーブルのカラム定義を書いてしまい、その後にインデックスや外部キー制約を書いていくのが良さそうでした。
特に外部キー制約関連は、途中まで書いてフォーマットをかけると、prismaのフォーマッタが自動で埋めてくれることがあるのですが、 relation nameを使わないキャメルケースの定義を追加するので、 同じ意図の定義が、自分が書いたものとフォーマッタで複数追加されたりしたので、注意が必要でした。

まとめ

マイグレーションにおいてはTypeORMの方が細かく指定できていいかもしれないですが、prismaSQLで出力されるのでDDLを変更できる人であればなんとでもできそうでした。