datamodel有两个主要功能:

  • 定义底层数据库模式和表(在无模式数据库(如MongoDB)的情况下,它们映射到数据库表或等效结构,如documents)。
  • 它是Prisma API的自动生成的CRUD和实时操作代码的基础。 了解更多

数据模型使用GraphQL的 schema定义语言SDL(SDL)编写,并存储在一个或多个.graphql后缀的文件中。这些.graphql文件需要在prisma.ymldatamodel属性中引用。例如:


endpoint: __YOUR_PRISMA_ENDPOINT__
datamodel: datamodel.prisma

有几个可用的构建块来塑造你的数据模型。

  • Types由多个fields字段组成,通常表示应用程序中的实体(例如UserCarOrder)。数据模型中的每种类型都映射到数据库表(或无模式数据库的等效结构),并且将CRUD操作添加到GraphQL schema中。
  • Relations描述类型之间的relationship关系
  • Directives指令涵盖不同的用例,例如类型约束或级联删除行为。
  • Interfaces是抽象类型,包括一组字段,类型必须包含在implement接口中。接口目前不适用于Prisma中的数据建模,但将来会支持待实现功能

SDL用于数据建模的主要原因有两个:

  • SDL是一种 直观,简洁,简洁的表达类型定义的方式,因此有助于提供良好的开发人员体验
  • 使用GraphQL SDL定义用作GraphQL API基础的模型是一种 惯用的方法。

没有严格的技术要求必须使用SDL,在未来Prisma可能允许其他方法提供模型定义。

要了解有关SDL的更多信息,你可以查看官方GraphQL文档或阅读实际规范

一个简单的datamodel.prisma文件例子:


type Tweet {
  id: ID! @id
  createdAt: DateTime! @createdAt
  text: String!
  owner: User! @relation(link: INLINE)
  location: Location!
}
type User {
  id: ID! @id
  createdAt: DateTime! @createdAt
  updatedAt: DateTime! @updatedAt
  handle: String! @unique
  name: String
  tweets: [Tweet!]!
}
type Location {
  latitude: Float!
  longitude: Float!
}

此示例说明了使用数据模型时的一些重要概念:

  • 三种类型TweetUserLocation被映射到数据库表(或无模式数据库的等效结构)。
  • "User"和"Tweet"之间存在双向关系(通过ownertweets字段)。@relation(link: INLINE)表示这个关系表现为在Tweet表上的外键。
  • TweetLocation(通过location字段)有一个单向关系。他们的关系通过中间表来实现。
  • 除了User上的name字段外,所有字段在数据模型中都是必填字段(由类型后面的!表示)。
  • idcreatedAtupdatedAt字段由Prisma管理,在暴露的Prisma API中是只读的(意味着它们不能通过Mutation改变)。
  • @unique指令表示唯一约束,这意味着Prisma会自动确保永远不会有两条数据具有相同的值,比如说id或手机号不会相同从而造成重复注册。

创建和更新数据模型就像编写和保存datamodel文件一样简单。一旦对数据模型感到满意,就可以通过运行prisma deploy来保存文件并将更改应用到Prisma service:


$ 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

当使用GraphQL和Prisma时,你正在使用的.graphql后缀的文件的作用可能会令人困惑。所以,了解每一个的功能至关重要。

我们看在Prisma项目中,有三个相关的.graphql后缀的文件:

  • datamodel包含服务API的数据模型定义,通常称为datamodel.prisma
  • Prisma GraphQL schema定义服务API(也就是前端能操作和请求)的实际CRUD和实时操作(realtime operations),通常称为prisma.graphql
  • 应用程序schema定义通过http请求能够执行什么样的操作,不像上面的prisma.graphql包含所有操作,应用程序schema只需要写你能用到的,或去掉危险操作比如删除所有用户这种。所有在这里定义的Query、Mutation和Subscription都要一一对应写resolvers来决定返回的数据。通常为schema.graphqlapp.graphql.

Prisma数据库的表基于datamodel生成:

该图展示了生成的Prisma GraphQL schema的简化版本,你可以找到完整的schema此处

GraphQL schema定义了GraphQL API的操作。它实际上是用SDL编写的types集合(SDL还支持接口,枚举,联合类型等原语,你可以学习有关GraphQL类型系统的所有内容这里)。

GraphQL schema有三个特殊的根类型:root types:QueryMutationSubscription。这些类型定义API的入口点(类似路由的url):entry points定义API将接受的操作。要了解有关GraphQL schema的更多信息,请查看此文章

数据模型由Prisma service的开发者手动编写。它定义了开发人员想要在其API和数据库中使用的模型和结构。

严格来说,datamodel不是有效的GraphQL schema,因为它不包含任何GraphQL的根类型(QueryMutationSubscription),因此不定义任何API操作。

数据模型仅是生成Prisma GraphQL API的实际GraphQL schema的基础

可以使用Prisma.yml中的graphql-schema generator从Prisma service下载Prisma GraphQL schema:


generate:
  - generator: `graphql-schema`
    output: .src/generated/prisma.graphql

现在您可以运行prisma generate,Prisma CLI将在./prisma-schema/prisma.graphql中存储Prisma GraphQL Schema。

在实际开发设置中,你希望保持本地Prisma GraphQL schema与服务的API同步,并确保在每次部署Prisma service时更新它。你可以使用post-deployment 来实现。

.graphqlconfig.yml和prisma.yml在设置好的样子:

prisma.yml


datamodel: datamodel.prisma
endpoint: http://localhost:4466/myservice/dev
secret: mysecret
hooks:
  post-deploy:
    - prisma generate

