发布于 2026-01-06 5 阅读
0

从重新设计 GraphQL API 中吸取的经验教训

从重新设计 GraphQL API 中吸取的经验教训

(本文最初发表于Bearer.sh

在 Bearer,我们使用 GraphQL 来实现仪表盘应用与数据库的通信。最近,我们对这个 GraphQL API 进行了重新设计。以下是我们在此过程中总结的一些经验教训。

第一课:服务用户,而非服务数据库

GraphQL是 API 的查询语言”。

这一点值得重申:GraphQL 是API 的查询语言。当用户想要与 API 交互时,他们将使用 GraphQL 进行交互。

GraphQL API 的主要关注点在于 API 使用者的需求。与其他查询语言(例如 SQL)不同,GraphQL 对数据库结构并不十分关心。这在理论上听起来似乎无关紧要,但在实践中,设计 GraphQL API 时却很容易陷入“数据库优先”的陷阱。

举个简单的例子,假设我们正在搭建一个狗狗个人资料网站。我们的用户界面包含狗狗的名字、出生日期和最喜欢的玩具。

示例狗狗 UI 组件
我们的数据库大致如下:

dogs
name: String
birthdate: Date

toys
dog_id: Int
description: String
preference: Int
Enter fullscreen mode Exit fullscreen mode

狗狗有名字、有生日,而且可以拥有任意数量的玩具。每件玩具都有它的“最爱”,也就是说,每只拥有一个或多个玩具的狗狗都有一个“最喜欢的玩具”。

在 Rails 中,这看起来会像这样:

class dog < ApplicationRecord
  has_many :toys

  def favourite_toy
    toys.order(preference: :desc).first
  end
end

class toy < ApplicationRecord
  belongs_to :dog
end
Enter fullscreen mode Exit fullscreen mode

直接基于此数据库对我们的 GraphQL API 进行建模,我们将得到一个类似这样的 Dog 类型:

type Dog {
  id: ID!
  name: String!
  birthDate: DateTime!
  toys: [Toy!]!
}

type Toy {
  preference: Int!
  description: String!
}
Enter fullscreen mode Exit fullscreen mode

查询端点如下所示:

query {
  dogs {
    name
    birthDate
    toys {
      preference
      description
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

这对我们的 API 用户来说运行良好,但这要求他们了解我们的玩具偏好指标是如何运作的,并使用该指标计算每只狗最喜欢的玩具。

然而,如果我们能将注意力从现有的数据库结构转移到 API 用户的需求上(在本例中,即构建用户画像网站),我们最终得到的狗的类型将更像这样:

type Dog {
  id: ID!
  name: String!
  birthDate: DateTime!
  favoriteToy: Toy!
}

type Toy {
  description: String!
}
Enter fullscreen mode Exit fullscreen mode

查询端点更像这样:

query {
  dogs {
    name
    birthDate
    favoriteToy {
      description
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

在这里,我们的 GraphQL API 可以为用户提供他们想要的一切:每只狗的名字和最喜欢的玩具,而无需任何计算或对底层数据结构的任何了解。

这种以用户为中心的方式可以带来一些意想不到的好处。例如,我们可以自由地调整当前的“最受欢迎玩具”计算方法,或者重构现有的数据库,而无需打扰GraphQL API的用户。

这就是我们重新设计内部 GraphQL API 的方法。我们首先从消费者(即重新设计的仪表盘应用程序)入手,然后反向推导至数据库。通过这种方式,我们最终得到了一个以消费者为中心、易于使用且基本不受后台变更和更新影响的 GraphQL API。

第二课:拒绝空值!拥抱接口

假设我们想在狗狗的个人资料中显示更多信息:
对于救援犬,我们想显示它们的领养日期和领养机构;
对于参赛犬,我们想显示它们的完整注册名称。

我们可以扩展 GraphQL API 来适应这种情况。由于新数据并非适用于所有犬只,我们将新增字段添加为可空字段:

type Dog {
  id: ID!
  name: String!
  birthDate: DateTime!
  favoriteToy: Toy!
  adoptionDate: DateTime
  adoptionShelter: String
  registeredName: String
}
Enter fullscreen mode Exit fullscreen mode

这些可为空的字段允许我们在同一个“犬类”类型下表示不同种类的狗。这样的模式简化了我们的 API,但这种简洁性也带来了一些代价。因为我们用一个“犬类”类型来表示所有狗,所以不同种类狗的信息就变得隐含了。

例如,我们的API不会告诉我们犬只分为两类:待领养犬和参赛犬;也不会告诉我们只有参赛犬才有注册名字,以及待领养犬有领养日期和收容所信息。它期望用户了解其中包含哪些类型的犬只(待领养犬、参赛犬)以及哪些字段与哪些类型的犬只相关联。

此外,我们无法明确哪些字段对哪些类型的犬只来说是必填项。我们能否期望每只待领养的犬只都有领养日期和领养机构名称,还是只有部分待领养的犬只记录了其领养机构名称?

最后,就目前的形式而言,我们的模式无法保证不会返回如下的无意义数据:

{
  "data": {
    "dogs": [
      {
        "id":"RG9nLTE=",
        "name": "Fido",
        "birthDate": "2017-08-30",
        “favouriteToy”: {
          “description”: “Yellow Tennis Ball”
        },
        “adoptionDate”: null,
        “adoptionShelter”: “Second Chance”,
        “fullName”: “Patrice Fidelius Wonderful III”
      },
      ...
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

菲多是只被救助的犬,还是只参赛犬?即使我们认定菲多是只被救助的参赛犬,它也没有领养日期,这对于一只被救助的犬来说,或许并非我们所期待的。

我们可以使用GraphQL 接口来表示相关类型之间的共享字段。GraphQL 中的接口作为抽象类运行,定义了任何实现该接口的类型必须包含哪些字段。以我们的例子为例,我们可以这样定义一个 Dog 接口:

interface Dog {
  id: ID!
  name: String!
  birthDate: DateTime!
  favoriteToy: Toy!
}
Enter fullscreen mode Exit fullscreen mode

普通犬、救援犬和参赛犬(以及,如果我们愿意的话,救援参赛犬)将被表示为实现此 Dog 接口的不同类型。

type CommonDog implements Dog {
  id: ID!
  name: String!
  birthDate: DateTime!
  favoriteToy: Toy!
}


type RescueDog implements Dog {
  id: ID!
  name: String!
  birthDate: DateTime!
  favoriteToy: Toy!
  adoptionDate: DateTime!
  adoptionShelter: String!
}

type ShowDog implements Dog {
  id: ID!
  name: String!
  birthDate: DateTime!
  favoriteToy: Toy!
  registeredName: String!
}
Enter fullscreen mode Exit fullscreen mode

通过使用接口,我们可以清楚地显示不同种类狗之间的关系,同时消除可空字段带来的问题。

查询接口类型看起来大致如下,完全体现了 GraphQL 的理念“提出你的需求,就能得到你想要的”。

query {
  dogs {
    name
    birthDate
    favoriteToy {
      description
    }
    ... on ShowDog {
      registeredName
    }
    ... on RescueDog {
      adoptionDate
      adoptionShelter
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

结论

以上只是我们在重新设计 GraphQL API 时获得的几个见解,也是在构建自己的 API 时需要记住的一些事项。

我们非常希望听到您对 GraphQL 的看法,以及您在构建和使用 GraphQL API 方面积累的任何经验教训。

文章来源:https://dev.to/bearer/lessons-learned-from-redesigning-our-graphql-api-5gpm