美文网首页prisma我爱编程
服务配置:数据建模(SDL)

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

作者: guog | 来源:发表于2018-05-03 10:42 被阅读13次

    本文属使用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: "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)也是如此

    系统字段

    三个字段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中的可用操作。对于每一个关系,

    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)

        本文链接:https://www.haomeiwen.com/subject/bldkrftx.html