如果你已经考虑过基于Prisma构建自己的GraphQL服务器,你可能会遇到另一个.graphql文件,它被称为你的应用程序schema(通常为schema.graphqlapp.graphql)。

这是一个有效的GraphQL schema(意味着它包含QueryMutationSubscription根类型),它定义了应用程序层的API。与Prisma的_通用CRUD API相比,应用程序层的API应该公开针对前端端应用程序需求而定制的特定的操作。

应用程序层使用Prisma作为data访问层并将传入的请求委托给服务端的Prisma API,它们实际上是针对数据库解析的。

你通常不会在应用程序schema中重新定义模型定义。相反,你使用graphql-importimport他们:

schema.graphql中如下,在这里#符号不是注释,而是特定导入语法。


# import Post from "prisma.graphql"
type Query {
  feed: [Post!]!
}

graphql-import目前使用SDL注释来导入类型。 将来,SDL可能会有一个正式的导入语法已经讨论过

阅读 深入理解Prisma了解有关应用程序schema的更多信息。

你可以将你的数据模型编写在一个.graphql文件中,或者将其分成多个。

包含datamodel的.graphql文件需要在prisma.yml下的属性datamodel中指定。 例如:


datamodel:
  - types.graphql
  - enums.graphql

如果只有一个文件定义了数据模型,则可以按如下方式指定:


datamodel: datamodel.prisma

object type(简称 type)定义数据模型中一个model的结构。 它用于表示应用领域中的实体

每个对象类型都映射到数据库。 对于关系数据库,每种类型创建一个table。 对于无模式数据库,使用等效结构(例如document)。 请注意,即使对于无模式数据库,Prisma也会强制执行模式!

类型具有name和一个或多个fields字段。 类型名称只能包含 字母、数字、字符,需要以大写字母开头。 它们可以包含 最多64个字符

类型的实例化称为node。 该术语指的是data graph中的节点。

你在数据模型中定义的每种类型都将在生成的Prisma GraphQL schema中作为类似类型提供。

在datamodel中使用关键字"type"定义对象类型:


type Article {
  id: ID! @id
  title: String!
  text: String
  isPublished: Boolean! @default(value: "false")
}

上面定义的类型具有以下属性:

  • Name: Article
  • Fields: id, title, text , isPublished (默认为 false)

id , title , isPublished 是必填字段 (被后面的 ! 符号定义), text 可选填.

所有ID 都必须是@unique

数据模型中的类型会影响Prisma API中的可用操作。以下是Prisma API中每种类型生成的CRUD和实时操作的概述:

  • Query允许你获取该类型的一个或多个节点
  • Mutations允许你创建,更新或删除该类型的节点
  • Subscriptions可以让你收到有关该类型节点更改的实时通知(即新节点是created或现有节点是updateddeleted)

Fields[type]的构建块,为节点提供shape。每个字段都由其名称引用,并且是标量关系字段。

字段名称只能包含 字母数字字符,需要以小写字母开头。它们可以包含 最多64个字符

通俗来说就是数据库表的列column。

String保存文本。用于用户名的类型,博客文章的内容或能表示为文本的任何其他内容。

在Demo服务器上,字符串值当前限制为256KB。使用cluster配置可以在其他cluster上增加此限制。

这是一个String标量定义的例子:


type User {
  name: String
}

当在操作中用作参数时,必须使用用双引号括起来指定String字段:


query {
  user(name: "Sarah") {
    id
  }
}

一个Int是一个整数,不能有小数。 使用此选项可存储值,例如年龄。

Int值的范围是-2147483648到2147483647。

这是一个Int标量定义的例子:


type User {
  age: Int
}

当在操作中用作参数时,写Int字段不能包含任何封闭字符:


query {
  user(age: 42) {
    id
  }
}

Float是一个可以有小数的数字。 使用此选项可存储值,例如商品的价格或复杂计算的结果。

在查询或Mutation中,写Float字段,不带任何封闭字符,小数点为可选:float:42float:4.2

以下是"Float"标量定义的示例:


type Item {
  price: Float
}

当在操作中用作参数时,写Float字段,不带任何封闭字符,小数点可选:


query {
  item(priceLargerThan: 42.2) {
    id
  }
}

一个Boolean的值可以是trueFALSE。 在判断场景比较常见,例如用户是否想要接收电子邮件或者用户是否是会员。

以下是Boolean标量定义的示例:


type User {
  overEighteen: Boolean
}

同样不能有封闭字符


query {
  user(overEighteen: true) {
    id
  }
}

DateTime类型可用于存储日期和/或时间值。可能是一个人的出生日期或特定事件发生时的时间/数据。

这是一个DateTime标量定义的例子:


type User {
  birthday: DateTime
}

当在操作中用作参数时,必须在ISO 8601格式中指定DateTime字段,并带有双引号:


query {
  user(birthday: "2015-11-22"){
    id
  }
}

ISO 8601还接受以下格式:

  • datetime:"2015"
  • datetime:"2015-11"
  • datetime:"2015-11-22"
  • datetime:"2015-11-22T13:57:31.123Z"

像"Boolean"一样,枚举可以有一组预定义的值。 区别在于可以定义可能的值(而对于Boolean,选项被限制为truefalse)。 例如,你可以通过创建具有可能值"COMPACT","WIDE"和"COVER"的枚举来指定文章的格式。

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

以下是枚举定义datamodel的示例:


enum ArticleFormat {
  COMPACT,
  WIDE,
  COVER
}
type Article {
  format: ArticleFormat
}

