本文属使用Prisma构建GraphQL服务系列。
概述
Prisma使用GraphQL Schema Definition Language(SDL)进行数据建模。您的数据模型是用一个或多个.graphql
文件编写的,并且是Prisma在底层生成的实际数据库schema的基础。如果您使用单个文件来进行类型定义,则该文件通常称为datamodel.graphql
。
要了解有关SDL的更多信息,可以查看官方GraphQL文档。
包含数据模型的.graphql
文件,需要在prisma.yml
文件中datamodel
属性下的指定。例如:
datamodel:
- types.graphql
- enums.graphql
如果只有一个文件定义数据模型,则可以如下方式指定:
datamodel: datamodel.graphql
数据模型是Prisma服务的GraphQL API的基础。基于数据模型,Prisma将生成一个强大的GraphQL schema(称为Prisma database schema),该schema为数据模型中的类型定义了CRUD操作。
GraphQL模式定义了GraphQL API的操作。它实际上是用SDL编写的类型集合(SDL还支持接口,枚举,联合类型等基元,您可以在这里了解有关GraphQL类型系统的内容)。 GraphQL schema有三种特殊的根类型:查询,突变和订阅。这些类型定义了API的入口点并定义了API将接受的操作。要了解更多关于GraphQL schema的信息,请查看这篇文章。
示例
一个简单的datamodel.graphql
文件:
type Tweet {
id: ID! @unique
createdAt: DateTime!
text: String!
owner: User!
location: Location!
}
type User {
id: ID! @unique
createdAt: DateTime!
updatedAt: DateTime!
handle: String! @unique
name: String
tweets: [Tweet!]!
}
type Location {
latitude: Float!
longitude: Float!
}
这个例子展示了使用数据模型时的一些重要概念:
-
Tweet
,User
和Location
这三种类型映射到数据库中的表(table)。 -
User
和Tweet之间存在双向关系 -
Tweet
与Location
之间存在单向关系 - 除
User
上的name
字段外,数据模型中所有字段都是必须的(如类型后面的!
所示) -
id
,createdAt
和updatedAt
字段由Prisma管理,在公开的GraphQL API中为只读(意味着它们不能通过突变进行更改)。
创建和更新数据模型与编写文本文件一样简单。对数据模型满意后,您可以通过运行prisma deploy
将更改应用到Prisma服务:
$ prisma deploy
Changes:
Tweet (Type)
+ Created type `Tweet`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `text` of type `String!`
+ Created field `owner` of type `Relation!`
+ Created field `location` of type `Relation!`
+ Created field `updatedAt` of type `DateTime!`
User (Type)
+ Created type `User`
+ Created field `id` of type `GraphQLID!`
+ Created field `createdAt` of type `DateTime!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `handle` of type `String!`
+ Created field `name` of type `String`
+ Created field `tweets` of type `[Relation!]!`
Location (Type)
+ Created type `Location`
+ Created field `latitude` of type `Float!`
+ Created field `longitude` of type `Float!`
+ Created field `id` of type `GraphQLID!`
+ Created field `updatedAt` of type `DateTime!`
+ Created field `createdAt` of type `DateTime!`
TweetToUser (Relation)
+ Created relation between Tweet and User
LocationToTweet (Relation)
+ Created relation between Location and Tweet
Applying changes... (22/22)
Applying changes... 0.4s
数据模型的构建块
有几种可用的构建模块来构造您的数据模型:
- 类型由多个字段组成,用于将相似的实体组合在一起。数据模型中的每种类型都映射到数据库,并将CRUD操作添加到GraphQL schema中。
- 关系描述类型之间的关系。
- 接口是抽象类型,它包含一组必须包含的用于实现接口的字段。目前,接口不能由用户定义,但有高级接口支持的待定功能请求。
- 特殊指令涵盖不同的用例,如类型约束或级联删除行为。
本页面的其余部分将更详细地介绍这些构建块。
Prisma database schema 与 Data Model
当开始使用GraphQL和Prisma时,您正在使用的.graphql
文件的数量可能会令人困惑。然而,了解每个的角色至关重要。
通常,.graphql
文件可以包含以下任一项:
- GraphQL操作(即查询,突变或订阅)
- SDL中的GraphQL类型定义
在区分Prisma database schema 与 Data Model的前提下,只有后者才相关!
请注意,并非每个后一类别的.graphql
文件都是有效的GraphQL schema。正如上面的提到的,GraphQL schema的特点是它有三种根类型:查询,突变和订阅以及API所需的任何其他类型。
现在,通过该定义,数据模型实际上并不是GraphQL schema,尽管它是用SDL编写的.graphql
文件。它缺少根类型,因此实际上并没有定义API操作! Prisma像是使用数据模型作为一个方便的工具来表示数据模型。
如上所述,Prisma将生成一个包含Query
,Mutation
和Subscription
根类型的实际GraphQL schema。该schema通常作为prisma.graphql
存储在您的项目中,并称为Prisma database schema。请注意,您不应该对此文件进行任何手动更改!
作为一个例子,请考虑以下非常简单的数据模型:
datamodel.graphql
type User {
id: ID! @uniue
name: String!
}
如果您将此数据模型部署到您的Prisma服务中,Prisma将生成以下Prisma database schema,该schema定义您的服务的GraphQL API:
prisma.graphql
type Query {
users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
user(where: UserWhereUniqueInput!): User
}
type Mutation {
createUser(data: UserCreateInput!): User!
updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
deleteUser(where: UserWhereUniqueInput!): User
}
type Subscription {
user(where: UserSubscriptionWhereInput): UserSubscriptionPayload
}
请注意,这是生成的schema的简化版本,您可以在此处找到完整的schema。
如果您已经考虑构建基于Prisma的自己的GraphQL服务,则可能会遇到另一个被称为application schema的.graphql文件。这是另一个适当的GraphQL schema(意味着它包含Query
,Mutation
和Subscription
根类型),它定义暴露给客户端应用程序的API。它使用底层的Prisma GraphQL API作为“查询引擎”来实际运行数据库的查询,变异和订阅。 基于Prisma的GraphQL服务通常具有两个GraphQL API,将它们视为服务的两个层次:
- 应用层:由应用模式定义(这里是您实现业务逻辑,认证,与第三方服务集成等的地方)
- 数据库层:由Prisma数据库服务定义
对象类型(Object types)
对象类型(简称为类型)定义数据模型的一个具体的结构。它用于表示应用程序域(application domain)的实体。
如果您熟悉SQL数据库,则可以将对象类型视为关系数据库中的表的schema。一个类型有一个名字和一个或多个字段。
一个类型的实例被称为节点(node)。这个术语是指数据图(data graph)中的一个节点。您在数据模型中定义的每种类型都将作为生成的Prisma database schema中的类似类型提供。
定义对象类型
在数据模型中使用type
关键字定义对象类型:
type Article {
id: ID! @unique
text: String!
isPublished: Boolean @default(value: "false")
}
上面定义的类型具有以下属性:
- 名称:
Article
- 字段:
id
、text
和isPublished
(默认值为false
)
为类型生成操作API
数据模型中的类型会影响Prisma GraphQL API中的可用操作。对于每种类型,
- query 允许获取一个或多个类型节点
- mutation 允许创建、修改或删除类型的节点
- subscription 允许当节点改变时获取通知(例如新节点添加,现有节点修改或删除)
字段(Fields)
字段是一种类型的构成,表示节点的形状。每个字段是标量或关系字段,可以使用其名称引用。
标量类型
字符串(string)
一个字符串String
为文本,用于username
类型、博客文章的内容或任何适合文本表示的内容。
注意:共享演示群集上的字符串值目前限制为256KB。使用集群(cluster)配置的其他群集可以提高此限制。
在查询或突变中,必须使用括住双引号(""
)来指定字符串字段:string:“some-string”
。
整型(nteger)
Int
是一个不能有小数的数字。使用它来存储值,例如配方所需的配料重量或最小年龄。
注意:
Int
值范围从-2147483648到2147483647。
在查询或突变中,指定Int
字段:int:42
。
浮点(Float)
浮点数(Float)是可以有小数的数字。使用它可以存储诸如商店中商品价格或复杂计算结果等值。
在查询或突变中,指定Float字段:float:42,float:4.2
。
布尔量(Boolean)
布尔量其值true
或false
。
如:boolean: true, boolean: false
日期时间(DateTime)
DateTime
类型可用于存储日期或时间值。一个很好的例子可能是一个人的出生日期。
在查询或突变中,必须用ISO 8601格式指定DateTime字段并加上双引号:
datetime: "2015"
datetime: "2015-11"
datetime: "2015-11-22"
datetime: "2015-11-22T13:57:31.123Z"
枚举(Enum)
枚举被定义在范围内。
像布尔值一样,枚举可以有一组预定义的值。区别在于您可以定义可能的值。例如,您可以通过创建具有可能值COMPACT
,WIDE
和COVER
的Enum来指定文章的格式。
注意:枚举值最多可以有191个字符长。
在查询或突变中,必须指定Enum字段,而不使用任何包围字符。您只能使用您为枚举定义的值:enum:COMPACT
,enum:WIDE
。
JSON
有时您需要为松散结构化的数据存储任意的Json值。 Json类型确保它实际上是有效的Json,并将该值作为解析的Json对象/数组而不是字符串返回。
注意:共享演示群集上的Json值目前限制为256KB。使用集群配置(cluster configuration)的其他群集可以提高此限制。
在查询或突变中,Json字段必须用双引号括起来。特殊字符必须转义:json: "{\"int\": 1, \"string\": \"value\"}"
。
ID
ID值是基于cuid生成的唯一的25个字符的字符串。具有ID值的字段是系统字段,只是在内部使用,因此无法使用ID类型创建新字段。
类型修饰符
列表(List)
标量字段可以用列表字段类型标记。具有多重性的关系的字段也将被标记为列表。
在查询或突变中,列表字段必须用方括号([]
)括起来,而列表中的每项遵守与上面标量列出的相同的格式化规则:listString: ["a string", "another string"], listInt: [12, 24]
必须(Required)
必须字段(有时也称为“非空”)。在创建新节点时,您需要为所需字段提供一个值,并且没有默认值。
必填字段在字段类型后使用!
标记:name:String!
.
字段约束
可以使用特定的字段约束来配置字段,以便将更多语义添加到数据模型中。
唯一(Unique)
设置unique
可确保相关类型的两个节点对于某个字段的值不能重复。唯一的例外是null
,这意味着多个节点可以具有null
值而不违反约束。
典型的例子是
User
类型的user
都有唯一的电子邮件地址。
注意,只有字符串字段中的前191个字符才被认为是唯一性的,并且唯一性检查是不区分大小写的。 如果前191个字符相同或仅大小写不同,则不能存储两个不同的字符串。
要将某个字段标记为唯一,只需将@unique
添加到其后面:
type User {
email: String! @unique
age: Int!
}
对于每个使用@unique
标记的字段,您都可以通过为该字段提供一个值来查询相应的节点。
例如,对于上述数据模型,您现在可以通过其电子邮件地址检索特定的用户节点:
query {
user(where: {
email: "alice@graph.cool"
}) {
age
}
}
默认值
您可以为标量字段设置默认值。在创建期间未提供值时,将为新节点采用该值。
要为字段指定默认值,可以使用@default
指令:
type Story {
isPublished: Boolean @default(value: "false")
someNumber: Int! @default(value: "42")
title: String! @default(value: "My New Post")
publishDate: DateTime! @default(value: "2018-01-26")
}
请注意,必须使用双引号提供默认值,即使对于非字符串类型(例如Boolean或Int)也是如此。
系统字段
三个字段id
,createdAt
和updatedAt
都有特殊的含义。 它们在您的数据模型中是可选的,但将始终保留在底层数据库中。 这样,您可以随后将字段添加到数据模型中,并且数据将可用于现有节点。
这些字段的值目前在GraphQL API中是只读的(除了导入数据时).
请注意,您不能拥有称为id
,createdAt
和updatedAt
的自定义字段,因为这些字段名称是为系统字段保留的。以下是这三个字段唯一支持的声明:
id: ID! @unique
createdAt: DateTime!
updatedAt: DateTime!
id
节点在创建时会自动获取全局唯一标识符,该标识符存储在id
字段中。
无论何时将id
字段添加到类型定义以将其暴露在GraphQL API中,您都必须使用@unique
指令对其进行标记。
该id
具有以下属性:
- 由25个字母数字字符组成(字母总是小写)
- 始终以(小写)字母c开头
- 遵循 cuid(collision resistant unique identifiers)方案
请注意,所有对象类型都将在数据库模式中实现Node
接口。这就是Node
接口的样子:
interface Node {
id: ID! @unique
}
createdAt
和updatedAt
数据模型还提供了两个可以添加到类型中的特殊字段:
-
createdAt:DateTime !
存储创建此对象类型的节点时的实际日期和时间。 -
updatedAt: DateTime!
存储上次更新此对象类型的节点时的确切日期和时间。
如果你想让你的类型公开这些字段,你可以简单地将它们添加到类型定义中,例如:
type User {
id: ID! @unique
createdAt: DateTime!
updatedAt: DateTime!
}
标量字段值迁移
您可以使用updateManyXs
突变为所有节点或仅特定子集迁移标量字段的值。
mutation {
# update the email of all users with no email address to the empty string
updateManyUsers(
where: {
email: null
}
data: {
email: ""
}
)
}
向数据模型添加一个必填字段
将必需的字段添加到已包含节点的模型时,您会收到此错误消息:
You are making a field required, but there are already nodes that would violate that constraint.您正在创建一个必需的字段,但已有节点会违反该限制。
这是因为所有节点对于这个字段都是空的。
以下是添加必填字段所需的步骤:
- 添加可选(
optional
)的字段。 - 使用
updateManyXs
将字段的所有节点从null
变为一个非空值。 - 现在您可以根据需要标记该字段并部署
关系
关系定义了两种类型之间连接的语义。 关系中的两种类型通过关系字段连接。 当关系可能不明确时,关系字段需要用@relation
指令注释以消除它的歧义。
一个关系也可以将一个类型与自己连接起来——称为自我关系。
所需的关系
对于一对一关系字段,您可以配置它是必需的还是可选的。所需的标志在GraphQL中充当契约,该字段不能为null
。因此用户地址的字段将是Address
或Address!
类型。
包含必需的一对一关系字段的类型的节点只能使用嵌套突变(nested mutation),以确保相关字段不会为null
。
请注意,多对多关系字段始终设置为必需。例如,包含许多用户地址的字段总是使用类型
[Address!]!
并且不能是[Address!]
类型。原因是,如果该字段不包含任何节点,将返回[]
,该值不为null
。
@relation 指令
定义类型之间的关系时,有@relation
指令提供关于关系的元信息。它可以有两个参数:
-
name
: 此关系的标识符(以字符串形式提供)。这个论点只有在关系不明确的情况下才需要。请注意,每次使用@relation
指令时,name
参数都是必需的。 -
onDelete
: 指定删除行为并启用级联删除。如果具有相关节点的节点被删除,则删除行为决定了相关节点应该发生什么。该参数的输入值被定义为具有以下可能值的枚举:-
SET_NULL
(默认):将相关节点设置为null
。 -
CASCADE
:删除相关的节点。请注意,无法将双向关系的两端设置为CASCADE
。
-
以下是使用@relation
指令的数据模型示例:
type User {
id: ID! @unique
stories: [Story!]! @relation(name: "StoriesByUser" onDelete: CASCADE)
}
type Story {
id: ID! @unique
text: String!
author: User @relation(name: "StoriesByUser")
}
本例中的删除行为如下所示:
- 当用户节点被删除时,其所有相关的
Story
节点也将被删除。 - 当
Story
节点被删除时,它将被简单地从相关User
节点上的Story
列表中删除。
省略@relation
指令
在最简单的情况下,如果两种类型之间的关系是明确的并且应该应用缺省删除行为(SET_NULL
),则相应的关系字段不必使用@relation
指令进行注释。
这里我们定义了User
和Story
类型之间的双向一对多关系。由于没有提供onDelete
,所以使用了SET_NULL
默认的删除行为:
type User {
id: ID! @unique
stories: [Story!]!
}
type Story {
id: ID! @unique
text: String!
author: User
}
删除行为如下:
- 当用户节点被删除时,其所有相关
Story
节点上的author
字段将被设置为null
。请注意,如果author
字段被标记为必需,则操作会导致错误。 - 当
Story
节点被删除时,它将被简单地从相关User
节点上的Story
列表中删除。
使用@relation
指令的name
参数
在某些情况下,您的数据模型可能包含不明确的关系。例如,考虑你不仅想要一个关系来表达User
和Story
之间的“作者关系”,而且你还需要一个关系来表达一个用户喜欢哪个故事节点。
在这种情况下,User
和Story
之间会有两种不同的关系!为了消除它们的歧义,你需要给关系一个名字:
type User {
id: ID! @unique
writtenStories: [Story!]! @relation(name: "WrittenStories")
likedStories: [Story!]! @relation(name: "LikedStories")
}
type Story {
id: ID! @unique
text: String!
author: User! @relation(name: "WrittenStories")
likedBy: [User!]! @relation(name: "LikedStories")
}
如果在这种情况下未提供name
参数,则无法确定书写的作品是否应与author
或likedBy
字段相关联。
使用@relation
指令的onDelete
参数
如上所述,您可以为相关节点指定专门的删除行为。这就是@relation
指令的onDelete
参数所要做的。
考虑下面的例子:
type User {
id: ID! @unique
comments: [Comment!]! @relation(name: "CommentAuthor", onDelete: CASCADE)
blog: Blog @relation(name: "BlogOwner", onDelete: CASCADE)
}
type Blog {
id: ID! @unique
comments: [Comment!]! @relation(name: "Comments", onDelete: CASCADE)
owner: User! @relation(name: "BlogOwner", onDelete: SET_NULL)
}
type Comment {
id: ID! @unique
blog: Blog! @relation(name: "Comments", onDelete: SET_NULL)
author: User @relation(name: "CommentAuthor", onDelete: SET_NULL)
}
我们来研究这三种类型的删除行为:
- 当
User
节点被删除时,- 所有相关的
Comment
节点将被删除。 - 相关的
Blog
节点将被删除。
- 所有相关的
- 当
Blog
节点被删除- 所有相关的
Comment
节点将被删除。 - 相关的
User
节点将其Blog
字段设置为null
。
- 所有相关的
- 当
Comment
节点被删除时,- 相关的
Blog
节点将继续存在,并将删除的Comment
节点从其comments
列表中删除。 - 他相关
User
节点将继续存在,并将删除的Comment
节点从其comments
列表中删除。
- 相关的
为关系生成API操作
包含在您的schema
中的关系会影响GraphQL API中的可用操作。对于每一个关系,
- 关系查询(relation queries)允许您跨类型查询数据或为关系聚合(请注意,使用Relay的连接模型connection model也可以)
- 嵌套突变(nested mutations)允许您创建,连接,更新,插入和删除不同类型的节点
- 关系订阅(relation subscriptions)允许您获得有关关系更改的通知
GraphQL指令
指令用于在数据模型中提供附加信息。它们看起来像这样:@name(argument: "value")
或者当没有参数时只是@name
。
数据模型指令
数据模型指令描述了关于GraphQL schema中的类型或字段的附加信息。
唯一标量字段
@unique
指令将标量字段标记为唯一(unique)。唯一字段将在底层数据库中应用唯一索引。
# the `User` type has a unique `email` field
type User {
email: String @unique
}
关系字段
可以将指令@relation(name:String,onDelete:ON_DELETE!=NO_ACTION)
附加到关系字段。
标量字段的默认值
指令@default(value: String!)
为标量字段设置默认值。请注意,value
参数对于所有标量字段都是String
类型(即使字段本身不是字符串):
# the `title`, `published` and `someNumber` fields have default values `New Post`, `false` and `42`
type Post {
title: String! @default(value: "New Post")
published: Boolean! @default(value: "false")
someNumber: Int! @default(value: "42")
}
临时指令
临时指令用于执行一次性迁移操作。部署包含临时指令的服务后,需要从类型定义文件中手动删除它。
重命名类型或字段
临时指令@rename(oldName: String!)
用于重命名类型或字段。
# renaming the `Post` type to `Story`, and its `text` field to `content`
type Story @rename(oldName: "Post") {
content: String @rename(oldName: "text")
}
如果没有使用rename指令,Prisma会在创建新类型和字段之前删除旧类型和字段,导致数据丢失!
命名约定
您在Prisma服务中遇到的不同对象(如类型或关系)遵循单独的命名约定来帮助区分它们。
类型
类型名称决定派生查询和变异的名称以及嵌套变异的参数名称。类型名称只能包含字母和数字,并且需要以大写字母开头。它们最多64个字符。
建议以单数形式选择类型名称。
类型名称在服务级别上是唯一的。
示例:
Post
PostCategory
标量和关系字段
标量字段的名称用于查询和突变的查询参数中。字段名称只能包含字母和数字,并且需要以小写字母开头。它们最多可以包含64个字符。
关系字段的名称遵循相同的约定,并确定关系突变的参数名称。
建议只为列表字段选择复数名称。
字段名称在类型级别上是唯一的。
示例:
name
email
categoryTags
关系
关系名称只能包含字母和数字,并且需要以大写字母开头。它们最多可以包含64个字符。 关系名称在服务级别上是唯一的。
示例:
-
UserOnPost
,UserPosts
,PostAuthor, 字段名称为
user和
posts` -
Appointments
,EmployeeOnAppointment
,AppointmentEmployee
, 字段名称为employee
和appointments
枚举
枚举值只能包含字母、数字和下划线,并且需要以大写字母开头。枚举值的名称可以用于查询过滤器和突变。它们最多可以包含191个字符。
枚举名称在服务级别上是唯一的。
枚举值名称在枚举级别上是唯一的。
示例
A
ROLE_TAG
RoleTag
更多SDL功能
在本节中,我们将介绍尚未支持用Prisma进行数据建模的更多SDL功能。
接口
“与许多类型的系统一样,GraphQL支持接口。接口是一种抽象类型,包含一组必须包含的用于实现接口的字段。”——官方GraphQL文档
Union
“联合类型与接口非常相似,但它们不能指定类型之间的任何公共字段。”——官方GraphQL文档