こんにちは、 @ttaka_66 です。
CROWDWORKSコンサルティングの業務効率化を目的とした自社システムを開発しています。自社システムではO/RマッパーにPrismaを利用しています。今回はスキーマ変更を伴うデータ移行を行ったので、それについて記載します。
背景
背景として、以前リンクスエージェントで利用していたシステムをCROWDWORKSコンサルティングで使えるように改修しています。
リンクスエージェントでは、クライアントとワーカーのマッチングを支援する事業であったため、クライアントとワーカーが1対1で対応する契約を結びます。
一方でCROWDWORKSコンサルティングでは、クライアントの経営課題解決を目的としたコンサルティング事業のため、クライアントとクラウドワークスが直接契約するケースがあります。また、課題解決にワーカーの方に支援いただくケースがあり、クライアントとワーカーが1対多になるケースも多く発生します。
リンクスエージェント向けにシステムを利用していた際は、1つの契約テーブルにクライアントとの契約情報とワーカーとの契約情報のフィールドを持たせていましたが、クライアントとワーカーが1対1ではないケースが発生することとなったのでスキーマを変えることにしました。
※ 現在リンクスエージェントはサービスを終了しています。
変更点
1つだった契約テーブルをクライアント・ワーカー共通項目、クライアント契約情報、ワーカー契約情報の3つのテーブルに分割しました。
今回は説明簡略化のために以下の例を用います。
- 契約情報(contracts)を契約共通項目(contracts)、クライアント契約情報(contract_clients)、ワーカー契約情報(contract_workers)に分割
- 契約開始日(start_date)と契約終了日(end_date)は契約共通項目
- 業務委託料(commission_fee)はクライアントとワーカーでそれぞれ持つ
手順
リリースを3つのステップに分けて移行をしました。
ステップ1 旧スキーマは削除せずに新スキーマで必要となるフィールドを追加し、アプリケーションからのINSERT、UPDATE、DELETEは新旧両方のスキーマに適応されるようにする
ステップ2 アプリケーションで表示している(SELECT)部分に新スキーマの情報を使うようにする
ステップ3 旧スキーマのフィールドとアプリケーション側の旧スキーマに対するINSERT、UPDATE、DELETEロジックの削除する
ステップを3つに分けた理由を以下です。
- 既存の契約情報も新しいスキーマに移す必要があったので、漏れがあったときのために元の情報を一定期間保持しておきたかった
- 契約情報は多くの情報に関連しているため、間違っていたときに途中で修正できるようなブレークポイントをおきたかった
- 業務に利用しているので、できればシステムを停止せずに移行したかった
対応
具体的に対応したことをコードを用いて記載します。
ステップ1
schema.prisma
に新スキーマのフィールドを定義してMigrateを実行
model Contract { id Int @id @default(autoincrement())' startDate DateTime @map(name: "start_date") @db.Date endDate DateTime @map(name: "end_date") @db.Date commissionFeeClient Int @map(name: "commission_fee_client") commissionFeeWorker Int @map(name: "commission_fee_worker") ... + contractClient ContractClient? + contractWorker ContractWorker? @@map(name: "contracts") } +model ContractClient { + id Int @id @default(autoincrement()) + contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade) + contractId Int @unique @map(name: "contract_id") + commissionFee Int @map(name: "commission_fee") + ... + + @@map(name: "contract_clients") +} +model ContractWorker { + id Int @id @default(autoincrement()) + contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade) + contractId Int @unique @map(name: "contract_id") + commissionFee Int @map(name: "commission_fee") + ... + + @@map(name: "contract_workers") +}
新旧テーブル両方でINSERT or UPDATE or DELETE(ここではINSERT)
import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); ... prisma.contract.create({ data: { startDate: startDateValue, endDate: endDateValue, commissionFeeClient: commissionFeeClientValue, commissionFeeWorker: commissionFeeWorkerValue, + contractClient: { + create: { + commissionFee: commissionFeeClientValue, + ...restContractClient + }, + }, + contractWorker: { + create: { + commissionFee: commissionFeeWorkerValue, + ...restContractWorker, + }, + }, ...rest, } });
ステップ1をリリースした段階で既存のデータを新スキーマのフィールドに追加
+import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main(): Promise<void> { + await prisma.$transaction( + async (tx) => { + const contracts = await tx.contract.findMany(); + for (const contract of contracts) { + const contractClient = await tx.contractClient.create({ + data: { + contract: { connect: { id: contract.id } }, + commissionFee: contract.comissionFeeClient, + ..., + }, + }); + + const contractWorker = await tx.contractWorker.create({ + data: { + contract: { connect: { id: contract.id } }, + comissionFee: contract.comissionFeeWorker, + ..., + }, + }); + } + } + ); +} + +main() + .catch(async (e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => await prisma.$disconnect());
ステップ2
新スキーマの方のデータを取得するように変更
import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); ... prisma.contract.findUnique({ where: { id: someId }, + include: { + contractWorker: true + }, });
DBから取得したデータを利用する側のプログラムで新スキーマのフィールド(上記のコードで追加したincludeの部分)を使うように修正
※ 利用する側のプログラムはシステムアーキテクチャによって違うのでコード例は割愛させてください
ステップ3
schema.prisma
にカラム削除を記述してMigrateを実行
model Contract { id Int @id @default(autoincrement()) startDate DateTime @map(name: "start_date") @db.Date endDate DateTime @map(name: "end_date") @db.Date - commissionFeeClient Int @map(name: "commission_fee_client") - commissionFeeWorker Int @map(name: "commission_fee_worker") ... contractClient ContractClient? contractWorker ContractWorker? @@map(name: "contracts") }
アプリケーション側の旧スキーマのフィールドに対するINSERT、UPDATE、DELETEロジックの削除(ここではINSERT)
import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); ... prisma.contract.create({ data: { startDate: startDateValue, endDate: endDateValue, - commissionFeeClient: commissionFeeClientValue, - commissionFeeWorker: commissionFeeWorkerValue, contractClient: { create: { commissionFee: commissionFeeClientValue, ...restContractClient }, }, contractWorker: { create: { commissionFee: commissionFeeWorkerValue, ...restContractWorker, }, }, ...rest, } });
所感
本記事では簡略化して記載しましたが、実際は契約情報や利用されている箇所は多く変更したコードもそれなりに多くなりました(トータルで追加5595行、削除2695行)が、ほぼ漏れがなくコード変更することができました。
これは、DBとの接続部分と取得データの利用部分でテストコードを記述することで、Prismaがしっかりエラーを出してくれたので実現できたと考えています。
それでも多少の漏れは発生しましたが、サービスに影響を及ぼすことなくステップ間でリカバリすることができました。
本記事が誰かのお役に立てれば大変嬉しく思います。