当在操作中用作参数时,必须在不包含双引号的情况下指定枚举字段:


query {
  article(format: COMPACT) {
    id
  }
}

有时你可能需要为松散结构化的数据存储任意JSON值。 Json类型确保它实际上是有效的JSON并将值作为解析的JSON对象/数组而不是字符串返回。

Json值目前限制为256KB。

这是一个Json定义的例子:


type Item {
  data: Json
}

当在操作中用作参数时,必须使用双引号括起来指定的Json字段。 特殊字符必须被转义: json: "{"int": 1, "string": "value"}".


mutation {
  createItem(data: "{"int": 1, "string": "value"}") {
    data
  }
}

"ID"值是基于cuid生成的唯一25个字符的字符串。 具有"ID"值的字段是系统字段并且仅在内部使用,因此无法使用"ID"类型创建新字段。

ID字段只能在一个类型上使用一次,并且必须使用@unique指令进行注释:

在字段定义中,可以使用类型修饰符注释类型。 GraphQL支持两种类型修改:

  • 必填字段:使用!注释类型,例如 name:String!
  • List:使用一对封闭的[]来注释类型,例如: friends: [User]

一般在一对多关系中使用List。

可以使用列表字段类型标记标量字段。 具有多重性的关系字段也将被标记为列表。

经常会发现列表定义与此类似:


type Article {
  tags: [String!]! @scalarList(strategy: RELATION)
}

注意两个!类型修饰符,这是它们表达的内容:

  • 第一个!类型修饰符(在String之后)意味着列表中的任何项都不能为"null",例如 tags的这个值无效:["Software",null,"GraphQL"]
  • 第二个!类型修饰符(在结束方括号之后)意味着列表本身永远不能为"null",但它可能是empty。 因此,null不是tags字段的有效值,而[]是。

记住,所有一对多关系,必须有两个!,不然prisma deploy时会报错,因为返回值不能为null,用!后返回值哪怕为空也是[]

字段可以标记为必需(有时也称为"非空")。 创建新节点时,需要为所需的字段提供值,并且没有默认值

在字段类型后面使用!标记必填字段:


type User {
  name: String!
}

可以使用字段约束配置字段以添加进一步的语义并在数据模型中强制执行某些规则。

设置unique约束可确保所讨论类型的两个节点对于某个字段不能具有相同的值。 唯一的例外是null值,这意味着多个节点可以具有值null而不违反约束。 唯一字段在底层数据库中应用了唯一的索引

一个典型的例子是User类型的email字段,其中假设每个User应该具有全球唯一的电子邮件地址。

只有String字段中的前191个字符被认为是唯一性,唯一的检查是不区分大小写的。 如果前191个字符相同或者它们仅在大小写上不同,则无法存储两个不同的字符串。

要将字段标记为唯一,只需将@unique指令附加到其定义:


type User {
  id: ID! @id
  email: String! @unique
  name: String!
}

对于使用@unique注释的每个字段,你都可以通过为该字段提供值作为查询参数来查询相应的数据。

例如,考虑到上面的数据模型,你现在可以通过其email地址检索特定的User节点:


query {
  user(where: {
    email: "kwc@1wire.com"
  }) {
    name
  }
}

将很快添加更多数据库约束。 如果你希望在Prisma中看到某些约束,请加入此功能请求中的讨论。

你可以为非列表标量字段设置default值。 当在create-mutation期间没有提供任何值时,该值将应用于新创建的节点。

要指定字段的默认值,可以使用@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")
  status: Status! @default(value: PUBLIC)
}
enum Status {
  PRIVATE
  PUBLIC
}

三个字段idcreatedAtupdatedAt在Prisma中有特殊的语义。它们在你的数据模型中是可选的,但始终在基础数据库中存在。这样,你可以随时将字段添加到数据模型中,并且数据仍可用于现有节点。

这些字段的值当前在Prisma API中是只读的(导入数据时除外),但将来可以配置。有关详细信息,请参阅此提案

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

  • id:ID! @id
  • createdAt:DateTime! @createdAt
  • updatedAt: DateTime! @updatedAt

所有系统字段都需要标记为必填,并且id字段还需要使用@unique指令进行注释。

节点在创建时将自动分配一个全局唯一标识符,该标识符存储在id字段中。

每当你将id字段添加到类型定义中以在GraphQL API中公开它时,你必须使用@unique指令对其进行注释。

id具有以下属性:

由25个字母数字字符组成(字母总是小写) 始终以(小写)字母开头,例如c *遵循cuid(collision resistant unique identifiers)方案

请注意,Prisma GraphQL schema中的所有模型类型都将生成Node接口。这就是Node接口的样子:


interface Node {
  id: ID! @id
}

id 的类型可以为:

  • ID
  • Int

比如你要设置id为自增字段,则设置为Int然后去数据库手动添加序列。

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

  • createdAt:DateTime! @createdAt:存储此对象类型的节点为created的确切日期和时间。
  • updatedAt: DateTime! @updatedAt:存储此对象类型的节点last updated的确切日期和时间。

如果希望类型公开这些字段,只需将它们添加到类型定义中,例如:


type User {
  id: ID! @id
  createdAt: DateTime! @createdAt
  updatedAt: DateTime! @updatedAt
}

不要忘记加!

@db 字段定义底层数据库表名:


type User @db(name: "user") {
  id: ID! @id
  name: String! @db(name: "full_name")
}

这样子表 User 会实际生成 user 表并且列 name 会变为 full_name.

