リレーション
リレーションとは、Prismaスキーマにおける2つのモデル間の接続です。例えば、1人のユーザーが複数のブログ投稿を持つことができるため、User
とPost
の間には1対多のリレーションが存在します。
以下のPrismaスキーマは、User
モデルとPost
モデルの間の1対多リレーションを定義しています。リレーションの定義に関わるフィールドが強調表示されています。
- リレーショナルデータベース
- MongoDB
model User {
id Int @id @default(autoincrement())
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
author User @relation(fields: [authorId], references: [id])
authorId Int // relation scalar field (used in the `@relation` attribute above)
title String
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
posts Post[]
}
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
author User @relation(fields: [authorId], references: [id])
authorId String @db.ObjectId // relation scalar field (used in the `@relation` attribute above)
title String
}
Prisma ORMレベルでは、User
/ Post
リレーションは以下で構成されます。
- 2つのリレーションフィールド:
author
とposts
。リレーションフィールドはPrisma ORMレベルでモデル間の接続を定義し、データベースには存在しません。これらのフィールドはPrisma Clientを生成するために使用されます。 @relation
属性によって参照されるスカラフィールドauthorId
。このフィールドはデータベースに存在します。これはPost
とUser
を接続する外部キーです。
Prisma ORMレベルでは、2つのモデル間の接続は、リレーションの両側にあるリレーションフィールドによって常に表されます。
データベースにおけるリレーション
リレーショナルデータベース
以下のエンティティ関係図は、リレーショナルデータベースにおけるUser
テーブルとPost
テーブルの間の同じ1対多リレーションを定義しています。
SQLでは、2つのテーブル間のリレーションを作成するために外部キーを使用します。外部キーはリレーションの片側に保存されます。この例は以下で構成されます。
Post
テーブルにあるauthorId
という名前の外部キーカラム。User
テーブルにあるid
という名前の主キーカラム。Post
テーブルのauthorId
カラムは、User
テーブルのid
カラムを参照しています。
Prismaスキーマでは、外部キー/主キーの関係はauthor
フィールドの@relation
属性によって表現されます。
author User @relation(fields: [authorId], references: [id])
注: Prismaスキーマのリレーションは、データベースのテーブル間に存在する関係を表します。データベースにその関係が存在しない場合、Prismaスキーマにも存在しません。
MongoDB
MongoDBの場合、Prisma ORMは現在、正規化されたデータモデル設計を使用しており、これはリレーショナルデータベースと同様に、ドキュメントがIDによって互いを参照することを意味します。
以下のドキュメントはUser
(User
コレクション内)を表します。
{ "_id": { "$oid": "60d5922d00581b8f0062e3a8" }, "name": "Ella" }
以下のPost
ドキュメントのリスト(Post
コレクション内)は、それぞれ同じユーザーを参照するauthorId
フィールドを持っています。
[
{
"_id": { "$oid": "60d5922e00581b8f0062e3a9" },
"title": "How to make sushi",
"authorId": { "$oid": "60d5922d00581b8f0062e3a8" }
},
{
"_id": { "$oid": "60d5922e00581b8f0062e3aa" },
"title": "How to re-install Windows",
"authorId": { "$oid": "60d5922d00581b8f0062e3a8" }
}
]
このデータ構造は、複数のPost
ドキュメントが同じUser
ドキュメントを参照するため、1対多リレーションを表します。
IDとリレーションスカラフィールドに対する@db.ObjectId
モデルのIDがObjectId
(String
フィールドで表される)の場合、モデルのIDとリレーションの反対側のリレーションスカラフィールドに@db.ObjectId
を追加する必要があります。
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
posts Post[]
}
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
author User @relation(fields: [authorId], references: [id])
authorId String @db.ObjectId // relation scalar field (used in the `@relation` attribute above)
title String
}
Prisma Clientにおけるリレーション
Prisma ClientはPrismaスキーマから生成されます。以下の例は、Prisma Clientを使用してレコードの取得、作成、更新を行う際にリレーションがどのように現れるかを示しています。
レコードとネストされたレコードの作成
以下のクエリは、1つのUser
レコードと2つの関連するPost
レコードを作成します。
const userAndPosts = await prisma.user.create({
data: {
posts: {
create: [
{ title: 'Prisma Day 2020' }, // Populates authorId with user's id
{ title: 'How to write a Prisma schema' }, // Populates authorId with user's id
],
},
},
})
基盤となるデータベースでは、このクエリは以下のようになります。
- 自動生成された
id
(例:20
)を持つUser
を作成します。 - 2つの新しい
Post
レコードを作成し、両方のレコードのauthorId
を20
に設定します。
レコードを取得し、関連レコードを含める
以下のクエリは、id
でUser
を取得し、関連するPost
レコードを含めます。
const getAuthor = await prisma.user.findUnique({
where: {
id: "20",
},
include: {
posts: true, // All posts where authorId == 20
},
});
基盤となるデータベースでは、このクエリは以下のようになります。
id
が20
のUser
レコードを取得します。authorId
が20
のすべてのPost
レコードを取得します。
既存のレコードを別の既存のレコードに関連付ける
以下のクエリは、既存のPost
レコードを既存のUser
レコードに関連付けます。
const updateAuthor = await prisma.user.update({
where: {
id: 20,
},
data: {
posts: {
connect: {
id: 4,
},
},
},
})
基盤となるデータベースでは、このクエリはネストされたconnect
クエリを使用して、id
が4の投稿をid
が20のユーザーにリンクします。クエリは以下の手順でこれを行います。
- クエリはまず、
id
が20
のユーザーを検索します。 - 次にクエリは
authorID
外部キーを20
に設定します。これにより、id
が4
の投稿がid
が20
のユーザーにリンクされます。
このクエリでは、authorID
の現在の値は関係ありません。クエリは、現在の値にかかわらず、authorID
を20
に変更します。
リレーションの種類
Prisma ORMには3つの異なるタイプ(またはカーディナリティ)のリレーションがあります。
以下のPrismaスキーマには、すべてのタイプのリレーションが含まれています。
- 1対1:
User
↔Profile
- 1対多:
User
↔Post
- 多対多:
Post
↔Category
- リレーショナルデータベース
- MongoDB
model User {
id Int @id @default(autoincrement())
posts Post[]
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int @unique // relation scalar field (used in the `@relation` attribute above)
}
model Post {
id Int @id @default(autoincrement())
author User @relation(fields: [authorId], references: [id])
authorId Int // relation scalar field (used in the `@relation` attribute above)
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
posts Post[]
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
posts Post[]
profile Profile?
}
model Profile {
id String @id @default(auto()) @map("_id") @db.ObjectId
user User @relation(fields: [userId], references: [id])
userId String @unique @db.ObjectId // relation scalar field (used in the `@relation` attribute above)
}
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
author User @relation(fields: [authorId], references: [id])
authorId String @db.ObjectId // relation scalar field (used in the `@relation` attribute above)
categories Category[] @relation(fields: [categoryIds], references: [id])
categoryIds String[] @db.ObjectId
}
model Category {
id String @id @default(auto()) @map("_id") @db.ObjectId
posts Post[] @relation(fields: [postIds], references: [id])
postIds String[] @db.ObjectId
}
このスキーマはデータモデルの例と同じですが、スカラフィールド(必須のリレーションスカラフィールドを除く)がすべて削除されており、リレーションフィールドに焦点を当てることができます。
この例では暗黙の多対多リレーションを使用しています。これらのリレーションは、リレーションの曖昧さを解消する必要がある場合を除き、@relation
属性を必要としません。
リレーショナルデータベースとMongoDBでは、特に多対多リレーションの構文が若干異なることに注意してください。
リレーショナルデータベースの場合、以下のエンティティ関係図は、サンプルPrismaスキーマに対応するデータベースを表します。
MongoDBの場合、Prisma ORMは正規化されたデータモデル設計を使用しており、これはリレーショナルデータベースと同様に、ドキュメントがIDによって互いを参照することを意味します。MongoDBセクションで詳細を確認してください。
暗黙的および明示的な多対多リレーション
リレーショナルデータベースにおける多対多リレーションは、2つの方法でモデル化できます。
- 明示的な多対多リレーション。この場合、リレーションテーブルはPrismaスキーマに明示的なモデルとして表現されます。
- 暗黙的な多対多リレーション。この場合、Prisma ORMがリレーションテーブルを管理し、Prismaスキーマには表示されません。
暗黙的な多対多リレーションでは、両方のモデルが単一の@id
を持つ必要があります。以下の点に注意してください。
- 複数フィールドのIDを使用することはできません。
@id
の代わりに@unique
を使用することはできません。
これらの機能のいずれかを使用するには、代わりに明示的な多対多を設定する必要があります。
暗黙的な多対多リレーションは、基盤となるデータベースではリレーションテーブルとして依然として現れます。ただし、Prisma ORMがこのリレーションテーブルを管理します。
明示的な多対多リレーションの代わりに暗黙的な多対多リレーションを使用すると、Prisma Client APIがよりシンプルになります(例えば、ネストされた書き込みのネストレベルが1つ減るためです)。
Prisma Migrateを使用せず、イントロスペクションからデータモデルを取得する場合でも、Prisma ORMのリレーションテーブルの規則に従うことで、暗黙的な多対多リレーションを利用できます。
リレーションフィールド
リレーションフィールドは、Prisma モデル上のフィールドであり、スカラ型を持ちません。代わりに、その型は別のモデルです。
すべてのリレーションには、各モデルに1つずつ、厳密に2つのリレーションフィールドが必要です。1対1および1対多リレーションの場合、@relation
属性の2つのリレーションフィールドのいずれかによってリンクされる追加のリレーションスカラフィールドが必要です。このリレーションスカラフィールドは、基盤となるデータベースにおける外部キーを直接表現するものです。
- リレーショナルデータベース
- MongoDB
model User {
id Int @id @default(autoincrement())
email String @unique
role Role @default(USER)
posts Post[] // relation field (defined only at the Prisma ORM level)
}
model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id]) // relation field (uses the relation scalar field `authorId` below)
authorId Int // relation scalar field (used in the `@relation` attribute above)
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
email String @unique
role Role @default(USER)
posts Post[] // relation field (defined only at the Prisma ORM level)
}
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
author User @relation(fields: [authorId], references: [id]) // relation field (uses the relation scalar field `authorId` below)
authorId String @db.ObjectId // relation scalar field (used in the `@relation` attribute above)
}
posts
とauthor
はどちらもリレーションフィールドです。なぜなら、それらの型はスカラ型ではなく、他のモデルだからです。
また、アノテーション付きリレーションフィールドであるauthor
が、@relation
属性内でPost
モデル上のリレーションスカラフィールドauthorId
をリンクする必要があることにも注意してください。リレーションスカラフィールドは、基盤となるデータベースにおける外部キーを表します。
両方のリレーションフィールド(すなわち、posts
とauthor
)は純粋にPrisma ORMレベルで定義されており、データベースには現れません。
アノテーション付きリレーションフィールド
リレーションの片側が@relation
属性でアノテーションされる必要があるリレーションは、アノテーション付きリレーションフィールドと呼ばれます。これには以下が含まれます。
- 1対1リレーション
- 1対多リレーション
- MongoDBのみの多対多リレーション
@relation
属性でアノテーションされたリレーション側は、基盤となるデータベースに外部キーを保存する側を表します。外部キーを表す「実際の」フィールドもそのリレーション側に必要であり、それはリレーションスカラフィールドと呼ばれ、@relation
属性内で参照されます。
- リレーショナルデータベース
- MongoDB
author User @relation(fields: [authorId], references: [id])
authorId Int
author User @relation(fields: [authorId], references: [id])
authorId String @db.ObjectId
スカラフィールドは、@relation
属性のfields
で使用されると、リレーションスカラフィールドになります。
リレーションスカラフィールド
リレーションスカラフィールドの命名規則
リレーションスカラフィールドは常にリレーションフィールドに属するため、以下の命名規則が一般的です。
- リレーションフィールド:
author
- リレーションスカラフィールド:
authorId
(リレーションフィールド名 +Id
)
@relation
属性
@relation
属性は、リレーションフィールドにのみ適用でき、スカラフィールドには適用できません。
@relation
属性は以下の場合に必要です。
- 1対1または1対多リレーションを定義する場合、リレーションの片側に(対応するリレーションスカラフィールドとともに)必要です。
- リレーションの曖昧さを解消する必要がある場合(例えば、同じモデル間に2つのリレーションがある場合など)。
- 自己リレーションを定義する場合。
- MongoDBの多対多リレーションを定義する場合。
- 基盤となるデータベースでリレーションテーブルがどのように表現されるかを制御する必要がある場合(例: リレーションテーブルに特定の名前を使用する)。
注: リレーショナルデータベースにおける暗黙の多対多リレーションは、
@relation
属性を必要としません。
リレーションの曖昧さ解消
同じ2つのモデル間に2つのリレーションを定義する場合、それらの曖昧さを解消するために@relation
属性にname
引数を追加する必要があります。なぜそれが必要なのかの例として、以下のモデルを考えてみましょう。
- リレーショナルデータベース
- MongoDB
// NOTE: This schema is intentionally incorrect. See below for a working solution.
model User {
id Int @id @default(autoincrement())
name String?
writtenPosts Post[]
pinnedPost Post?
}
model Post {
id Int @id @default(autoincrement())
title String?
author User @relation(fields: [authorId], references: [id])
authorId Int
pinnedBy User? @relation(fields: [pinnedById], references: [id])
pinnedById Int?
}
// NOTE: This schema is intentionally incorrect. See below for a working solution.
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String?
writtenPosts Post[]
pinnedPost Post?
}
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String?
author User @relation(fields: [authorId], references: [id])
authorId String @db.ObjectId
pinnedBy User? @relation(fields: [pinnedById], references: [id])
pinnedById String? @db.ObjectId
}
その場合、リレーションは曖昧であり、解釈には4つの異なる方法があります。
User.writtenPosts
↔Post.author
+Post.authorId
User.writtenPosts
↔Post.pinnedBy
+Post.pinnedById
User.pinnedPost
↔Post.author
+Post.authorId
User.pinnedPost
↔Post.pinnedBy
+Post.pinnedById
これらのリレーションの曖昧さを解消するには、リレーションフィールドに@relation
属性をアノテーションし、name
引数を指定する必要があります。任意のname
(空の文字列""
を除く)を設定できますが、リレーションの両側で同じである必要があります。
- リレーショナルデータベース
- MongoDB
model User {
id Int @id @default(autoincrement())
name String?
writtenPosts Post[] @relation("WrittenPosts")
pinnedPost Post? @relation("PinnedPost")
}
model Post {
id Int @id @default(autoincrement())
title String?
author User @relation("WrittenPosts", fields: [authorId], references: [id])
authorId Int
pinnedBy User? @relation("PinnedPost", fields: [pinnedById], references: [id])
pinnedById Int? @unique
}
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String?
writtenPosts Post[] @relation("WrittenPosts")
pinnedPost Post? @relation("PinnedPost")
}
model Post {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String?
author User @relation("WrittenPosts", fields: [authorId], references: [id])
authorId String @db.ObjectId
pinnedBy User? @relation("PinnedPost", fields: [pinnedById], references: [id])
pinnedById String? @unique @db.ObjectId
}