服务配置:数据建模(SDL)

本文属使用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!
}

这个例子展示了使用数据模型时的一些重要概念:

  • TweetUserLocation这三种类型映射到数据库中的表(table)。
  • User和Tweet之间存在双向关系
  • TweetLocation之间存在单向关系
  • User上的name字段外,数据模型中所有字段都是必须的(如类型后面的!所示)
  • idcreatedAtupdatedAt字段由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将生成一个包含QueryMutationSubscription根类型的实际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(意味着它包含QueryMutationSubscription根类型),它定义暴露给客户端应用程序的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
  • 字段:idtextisPublished(默认值为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)

布尔量其值truefalse

如: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)

枚举被定义在范围内。

像布尔值一样,枚举可以有一组预定义的值。区别在于您可以定义可能的值。例如,您可以通过创建具有可能值COMPACTWIDECOVER的Enum来指定文章的格式。

注意:枚举值最多可以有191个字符长。

在查询或突变中,必须指定Enum字段,而不使用任何包围字符。您只能使用您为枚举定义的值:enum:COMPACTenum: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类型的email字段,其中假设每个user都有唯一的电子邮件地址。

注意,只有字符串字段中的前191个字符才被认为是唯一性的,并且唯一性检查是不区分大小写的。 如果前191个字符相同或仅大小写不同,则不能存储两个不同的字符串。

要将某个字段标记为唯一,只需将@unique添加到其后面:

type User {
  email: String! @unique
  age: Int!
}

对于每个使用@unique标记的字段,您都可以通过为该字段提供一个值来查询相应的节点。

例如,对于上述数据模型,您现在可以通过其电子邮件地址检索特定的用户节点:

query {
  user(where: {
    email: "[email protected]"
  }) {
    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)也是如此

系统字段

三个字段idcreatedAtupdatedAt都有特殊的含义。 它们在您的数据模型中是可选的,但将始终保留在底层数据库中。 这样,您可以随后将字段添加到数据模型中,并且数据将可用于现有节点。

这些字段的值目前在GraphQL API中是只读的(除了导入数据时).

请注意,您不能拥有称为idcreatedAtupdatedAt的自定义字段,因为这些字段名称是为系统字段保留的。以下是这三个字段唯一支持的声明:

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
}

createdAtupdatedAt

数据模型还提供了两个可以添加到类型中的特殊字段:

  • 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。因此用户地址的字段将是AddressAddress!类型。

包含必需的一对一关系字段的类型的节点只能使用嵌套突变(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指令进行注释。

这里我们定义了UserStory类型之间的双向一对多关系。由于没有提供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参数

在某些情况下,您的数据模型可能包含不明确的关系。例如,考虑你不仅想要一个关系来表达UserStory之间的“作者关系”,而且你还需要一个关系来表达一个用户喜欢哪个故事节点。

在这种情况下,UserStory之间会有两种不同的关系!为了消除它们的歧义,你需要给关系一个名字:

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参数,则无法确定书写的作品是否应与authorlikedBy字段相关联。

使用@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, 字段名称为userposts`
  • Appointments , EmployeeOnAppointment , AppointmentEmployee , 字段名称为 employeeappointments

枚举

枚举值只能包含字母、数字和下划线,并且需要以大写字母开头。枚举值的名称可以用于查询过滤器和突变。它们最多可以包含191个字符

枚举名称在服务级别上是唯一的。

枚举值名称在枚举级别上是唯一的。

示例

  • A
  • ROLE_TAG
  • RoleTag

更多SDL功能

在本节中,我们将介绍尚未支持用Prisma进行数据建模的更多SDL功能。

接口

“与许多类型的系统一样,GraphQL支持接口。接口是一种抽象类型,包含一组必须包含的用于实现接口的字段。”——官方GraphQL文档

Union

“联合类型与接口非常相似,但它们不能指定类型之间的任何公共字段。”——官方GraphQL文档

你可能感兴趣的:(服务配置:数据建模(SDL))