@scalarList(strategy: STRATEGY!) 目前仅在String列表有用.


type Post {
  tags: [String!]! @scalarList(strategy: RELATION)
}

数据模型中的字段会影响可用的查询参数。

你可以使用updateManyXXXMutation来更改所有节点的标量字段值,或仅更改特定子集。


mutation {
  # 将没有电子邮件地址的所有用户的电子邮件更新为空字符串
  updateManyUsers(
    where: {
      email: null
    }
    data: {
      email: ""
    }
  )
}

向必须包含节点的模型添加必填字段时,会收到以下错误消息:"You are creating a required field but there are already nodes present that would violate that constraint.你正在创建必填字段,但已存在违反该约束的节点。"

这是因为所有现有节点都会收到该字段的"null"值。这将违反该字段的约束必填字段(或non-nullable)。

以下是添加必填字段所需的步骤:

  • 把它变为可选字段

  • 使用updateManyXXX将所有节点的字段从null迁移到非null值

  • 现在你可以将该字段标记为必填字段并按预期进行部署

relation定义两个类型之间连接的语义。关系中的两种类型通过关系字段连接。当关系不明确时,需要使用@relation指令对关系字段进行注释以消除歧义。

关系在底层数据库有两种处理方式:

  • 关系表:关系之间会有关系表定位两者关系。该方法为默认关系设置。
  • 内联关系:关系通过一个表的外键定位。该方法不能适用多对多关系。因为外键对性能的影响,请谨慎设置。

以下是简单双向关系的示例:


type User {
  id: ID! @id
  profile: Profile! @relation(link: INLINE)
  articles: [Article!]!
}
type Article {
  id: ID! @id
  author: User!
}
type Profile {
  id: ID! @id
  user: User!
}

关系也可以将类型与自身连接起来。 然后将其称为self-relation:


type User {
  id: ID! @id
  friends: [User!]!
}

自我关系也可以是双向的:


type User {
  id: ID! @id
  following: [User!]! @relation(name: "Following")
  followers: [User!]! @relation(name: "Followers")
}

请注意,在这种情况下,关系需要使用@relation指令进行注释。

对于单向关系字段,你可以配置它是必填字段还是可选的!类型修饰符在GraphQL中充当契约,该字段永远不能为"null"。 因此,用户地址的字段将是"Address"或"Address!"类型。

包含所需单向关系字段的类型的节点只能使用[嵌套Mutation]创建,以确保相应字段不为"null"。

再来看以下关系:


type User {
  id: ID! @id
  car: Car!
}
type Car {
  id: ID! @id
  owner: User!
  color: String!
}

如果没有"用户",就永远不能创建"Car",反之亦然,因为这会违反所需的约束。 因此,你需要使用嵌套Mutation同时创建两者:


mutation {
  createUser(data: {
    car: {
      create: {
        color: "Yellow"
      }
    }
  }) {
    id
    car {
      id
    }
  }
}

再次提醒,一对多关系字段始终设置为required。例如,包含许多用户地址的字段总是使用类型[Address!]!并且永远不能是[Address!][Address]![Address]

在定义类型之间的关系时,可以使用@relation指令,该指令提供关于关系的元信息。如果关系不明确,那么你必须使用@relation指令来消除它的歧义。

它可以有两个参数:

  • name(必需):此关系的标识符,以字符串形式提供。
  • link: 定义底层数据库如何处理关系表(默认为中间表TABLE):

    • INLINE: 关系在数据库表中为外键.
    • TABLE: 关系在数据库表中为中间表.
  • onDelete:指定删除行为deletion behaviour并启用级联删除cascading deletes。在具有相关节点的节点被删除的情况下,删除行为确定相关节点应该发生什么。此参数的输入值定义为具有以下可能值的枚举:    - SET_NULL(默认值):将相关节点设置为"null"。    - CASCADE:删除相关节点。请注意,无法将双向关系的端都设置为"CASCADE"。

以下是使用@relation指令的数据模型示例:


type User {
  id: ID! @id
  stories: [Story!]! @relation(name: "StoriesByUser" onDelete: CASCADE)
}
type Story {
  id: ID! @id
  text: String!
  author: User @relation(name: "StoriesByUser")
}

该关系被命名为"StoriesByUser",删除行为如下:

  • 删除"User"节点时,其所有相关的"Story"节点也将被删除。
  • 当一个Story节点被删除时,它将被简单地从相关的User节点上的stories列表中删除。

在最简单的情况下,两个类型之间的关系是明确的并且应该应用默认删除行为(SET_NULL),相应的关系字段不必用@relation指令注释。

这里我们定义了UserStory类型之间的双向one-to-many关系。 由于尚未提供onDelete,因此使用默认删除行为:SET_NULL:


type User {
  id: ID! @id
  stories: [Story!]!
}
type Story {
  id: ID! @id
  text: String!
  author: User
}

此示例中的删除行为如下:

  • User节点被删除时,其所有相关Story节点上的author字段将被设置为null。 请注意,如果author字段被标记为必填,则操作将导致错误。
  • 当一个Story节点被删除时,它将被简单地从相关的User节点上的stories列表中删除。

在某些情况下,你的数据模型可能包含不明确的关系。 例如,考虑到你不仅想要一个关系来表达"用户"和"故事"之间的"作者关系",而且你还想要一个关系来表达"故事"节点已经被"用户"点赞

在这种情况下,你最终会得到"用户"和"故事"之间的两种不同关系! 为了消除它们的歧义,你需要为该关系指定一个名称:


