スキーマの非互換性
概要
このページの各セクションでは、Prisma 1からPrisma ORM 2.x 以降へのアップグレード時に起こりうる潜在的な問題について説明し、利用可能な回避策を解説します。
デフォルト値がデータベースに表現されない
問題
Prisma 1データモデルで@default
ディレクティブを追加すると、このフィールドのデフォルト値はPrisma 1サーバーによってランタイムに生成されます。データベースの列にはDEFAULT
制約は追加されません。この制約はデータベース自体に反映されないため、Prisma ORM 2.x 以降のイントロスペクションでは認識できません。
例
Prisma 1データモデル
type Post {
id: ID! @id
published: Boolean @default(value: false)
}
Prisma 1で生成されたSQLマイグレーション
CREATE TABLE "Post" (
id VARCHAR(25) PRIMARY KEY NOT NULL,
published BOOLEAN NOT NULL
);
Prisma ORMバージョン2.x 以降のイントロスペクションの結果
model Post {
id String @id
published Boolean
}
prisma deploy
でPrisma 1データモデルをデータベースにマッピングする際に、DEFAULT
制約がデータベースに追加されていないため、Prisma ORM v2(以降のバージョン)はイントロスペクション中にそれを認識しません。
回避策
データベースの列にDEFAULT
制約を手動で追加する
次のように列を変更してDEFAULT
制約を追加できます。
ALTER TABLE `Post`
ALTER COLUMN published SET DEFAULT false;
この調整後、データベースを再イントロスペクトすると、@default
属性がpublished
フィールドに追加されます。
model Post {
id String @id
published Boolean @default(false)
}
Prismaモデルに@default
属性を手動で追加する
Prismaモデルに@default
属性を追加できます。
model Post {
id String
published Boolean @default(false)
}
@default
属性がPrismaスキーマに設定され、prisma generate
を実行すると、結果として得られるPrisma Clientコードは、指定されたデフォルト値をランタイムに生成します(Prisma 1サーバーがPrisma 1で行っていたのと同様)。
生成されたCUIDがID値としてデータベースに表現されない
問題
Prisma 1は、@id
ディレクティブでアノテーションが付けられたID
フィールドに対して、ID値をCUIDとして自動生成します。これらのCUIDは、Prisma 1サーバーによってランタイムに生成されます。この動作はデータベース自体に反映されないため、Prisma ORM 2.x 以降のイントロスペクションでは認識できません。
例
Prisma 1データモデル
type Post {
id: ID! @id
}
Prisma 1で生成されたSQLマイグレーション
CREATE TABLE "Post" (
id VARCHAR(25) PRIMARY KEY NOT NULL
);
Prisma ORMバージョン2.x 以降のイントロスペクションの結果
model Post {
id String @id
}
データベースにCUIDの動作を示すものが何もないため、Prisma ORMのイントロスペクションはそれを認識しません。
回避策
回避策として、Prismaモデルに@default(cuid())
属性を手動で追加できます。
model Post {
id String @id @default(cuid())
}
@default
属性がPrismaスキーマに設定され、prisma generate
を実行すると、結果として得られるPrisma Clientコードは、指定されたデフォルト値をランタイムに生成します(Prisma 1サーバーがPrisma 1で行っていたのと同様)。
イントロスペクションは(以前のバージョンのPrismaスキーマを上書きするため)属性を削除するため、イントロスペクションごとに属性を再追加する必要があることに注意してください!
@createdAt
がデータベースに表現されない
問題
Prisma 1は、@createdAt
ディレクティブでアノテーションが付けられたDateTime
フィールドに対して値を自動生成します。これらの値は、Prisma 1サーバーによってランタイムに生成されます。この動作はデータベース自体に反映されないため、Prisma ORM 2.x 以降のイントロスペクションでは認識できません。
例
Prisma 1データモデル
type Post {
id: ID! @id
createdAt: DateTime! @createdAt
}
Prisma 1で生成されたSQLマイグレーション
CREATE TABLE "Post" (
id VARCHAR(25) PRIMARY KEY NOT NULL,
"createdAt" TIMESTAMP NOT NULL
);
Prisma ORMバージョン2.x 以降のイントロスペクションの結果
model Post {
id String @id
createdAt DateTime
}
回避策
データベースの列にDEFAULT CURRENT_TIMESTAMP
を手動で追加する
次のように列を変更してDEFAULT
制約を追加できます。
ALTER TABLE "Post"
ALTER COLUMN "createdAt" SET DEFAULT CURRENT_TIMESTAMP;
この調整後、データベースを再イントロスペクトすると、@default
属性がcreatedAt
フィールドに追加されます。
model Post {
id String
createdAt DateTime @default(now())
}
Prismaモデルに@default(now())
属性を手動で追加する
回避策として、Prismaモデルに@default(now())
属性を手動で追加できます。
model Post {
id String @id
createdAt DateTime @default(now())
}
@default
属性がPrismaスキーマに設定され、prisma generate
を実行すると、結果として得られるPrisma Clientコードは、指定されたデフォルト値をランタイムに生成します(Prisma 1サーバーがPrisma 1で行っていたのと同様)。
イントロスペクションは(以前のバージョンのPrismaスキーマを上書きするため)属性を削除するため、イントロスペクションごとに属性を再追加する必要があることに注意してください!
@updatedAt
がデータベースに表現されない
問題
Prisma 1は、@updatedAt
ディレクティブでアノテーションが付けられたDateTime
フィールドに対して値を自動生成します。これらの値は、Prisma 1サーバーによってランタイムに生成されます。この動作はデータベース自体に反映されないため、Prisma ORM 2.x 以降のイントロスペクションでは認識できません。
例
Prisma 1データモデル
type Post {
id: ID! @id
updatedAt: DateTime! @updatedAt
}
Prisma 1で生成されたSQLマイグレーション
CREATE TABLE "Post" (
id VARCHAR(25) PRIMARY KEY NOT NULL,
updatedAt TIMESTAMP
);
Prisma ORMバージョン2.x 以降のイントロスペクションの結果
model Post {
id String @id
updatedAt DateTime
}
回避策
Prismaモデルに@updatedAt
属性を手動で追加する
回避策として、Prismaモデルに@updatedAt
属性を手動で追加できます。
model Post {
id String @id
updatedAt DateTime @updatedAt
}
@updatedAt
属性がPrismaスキーマに設定され、prisma generate
を実行すると、結果として得られるPrisma Clientコードは、既存のレコードが更新されたときにこの列の値を自動的に生成します(Prisma 1サーバーがPrisma 1で行っていたのと同様)。
イントロスペクションは(以前のバージョンのPrismaスキーマを上書きするため)属性を削除するため、イントロスペクションごとに属性を再追加する必要があることに注意してください!
インライン1対1リレーションが1対多として認識される(UNIQUE
制約がない)
問題
データモデルv1.1(Prisma ORM v1.31で導入)では、1対1リレーションをインラインとして宣言できます。その場合、リレーションはリレーションテーブルを介してではなく、関係する2つのテーブルのうち1つにある単一の外部キーを介して維持されます。
このアプローチを使用すると、Prisma ORMは外部キー列にUNIQUE
制約を追加しません。つまり、Prisma ORMバージョン2.x 以降のイントロスペクションの後、以前の1対1リレーションはPrismaスキーマに1対多リレーションとして追加されます。
例
Prisma ORMデータモデルv1.1(Prisma ORM v1.31以降で利用可能)
type User {
id: ID! @id
profile: Profile @relation(link: INLINE)
}
type Profile {
id: ID! @id
user: User
}
この場合、@relation
ディレクティブを省略しても、link: INLINE
が1対1リレーションのデフォルトであるため、同じ動作になります。
Prisma 1で生成されたSQLマイグレーション
CREATE TABLE "User" (
id VARCHAR(25) PRIMARY KEY NOT NULL
);
CREATE TABLE "Profile" (
id VARCHAR(25) PRIMARY KEY NOT NULL,
"user" VARCHAR(25),
FOREIGN KEY ("user") REFERENCES "User"(id)
);
Prisma ORMバージョン2.x 以降のイントロスペクションの結果
model User {
id String @id
Profile Profile[]
}
model Profile {
id String @id
user String?
User User? @relation(fields: [user], references: [id])
}
(このリレーションの外部キーを表す)user
列にUNIQUE
制約が定義されていないため、Prisma ORMのイントロスペクションはリレーションを1対多として認識します。
回避策
外部キー列にUNIQUE
制約を手動で追加する
次のように外部キー列を変更してUNIQUE
制約を追加できます。
ALTER TABLE `Profile`
ADD CONSTRAINT userId_unique UNIQUE (`user`);
この調整後、データベースを再イントロスペクトすると、1対1リレーションが正しく認識されます。
model User {
id String @id
Profile Profile?
}
model Profile {
id String @id
user String? @unique
User User? @relation(fields: [user], references: [id])
}
すべての非インラインリレーションが多対多として認識される
問題
Prisma 1は、ほとんどの場合、リレーションをリレーションテーブルとして表現します。
- Prisma 1のデータモデルv1.0のすべてのリレーションは、リレーションテーブルとして表現されます。
- データモデルv1.1では、すべての多対多リレーションと、
link: TABLE
として宣言された1対1および1対多リレーションは、リレーションテーブルとして表現されます。
この表現のため、Prisma ORMバージョン2.x 以降のイントロスペクションは、これらのすべてのリレーションを多対多リレーションとして認識します。たとえPrisma 1で1対1または1対多として宣言されていたとしてもです。
例
Prisma 1データモデル
type User {
id: ID! @id
posts: [Post!]!
}
type Post {
id: ID! @id
author: User! @relation(link: TABLE)
}
Prisma 1で生成されたSQLマイグレーション
CREATE TABLE "User" (
id VARCHAR(25) PRIMARY KEY NOT NULL
);
CREATE TABLE "Post" (
id VARCHAR(25) PRIMARY KEY NOT NULL
);
CREATE TABLE "_PostToUser" (
"A" VARCHAR(25) NOT NULL REFERENCES "Post"(id) ON DELETE CASCADE,
"B" VARCHAR(25) NOT NULL REFERENCES "User"(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX "_PostToUser_AB_unique" ON "_PostToUser"("A" text_ops,"B" text_ops);
CREATE INDEX "_PostToUser_B" ON "_PostToUser"("B" text_ops);
Prisma ORMバージョン2.x 以降のイントロスペクションの結果
model User {
id String @id
Post Post[] @relation(references: [id])
}
model Post {
id String @id
User User[] @relation(references: [id])
}
Prisma 1によって作成されたリレーションテーブルが、Prisma ORMバージョン2.x 以降と同じリレーションテーブルの規約を使用しているため、リレーションは多対多リレーションとして認識されるようになりました。
回避策
回避策として、データをPrisma ORMの1対多リレーションと互換性のある構造に移行できます。
Post
テーブルに新しい列authorId
を作成します。この列は、User
テーブルのid
フィールドを参照する外部キーである必要があります。ALTER TABLE `Post` ADD COLUMN `authorId` VARCHAR(25);
ALTER TABLE `Post` ADD FOREIGN KEY (`authorId`) REFERENCES `User` (`id`);_PostToUser
リレーションテーブルからすべての行を読み取るSQLクエリを作成し、各行に対して以下を行います。- 列
A
の値を参照して、それぞれのPost
レコードを見つけます。 - 列
B
の値をauthorId
の値としてそのPost
レコードに挿入します。
UPDATE Post, _PostToUser
SET Post.authorId = _PostToUser.B
WHERE Post.id = _PostToUser.A- 列
_PostToUser
リレーションテーブルを削除します。DROP TABLE `_PostToUser`;
その後、データベースをイントロスペクトすると、リレーションが1対多として認識されるようになります。
model User {
id String @id
Post Post[]
}
model Post {
id String @id
User User @relation(fields: [authorId], references: [id])
authorId String
}
Json
型がデータベースでTEXT
として表現される
問題
Prisma 1は、データモデルでJson
データ型をサポートしています。ただし、基盤となるデータベースでは、Json
型のフィールドは、基盤となるデータベースのTEXT
データ型を使用してプレーンテキストとして実際に保存されます。保存されたJSONデータの解析と検証は、Prisma 1サーバーによってランタイムに実行されます。
例
Prisma 1データモデル
type User {
id: ID! @id
jsonData: Json
}
Prisma 1で生成されたSQLマイグレーション
CREATE TABLE "User" (
id VARCHAR(25) PRIMARY KEY NOT NULL,
jsonData TEXT
);
Prisma ORMバージョン2.x 以降のイントロスペクションの結果
model User {
id String @id
jsonData String?
}
回避策
列の型をJSON
に手動で変更できます。
ALTER TABLE User MODIFY COLUMN jsonData JSON;
この調整後、データベースを再イントロスペクトすると、フィールドがJson
として認識されるようになります。
model User {
id String @id
jsonData Json?
}
EnumがデータベースでTEXT
として表現される
問題
Prisma 1は、データモデルでenum
データ型をサポートしています。ただし、基盤となるデータベースでは、enum
として宣言された型は、基盤となるデータベースのTEXT
データ型を使用してプレーンテキストとして実際に保存されます。保存されたenum
データの検証は、Prisma 1サーバーによってランタイムに実行されます。
例
Prisma 1データモデル
type User {
id: ID! @id
role: Role
}
enum Role {
ADMIN
CUSTOMER
}
Prisma 1で生成されたSQLマイグレーション
CREATE TABLE "User" (
id VARCHAR(25) PRIMARY KEY NOT NULL,
role TEXT
);
Prisma ORMバージョン2.x 以降のイントロスペクションの結果
model User {
id String @id
role String?
}
回避策
role
列を目的の値を持つenumに手動で変更できます。
- Prisma 1データモデルで定義した
enum
を反映するenum
をデータベースに作成します。CREATE TYPE "Role" AS ENUM ('CUSTOMER', 'ADMIN');
- 型を
TEXT
から新しいenum
に変更します。ALTER TABLE "User" ALTER COLUMN "role" TYPE "Role"
USING "role"::text::"Role";
イントロスペクション後、型がenumとして正しく認識されるようになります。
model User {
id String @id
role Role?
}
enum Role {
ADMIN
CUSTOMER
}
CUID長の不一致
問題
Prisma 1は、すべてのデータベースレコードのID値としてCUIDを使用します。基盤となるデータベースでは、これらのIDは最大サイズが25文字の文字列(VARCHAR(25)
として)で表現されます。ただし、Prisma ORM 2.x(以降のバージョン)スキーマで@default(cuid())
を使用してデフォルトCUIDを構成すると、生成されるID値が25文字の制限を超える可能性があります(最大長は30文字になる可能性があります)。IDをPrisma ORM 2.x(以降のバージョン)に対応させるには、したがって、列の型をVARCHAR(30)
に調整する必要があります。
例
Prisma 1データモデル
type User {
id: ID! @id
}
Prisma 1で生成されたSQLマイグレーション
CREATE TABLE "User" (
id VARCHAR(25) PRIMARY KEY NOT NULL
);
Prisma ORMバージョン2.x 以降のイントロスペクションの結果
model User {
id String @id
}
回避策
VARCHAR(25)
列をVARCHAR(30)
に手動で変更できます。
SET FOREIGN_KEY_CHECKS=0;
ALTER TABLE `User` CHANGE `id` `id` char(30) CHARACTER SET utf8 NOT NULL;
SET FOREIGN_KEY_CHECKS=1;
注: Upgrade CLIでこの問題を修正する場合、基盤となるデータベースで列型を変更した後でも、生成されたSQLステートメントはUpgrade CLIに表示され続けます。これは現在、Upgrade CLIの制限事項です。
スカラーリスト(配列)が追加テーブルで管理される
問題
Prisma 1では、モデルにスカラー型のリストを定義できます。内部的には、これはリスト内の値を追跡する追加テーブルで実装されています。
隠れたパフォーマンスコストが発生する追加テーブルによるアプローチを削除するために、Prisma ORM 2.x 以降のバージョンは、使用するデータベースでネイティブにサポートされている場合にのみ、スカラーリストをサポートします。現時点では、PostgreSQLのみがスカラーリスト(配列)をネイティブにサポートしています。
したがって、PostgreSQLを使用すると、Prisma ORM 2.x 以降のバージョンでもスカラーリストを引き続き使用できますが、データ移行を実行して、Prisma 1からの追加テーブルからPostgreSQL配列にデータを転送する必要があります。
例
Prisma 1データモデル
type User {
id: ID! @id
coinflips: [Boolean!]! @scalarList(strategy: RELATION)
}
Prisma 1で生成されたSQLマイグレーション
CREATE TABLE "User" (
id VARCHAR(25) PRIMARY KEY NOT NULL
);
CREATE TABLE "User_coinflips" (
"nodeId" VARCHAR(25) REFERENCES "User"(id),
position INTEGER,
value BOOLEAN NOT NULL,
CONSTRAINT "User_coinflips_pkey" PRIMARY KEY ("nodeId", position)
);
CREATE UNIQUE INDEX "User_coinflips_pkey" ON "User_coinflips"("nodeId" text_ops,position int4_ops);
Prisma ORM 2イントロスペクションの結果
model User {
id String @id
User_coinflips User_coinflips[]
}
model User_coinflips {
nodeId String
position Int
value Boolean
User User @relation(fields: [nodeId], references: [id])
@@id([nodeId, position])
}
Prisma Clientを生成できるようになり、追加テーブルを介してスカラーリストからデータにアクセスできるようになることに注意してください。PostgreSQLユーザーは、代わりにデータをネイティブPostgreSQL配列に移行し、スカラーリスト用のより洗練されたPrisma Client APIの恩恵を受け続けることができます(詳細については、以下のセクションをお読みください)。
Prisma Client API呼び出しのサンプルを展開
coinflipsデータにアクセスするには、常にクエリにinclude
する必要があります。
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
coinflips: {
orderBy: { position: 'asc' },
},
},
})
注: リストの順序を保持するには、
orderBy
が重要です。
これはクエリの結果です。
{
id: 1,
name: 'Alice',
coinflips: [
{ id: 1, position: 1000, value: false },
{ id: 2, position: 2000, value: true },
{ id: 3, position: 3000, value: false },
{ id: 4, position: 4000, value: true },
{ id: 5, position: 5000, value: true },
{ id: 6, position: 6000, value: false }
]
}
リストからブール値のみにアクセスするには、次のようにuser
のcoinflips
をmap
できます。
const currentCoinflips = user!.coinflips.map((cf) => cf.value)
注: 上記の感嘆符は、
user
値を強制アンラップしていることを意味します。これは、前のクエリから返されたuser
がnull
である可能性があるため必要です。
map
の呼び出し後のcurrentCoinflips
の値は次のとおりです。
[false, true, false, true, true, false]
回避策
次の回避策は、PostgreSQLユーザーのみが利用できます!
スカラーリスト(つまり、配列)はネイティブPostgreSQL機能として利用できるため、Prismaスキーマでcoinflips: Boolean[]
の同じ表記法を引き続き使用できます。
ただし、そのためには、基盤となるデータをUser_coinflips
テーブルからPostgreSQL配列に手動で移行する必要があります。その方法を次に示します。
- 新しい
coinflips
列をUser
テーブルに追加します。ALTER TABLE "User" ADD COLUMN coinflips BOOLEAN[];
"User_coinflips".value
から"User.coinflips"
にデータを移行します。UPDATE "User"
SET coinflips = t.flips
FROM (
SELECT "nodeId", array_agg(VALUE ORDER BY position) AS flips
FROM "User_coinflips"
GROUP BY "nodeId"
) t
where t."nodeId" = "User"."id";- クリーンアップするには、
User_coinflips
テーブルを削除できます。DROP TABLE "User_coinflips";
これでデータベースをイントロスペクトすると、coinflips
フィールドが新しいPrismaスキーマで配列として表現されます。
model User {
id String @id
coinflips Boolean[]
}
以前と同様にPrisma Clientを引き続き使用できます。
const user = await prisma.user.findUnique({
where: { id: 1 },
})
これはAPI呼び出しからの結果です。
{
id: 1,
name: 'Alice',
coinflips: [ false, true, false, true, true, false ]
}