prismaでデータがあるテーブルの列追加をするパターンを考える

にあえん

August 10, 2023

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を直接編集する方法

スクリプトで実施する方法

公式ではこのようなケースはSQLを直接編集する方法で対処しています。

This page was helpful.

まとめ

SQLを直接編集することができるのは安心ですね。

(頻繁にはやりたくないですが)

よりPrismaが好きになりました。