Prisma、便利ですよね。
マイグレーションを重ねていくと、あるテーブルに対して列追加しなければいけないこともあると思います。
そういったときにどう対処すればよいか考えてみました。
検証用のパッケージを作成する
とりあえず検証用のパッケージだけ作成します。
mkdir prisma-paradigm-shift-pattern
cd prisma-paradigm-shift-pattern
yarn add -D typescript ts-node @types/node prisma
yarn prisma init --datasource-provider sqlite
prisma.schema
には、prismaのQuickStartにあるモデルを持ってきます。
// 参考モデル
// https://www.prisma.io/docs/getting-started/quickstart
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
一度最初のマイグレーションだけしておきましょう。
% yarn prisma migrate dev --name init
yarn run v1.22.15
$ /home/username/Project/study/prisma-add-column-pattern/node_modules/.bin/prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
SQLite database dev.db created at file:./dev.db
Applying migration `20230811001347_init`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20230811001347_init/
└─ migration.sql
Your database is now in sync with your schema.
Running generate... (Use --skip-generate to skip the generators)
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 2 new dependencies.
info Direct dependencies
└─ @prisma/client@5.1.1
info All dependencies
├─ @prisma/client@5.1.1
└─ @prisma/engines-version@5.1.1-1.6a3747c37ff169c90047725a05a6ef02e32ac97e
✔ Generated Prisma Client (5.1.1 | library) to ./node_modules/@prisma/client in 141ms
Done in 9.06s.
ついでにシード用のスクリプトも実装しておきましょう。
(これもまんまチュートリアルから持ってこれます)
// 参考
// https://www.prisma.io/docs/guides/migrate/seed-database#how-to-seed-your-database-in-prisma
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const alice = await prisma.user.upsert({
where: { email: 'alice@prisma.io' },
update: {},
create: {
email: 'alice@prisma.io',
name: 'Alice',
posts: {
create: {
title: 'Check out Prisma with Next.js',
content: 'https://www.prisma.io/nextjs',
published: true,
},
},
},
})
const bob = await prisma.user.upsert({
where: { email: 'bob@prisma.io' },
update: {},
create: {
email: 'bob@prisma.io',
name: 'Bob',
posts: {
create: [
{
title: 'Follow Prisma on Twitter',
content: 'https://twitter.com/prisma',
published: true,
},
{
title: 'Follow Nexus on Twitter',
content: 'https://twitter.com/nexusgql',
published: true,
},
],
},
},
})
console.log({ alice, bob })
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
package.json
に以下のセクションを追加します。
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
一度シードを実行します。
% yarn prisma db seed
yarn run v1.22.15
$ /home/username/Project/study/prisma-add-column-pattern/node_modules/.bin/prisma db seed
Environment variables loaded from .env
Running seed command `ts-node prisma/seed.ts` ...
{
alice: { id: 1, email: 'alice@prisma.io', name: 'Alice' },
bob: { id: 2, email: 'bob@prisma.io', name: 'Bob' }
}
🌱 The seed command has been executed.
Done in 3.79s.
準備できました。
マイグレーションSQLを直接編集する
まずはこの方法です。
先程のUserモデルにage
カラムを追加します。
model User {
// ...
age Int
// ...
}
これはNOT NULLなので、普通にマイグレーションしようとすると怒られます。
% yarn prisma migrate dev --name add_age
yarn run v1.22.15
$ /home/username/Project/study/prisma-add-column-pattern/node_modules/.bin/prisma migrate dev --name add_age
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
Error:
⚠️ We found changes that cannot be executed:
• Step 0 Added the required column `age` to the `User` table without a default value. There are 2 rows in this table, it is not possible to execute this step.
You can use yarn prisma migrate dev --create-only to create the migration file, and manually modify it to address the underlying issue(s).
Then run yarn prisma migrate dev to apply it and verify it works.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
エラーメッセージ中にもありますが、これは--create-only
でマイグレーションファイルだけ作って、そのファイルを手動で変更して再度prisma migrate dev
するというやり方です。
ではマイグレーションファイルだけ作ってみます。
% yarn prisma migrate dev --create-only --name add_age
yarn run v1.22.15
$ /home/username/Project/study/prisma-add-column-pattern/node_modules/.bin/prisma migrate dev --create-only --name add_age
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
⚠️ We found changes that cannot be executed:
• Step 0 Added the required column `age` to the `User` table without a default value. There are 2 rows in this table, it is not possible to execute this step.
Prisma Migrate created the following migration without applying it 20230811002846_add_age
You can now edit it and apply it by running yarn prisma migrate dev.
Done in 1.82s.
怒られていますが無事作成されました。
SQLファイルの中身はこうなっています。
/*
Warnings:
- Added the required column `age` to the `User` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"age" INTEGER NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT
);
INSERT INTO "new_User" ("email", "id", "name") SELECT "email", "id", "name" FROM "User";
DROP TABLE "User";
ALTER TABLE "new_User" RENAME TO "User";
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;
SQLファイルの中にも丁寧に警告文が載っていますね。親切…
ようはage
に値が入っていればいいので、変更すべきはINSERT文のあたりですね。
とりあえず全員20歳で登録してしまいましょう。
INSERT INTO "new_User" ("email", "id", "age", "name") SELECT "email", "id", 20, "name" FROM "User";
マイグレーションを実行してみます。
% yarn prisma migrate dev
yarn run v1.22.15
$ /home/username/Project/study/prisma-add-column-pattern/node_modules/.bin/prisma migrate dev
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
Applying migration `20230811002846_add_age`
The following migration(s) have been applied:
migrations/
└─ 20230811002846_add_age/
└─ migration.sql
Your database is now in sync with your schema.
✔ Generated Prisma Client (5.1.1 | library) to ./node_modules/@prisma/client in 161ms
Done in 2.80s.
無事実行できましたね。
この際、シードのスクリプトでageを設定していないため、エラーになっています。
忘れずに修正しておきましょう。
const alice = await prisma.user.upsert({
// ...
create: {
// ...
age: 42,
// ...
})
const bob = await prisma.user.upsert({
// ...
create: {
// ...
age: 24,
// ...
})
スクリプトでデータ不整合を解消する場合
追加する列に特定のデータを入れたいようなケースではこちらのほうがいいかも?
先ほどと同じようにUserモデルにage
カラムを追加しますが、今回はOptionalで追加しましょう。
model User {
// ...
age Int?
// ...
}
これにより、マイグレーションを実行してもエラーにならなくなりました。
% yarn prisma migrate dev --name add_age
yarn run v1.22.15
$ /home/username/Project/study/prisma-add-column-pattern/node_modules/.bin/prisma migrate dev --name add_age
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
Applying migration `20230811004557_add_age`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20230811004557_add_age/
└─ migration.sql
Your database is now in sync with your schema.
✔ Generated Prisma Client (5.1.1 | library) to ./node_modules/@prisma/client in 139ms
Done in 2.54s.
既存データのageに値を入れるためにtypescriptファイルを作成します。
先程作成したマイグレーションディレクトリ内に入れておくとわかりやすいかと思います。
touch prisma/migrations/20230811004557_add_age/add_value_to_age.ts
作成したTypeScript上に実装を追加します。
シードと同じでクライアントを使う感じですね。
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
const alice = await prisma.user.update({
where: { email: 'alice@prisma.io' },
data: {
age: 42,
},
})
const bob = await prisma.user.update({
where: { email: 'bob@prisma.io' },
data: {
age: 24,
},
})
console.log({ alice, bob })
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
今登録されているのはシードで登録されたデータ2件だけなので、これで対処できます。
もしデプロイする環境ごとに登録されているデータが異なるのであれば、それごとにスクリプトを作成する必要がありますね。
このスクリプトを実行します。
% yarn ts-node prisma/migrations/20230811004557_add_age/add_value_to_age.ts
yarn run v1.22.15
$ /home/username/Project/study/prisma-add-column-pattern/node_modules/.bin/ts-node prisma/migrations/20230811004557_add_age/add_value_to_age.ts
{
alice: { id: 1, age: 42, email: 'alice@prisma.io', name: 'Alice' },
bob: { id: 2, age: 24, email: 'bob@prisma.io', name: 'Bob' }
}
Done in 1.87s.
つづけて、schema.prisma
のageをNOT NULLに変更します。
model User {
// ...
age Int
// ...
}
マイグレーションを実行します。
% yarn prisma migrate dev --name change_age_notnull
yarn run v1.22.15
$ /home/username/Project/study/prisma-add-column-pattern/node_modules/.bin/prisma migrate dev --name change_age_notnull
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
Applying migration `20230811005924_change_age_notnull`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20230811005924_change_age_notnull/
└─ migration.sql
Your database is now in sync with your schema.
✔ Generated Prisma Client (5.1.1 | library) to ./node_modules/@prisma/client in 145ms
Done in 2.73s.
成功しました!
この場合もシードのスクリプトがエラーになっているので、直しておきます。
const alice = await prisma.user.upsert({
// ...
create: {
// ...
age: 42,
// ...
})
const bob = await prisma.user.upsert({
// ...
create: {
// ...
age: 24,
// ...
})
メリット・デメリット
SQLを直接編集する方法
- メリット
- prismaのマイグレーションの手順の一環として行える
- デメリット
- 複雑なことができない
スクリプトで実施する方法
- メリット
- 複雑なデータを入れ込める
- デメリット
- 公式に用意されている手順ではないため、マイグレーションが二度手間になる
公式ではこのようなケースはSQLを直接編集する方法で対処しています。
まとめ
SQLを直接編集することができるのは安心ですね。
(頻繁にはやりたくないですが)
よりPrismaが好きになりました。