type User {
  id: ID! @id
  writtenStories: [Story!]! @relation(name: "WrittenStories")
  likedStories: [Story!]! @relation(name: "LikedStories")
}
type Story {
  id: ID! @id
  text: String!
  author: User! @relation(name: "WrittenStories")
  likedBy: [User!]! @relation(name: "LikedStories")
}

如果在这种情况下没有提供name,则无法确定writtenStories是否应该与authorlikesBy字段相关。

如上所述,你可以为相关节点指定专用删除行为。 这就是@relation指令的onDelete参数。

我们看下面的示例:


type User {
  id: ID! @id
  comments: [Comment!]! @relation(name: "CommentAuthor", onDelete: CASCADE)
  blog: Blog @relation(name: "BlogOwner", onDelete: CASCADE)
}
type Blog {
  id: ID! @id
  comments: [Comment!]! @relation(name: "Comments", onDelete: CASCADE)
  owner: User! @relation(name: "BlogOwner", onDelete: SET_NULL)
}
type Comment {
  id: ID! @id
  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列表中删除。

schema中包含的关系会影响Prisma API中的可用操作。以下是Prisma API中每个关系生成的CRUD和实时操作的概述:

  • [关系Query]允许你跨类型查询数据或为关系聚合(请注意,这也可以使用Relay[连接模型]
  • [嵌套Mutation]允许你在同一Mutation中创建,连接,更新,升级和删除多个相关节点。
  • [关系Subscriptions]允许你获得关系更改的通知。

指令用于在你的数据模型中提供其他信息。它们看起来像这样:@name(argument:"value")或者当没有参数时只有@name

Datamodel指令描述了有关GraphQL schema中的类型或字段的其他信息。

@unique指令将标量字段标记为唯一。唯一字段将在底层数据库中应用唯一的index


# `User` 有唯一的 `email` 
type User {
  email: String @unique
}

查找有关@unique指令的更多信息。

指令@relation(name:String,onDelete:ON_DELETE!= SET_NULL)可以附加到关系字段。

指令@default(value:String!)为标量字段设置默认值。 请注意,value参数对于所有标量字段都是String类型(即使字段本身不是字符串):


# `title`, `published`,`someNumber` 默认值为 `New Post`, `false` , `42`
type Post {
  title: String! @default(value: "New Post")
  published: Boolean! @default(value: "false")
  someNumber: Int! @default(value: "42")
}

临时指令用于执行一次性迁移操作。 在部署包含临时指令的服务之后,需要从类型定义文件中手动删除临时指令。

临时指令@rename(oldName:String!)用于重命名类型或字段。


# 重命名 `Post` 为 `Story`, 并且它的 `text` 字段改为 `content`
type Story @rename(oldName: "Post") {
  content: String @rename(oldName: "text")
}

如果未使用重命名指令,Prisma会在创建新类型和字段之前删除旧类型和字段,从而导致数据丢失!

你在Prisma service中遇到的不同对象(如类型或关系)遵循不同的命名约定,以帮助你区分它们。

类型名称确定派生查询和Mutation的名称以及嵌套Mutation的参数名称。

以下是命名类型的约定:

  • 单个数据时选择类型名称:

    • Yes: type User { ... }
    • No: type Users { ... }

请严格遵守,因为当创建时若为type Users,那么自动生成的代码里查询多个用户会成为Userss

标量字段的名称用于查询和Mutation的查询参数中。关系字段的名称遵循相同的约定,并确定关系Mutation的参数名称。关联字段名称只能包含 字母数字字符,并且需要以大写字母开头。它们可以包含 最多64个字符

字段名称每种类型都是唯一的。

以下是命名字段的约定:

  • 为列表字段选择复数名称:

    • Yes: friends: [User!]!
    • No: friendList: [User!]!
  • 为非列表字段选择单数名称:

    • Yes: post: Post!
    • No: posts: Post!

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

"与许多类型系统一样,GraphQL支持接口。接口是一种抽象类型,包含一组必须包含的字段才能实现接口。"来自官方GraphQL文档

要了解有关接口何时以及如何进入Prisma的更多信息,请查看此功能请求

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

要了解有关联合类型何时以及如何进入Prisma的更多信息,请查看此功能请求

不同于MySQL和PostgreSQL等关系型数据库,MongoDB作为NoSQL在datamodel的定义上与上述有所区别,下面是独特字段的举例:

一个简单的datamodel.prisma文件例子:


type Tweet {
  id: ID! @id
  createdAt: DateTime! @createdAt
  text: String!
  owner: User!
  location: Location!
}
type User {
  id: ID! @id
  createdAt: DateTime! @createdAt
  updatedAt: DateTime! @updatedAt
  handle: String! @unique
  name: String
  tweets: [Tweet!]! @relation(link: INLINE)
}
type Location @embedded {
  latitude: Float!
  longitude: Float!
}

此示例基于[datamodel v1.1]格式,该格式当前位于[预览]中。

此示例说明了使用数据模型时的一些重要概念:

  • 两种类型TweetUser分别映射到MongoDB集合。
  • UserTweet之间存在双向关系(通过ownertweets字段)。请注意,在底层数据库中,只有User文档存储对Tweet文档的引用(而不是相反),因为User上的tweets字段指定@relationdirective上的link。但是,Prisma API仍然允许您直接查询Tweetowner
  • TweetLocation(通过location字段)有一个单向关系。 Location是一个embedded type,这意味着没有Location文档存储在他们自己的Location集合中 - 相反,每个Location都存储在Tweet集合中的Tweet文档中。
  • 除了User上的name字段外,所有字段在数据模型中都是required(由类型后面的表示)。
  • 使用@id@reatedAt@updatedAt-指令注释的字段由Prisma管理,并且在公开的Prisma API中使用read-only,这意味着它们不能通过突变进行更改(除非[使用NDF格式导入数据])。
  • @unique指令表示唯一约束,这意味着Prisma确保永远不会有两个节点具有相同的注释字段值。

创建和更新数据模型就像编写和保存datamodel文件一样简单。一旦对数据模型感到满意,就可以通过运行prisma deploy来保存文件并将更改应用到Prisma服务:

这些字段的值当前是只读的(除了[在Prisma API中使用NDF格式导入数据]),但将来可以配置。有关详细信息,请参阅此提案

MongoDB的三个系统字段不同:


type User {
  id: ID! @id
  created_at: DateTime! @createdAt
  updated_at: DateTime! @updatedAt
}

关系定义了两种类型之间连接的语义。关系中的两种类型通过关系字段连接。当关系不明确时,需要使用@relation指令对关系字段进行注释以消除歧义。

MongoDB中的关系 文档和关系数据库之间最大的区别之一是如何处理数据类型之间的关系。

虽然关系数据库使用数据库规范化来存储通过密钥相互引用的平面数据记录,但文档数据库能够存储物理上位于同一集合内的相关对象的对象。后者称为嵌入数据(即,集合中的文档可以具有嵌入在同一集合内的子文档/阵列)。

使用MongoDB,可以通过直接在父文档中嵌入数据或使用引用来表达关系。可以在MongoDB文档中找到有关差异的良好概述。在为底层MongoDB建模数据时,Prisma采用嵌入式类型的思想。

嵌入式类型 MongoDB连接器引入了嵌入式类型的概念。嵌入式......

  • ...始终使用@embedded指令进行注释。
  • ...总是(至少)有一种父类型。
  • ...始终直接存储在底层Mongo数据库的父类型集合中(即嵌入类型永远不会有自己的集合)。
  • ...不能有唯一的字段(即用@unique指令注释的字段)。
  • ...不能与其父类型具有(反向)关系(但它可以与其他非嵌入类型有关系)。
  • ...不能使用Prisma API直接查询,而只能通过父类型的嵌套操作查询。
  • ...不能使用Prisma API直接创建,更新或删除,而只能通过父类型的嵌套操作创建,更新或删除。

以下是数据模型的示例,其中Coordinates被定义为嵌入类型:


type City {
  id: ID! @id
  name: String!
  coordinates: Coordinates
}
type Coordinates @embedded {
  latitude: Float!
  longitude: Float!
}

以下是基于此数据模型存储在基础MongoDB数据库中的数据示例:

使用此设置,无法直接查询任何坐标实例,因为坐标是嵌入类型。 只能通过城市类型查询坐标。 同样,您无法直接创建,更新或删除坐标,而是需要创建,更新或删除City才能在Coordinates实例上执行此类操作。

拓展解释

使用MongoDB,您可以通过两种方式建立关系模型:

  • 使用@embedded,如上所述
  • 使用references,在Prisma术语中称为links

与MongoDB连接器的link relation的工作方式如下:

  • 关系的一边(A)存储另一边的文档ID(B),这称为inlined link
  • 该关系的另一方(B)对初始方面的文件完全没有引用(A)
  • 关系的每一面都由底层MongoDB中自己的集合表示,即链接关系总是跨越多个集合。

您可以使用@relation指令的link参数来表示应该存储ID的关系的一面。 在下面的示例中,User类型存储与其相关的所有Post文档的ID值。 然而,Post文档不会在底层Mongo数据库中存储有关其author的任何信息:


type User {
  id: ID! @id
  name: String!
  posts: [Post!]! @relation(link: INLINE)
}
type Post {
  id: ID! @id
  title: String!
  author: User!
}

以下是基于此数据模型存储在基础MongoDB数据库中的数据示例:

虽然这种方法可以直接在Prisma API中查询Post文档(而不是只能通过嵌套操作通过其父类型查询的嵌入类型),但在以这种方式建模关系时存在性能方面的考虑。

通过author字段从PostUser的操作有性能问题。 这是因为底层的Post对象不知道他们的作者是谁,而Prisma需要过滤所有User以找到Postauthor是谁。

关系也可以将类型与自身连接起来。 然后将其称为self-relation

请注意,自我关系还需要在@relation指令中包含link参数:


type User {
  id: ID! @id
  friends: [User!]! @relation(link: INLINE)
}

自我关系也可以是双向的:


type User {
  id: ID! @id
  following: [User!]! @relation(name: "FollowRelation", link: INLINE)
  followers: [User!]! @relation(name: "FollowRelation")
}

请注意,在这种情况下,需要使用@relation指令注释关系,并且必须提供name参数。

对于to-one relation字段,您可以配置它是required还是optional类型修饰符在GraphQL中充当契约,该字段永远不能为null。因此,用户地址的字段将是AddressAddress!类型。

包含所需to-one relation字段的类型的节点只能使用[嵌套mutation]创建,以确保相应字段不为null

再考虑以下关系:


type User {
  id: ID! @id
  car: Car! @relation(link: INLINE)
}
type Car {
  id: ID! @id
  owner: User!
  color: String!
}

如果没有User,就永远不会创建Car,反之亦然,因为这会违反所需的约束。因此,您需要使用声明性嵌套写入同时创建两者:


const newUser = await prisma
  .createUser({
    car: {
      create: {
        color: "Yellow"
      }
    }
  })

graphql:


mutation {
  createUser(data: {
    car: {
      create: {
        color: "Yellow"
      }
    }
  }) {
    id
    car {
      id
    }
  }
}

请注意,to-many relation字段始终设置为required。例如,包含许多用户地址的字段总是使用类型[Address!]!,并且永远不能是[Address!][Address]![Address]

在定义类型之间的关系时,可以使用@relation指令,该指令提供关于关系的元信息。有时,可能需要@relation指令,例如如果一个关系是模糊的,你必须使用@relation指令来消除它的歧义。或者,如果要定义[link relation],则必须使用@relation指令对关系的一侧进行注释,以指定link参数。

它可能需要两个参数:

  • name(必需):此关系的标识符,以字符串形式提供。
  • link(作为[link relation]是需要的):指定关系的哪一侧应该存储对另一方的引用。
  • onDelete:指定删除行为并启用级联删除。在具有相关节点的节点被删除的情况下,删除行为确定相关节点应该发生什么。此参数的输入值定义为具有以下可能值的枚举:  -  - SET_NULL(默认值):将相关节点设置为null。  -  - CASCADE:删除相关节点。请注意,无法将双向关系的结尾设置为CASCADE

以下是使用@relation指令的数据模型示例:


type User {
  id: ID! @id
  stories: [Story!]! @relation(name: "StoriesByUser", link: INLINE, onDelete: CASCADE)
}
type Story {
  id: ID! @id
  text: String!
  author: User @relation(name: "StoriesByUser")
}

该关系被命名为StoriesByUser,删除行为如下:

  • User节点被删除时,其所有相关的Story节点也将被删除。
  • 当一个Story节点被删除时,它将被简单地从相关的User节点上的stories列表中删除。

在某些情况下,您的数据模型可能包含不明确的关系。例如,考虑到你不仅想要一个关系来表达UserStory之间的StoriesByUserrelation,而且你还想要一个关系来表达User已经likedStory节点。

在这种情况下,你最终会得到UserStory之间的两种不同关系!为了消除它们的歧义,您需要为该关系指定一个名称:


type User {
  id: ID! @id
  writtenStories: [Story!]! @relation(name: "WrittenStories", link: INLINE)
  likedStories: [Story!]! @relation(name: "LikedStories", link: INLINE)
}
type Story {
  id: ID! @id
  text: String!
  author: User! @relation(name: "WrittenStories")
  likedBy: [User!]! @relation(name: "LikedStories")
}

如果在这种情况下没有提供name,则无法确定writtenStories是否应该与authorlikesBy字段相关。

如上所述,您可以为相关节点指定专用删除行为。这就是@relation指令的onDelete参数。

请考虑以下示例:


type User {
  id: ID! @id
  comments: [Comment!]! @relation(name: "CommentAuthor", onDelete: CASCADE, link: INLINE)
  blog: Blog @relation(name: "BlogOwner", onDelete: CASCADE, link: INLINE
}
type Blog {
  id: ID! @id
  comments: [Comment!]! @relation(name: "Comments", onDelete: CASCADE, link: INLINE)
  owner: User! @relation(name: "BlogOwner", onDelete: SET_NULL)
}
type Comment {
  id: ID! @id
  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列表中删除。

架构中包含的关系会影响[Prisma API]中的可用操作。以下是Prisma API中每个关系生成的CRUD和实时操作的概述:

  • [Relation queries]允许您跨类型查询数据或为关系聚合(请注意,这也可以使用Relay relay /)的连接模型)。
  • [Nested mutations]允许您在同一突变中创建,连接,更新,升级和删除多个相关节点。
  • [Relation subscriptions]允许您获得关系更改的通知。

指令用于在您的数据模型中提供其他信息。它们看起来像这样:@name(argument: "value")或者只有@name,当没有参数时。

@unique指令将标量字段标记为[唯一]。唯一字段将在底层数据库中应用唯一的索引


type User {
  email: String @unique
}

指令@relation(name: String, onDelete: ON_DELETE! = SET_NULL)可以附加到关系字段。

[见上文]了解更多信息。

指令@default(value:<type>!)为标量字段设置[默认值]


type Post {
  title: String! @default(value: "New Post")
  published: Boolean! @default(value: false)
  someNumber: Int! @default(value: 42)
}

您在Prisma服务中遇到的不同对象(如类型或关系)遵循不同的命名约定,以帮助您区分它们。

类型名称确定派生查询和突变的名称以及嵌套突变的参数名称。

以下是命名类型的约定:

  • 单数中选择类型名称:    - 正确type User { ... }    - 错误type Users { ... }

标量字段的名称用于查询和突变的查询参数中。关系字段的名称遵循相同的约定,并确定关系突变的参数名称。关联字段名称只能包含字母数字字符,并且需要以大写字母开头。它们可以包含最多64个字符。字段名称每种类型都是唯一的。

以下是命名字段的约定:

  • 为列表字段选择复数名称:    - 正确friends: [User!]!    - 错误friendList: [User!]!
  • 为非列表字段选择单数名称:    - 正确post: Post!    - 错误posts: Post!

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

"与许多类型系统一样,GraphQL支持接口。接口是一种抽象类型,包含一组必须包含的字段才能实现接口。"来自官方GraphQL文档

要了解有关接口何时以及如何进入Prisma的更多信息,请查看此功能请求

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

要了解有关联合类型何时以及如何进入Prisma的更多信息,请查看此功能请求

如果将[Prisma service]配置为允许数据库迁移,则可以使用SDL定义和迁移数据库。

Prisma使用 临时指令来执行一次性迁移操作。 在部署包含临时指令的服务之后,需要从类型定义文件中手动删除临时指令。

临时指令@rename(oldName:String!)用于重命名类型和字段。

如果未使用@rename指令,Prisma会在创建新类型和字段之前删除旧类型和字段,从而导致数据丢失!

Post类型重命名为Story


type Story @rename(oldName: "Post") {
  content: String
}

在执行prisma deploy并重命名后,需要手动删除@rename(oldName:"Post")指令。

text字段重命名为content


type Story {
  content: String @rename(oldName: "text")
}

在执行prisma deploy并重命名后,需要手动删除@rename(oldName:"text")指令。

MongoDB是一个schemaless数据库,这意味着possible将各种结构的数据插入其中。 MongoDB从不因为一段插入的数据不符合某种预定义的预期格式而报错。这不同于关系数据库,其中插入的数据需要遵守预定义的数据库模式

将MongoDB与Prisma一起使用时会发生这种情况。 Prisma在MongoDB之上添加了一个“模式”(即Prisma [datamodel])。这就是为什么migrations现在成为将MongoDB与Prisma一起使用时的相关主题。

使用Prisma执行迁移的一般过程如下:

1.调整[datamodel]文件以反映新的所需模式 1.运行[prisma deploy]以应用更改并执行Prisma API(以及可能的)基础MongoDB数据库的迁移

在完成该过程时,Prisma将只对基础MongoDB数据库进行添加式结构更改。

在迁移期间,Prisma永远不会

  • 删除现有集合
  • 重命名现有集合
  • 删除现有数据库
  • 删除或更改现有文档上的任何字段名称

请注意,这可能会导致冗余表,即仍存在于基础MongoDB数据库中但无法通过Prisma API访问的集合或字段。

在迁移期间,Prisma可能

  • 创建新的集合
  • 创建新文档
  • 在现有文档上创建新字段

场景1:使用@db指令

假设您有以下数据模型:


type User @db(name: "users") {
  id: ID! @id
  name: String!
}

因为在类型上使用了@db指令,所以Prisma API中的类型名称是从底层MongoDB中的集合名称解耦的

要重命名Prisma API中的类型,您需要在Prisma数据模型中调整类型的名称:


type Person @db(name: "users") {
  id: ID! @id
  name: String!
}

要应用更改并更新Prisma API,请运行prisma deploy

场景2:没有@db指令

假设您有以下数据模型:


type User {
  id: ID! @id
  name: String!
}

因为没有@db指令,所以底层MongoDB中的集合也称为User

要重命名Prisma API中的类型,您需要在Prisma数据模型中调整类型的名称:


type Person {
  id: ID! @id
  name: String!
}

运行prisma deploy时,Prisma完成了三件事:

  • 从Prisma API中删除User类型的CRUD操作
  • 在Prisma API中为新的Person类型公开CRUD操作
  • 在底层数据库中创建一个新的Person集合

底层的User集合保持不变,它作为冗余数据保留在那里。

如果你仍想在保留的User集合中使用Prisma API中新的Person类型的数据,你需要手动重命名集合 可能会删除那个空的Person集合)。

假设您有以下数据模型:


type User {
  id: ID! @id
  name: String!
}

现在要将name字段重命名为lastName。这实际上意味着三件事:

1.您想要在Prisma API中仅重命名字段 1.您想要在基础MongoDB中重命名字段 1.您想要重命名Prisma API 底层MongoDB中的字段

场景1:仅在Prisma API中重命名

如果只想在Prisma API中重命名该字段,可以通过更改数据模型中字段的名称并添加@db指令来创建从lastName datamodel字段到name的映射来实现。底层MongoDB中的字段:


type User {
  id: ID! @id
  lastName: String! @db(name: "name")
}

现在运行prisma deploy来应用更改。

场景2:仅在底层MongoDB中重命名

因为Prisma从不对您的底层MongoDB执行任何结构更改,所以实现此目的的唯一方法是手动重命名字段

完成此操作后,您的Prisma API当然没有改变。这也意味着当你现在试图通过Prisma API检索User集合中文档的name时,Prisma将抛出一个错误,因为底层的MongoDB中不再存在name

为防止这种情况,您有两种选择:

  • 从数据模型中删除name字段并运行prisma deploy。这意味着无法通过Prisma访问新的lastName
  • 同样重命名Prisma API中的字段,请参阅下面。

场景3:在Prisma API和底层MongoDB中重命名

与第二种情况类似,您首先需要在底层MonngoDB中手动重命名字段

完成此操作后,您可以在Prisma API中重命名该字段。调整datamodel如下所示:


type User {
  id: ID! @id
  lastName: String! 
}

最后,运行prisma deploy

请注意,Prisma CLI可能要求您运行prisma deploy --force来执行此操作。这是[已在此处报告]的CLI中的错误(https://github.com/prisma/prisma/issues/3871)。

假设您有以下数据模型:


type User {
  id: ID! @id
  name: String!
}
type Post {
  id: ID!
  title: String!
}
  现在,您想要从Prisma API中删除`Post`类型,并在底层MongoDB中删除`Post`集合。

首先需要从数据模型中删除Post


type User {
  id: ID! @id
  name: String!
}

现在运行prisma deploy来应用更改。这不会改变底层MongoDB中的任何内容,Post集合仍然作为冗余数据存在。

要摆脱保留集合,您需要从MongoDB数据库中手动删除集合

下一节是Prisma的神奇操作,如何从数据库拿到数据。

Prisma client