支撑PolityLink的GraphQL技术

PolityLink(政狀連結)是一個政治「原文」的門戶網站。
我們致力於整理國會和行政機關官方網站上散落的資訊,並將其相互關聯,以便國民能夠輕鬆地獲取關於政治方面的「準確」且「中立」的資訊。

image

为了让用户能自由地使用PolityLink提供的信息,我们根据一种叫做GraphQL的Web API规范进行公开。我们公开了GraphQL的终端URL,可以用它来根据特定条件获取法案和委员会会议记录,并利用这些信息进行统计分析。关于具体的分析示例,请参考利用PolityLink进行分析的教程。此外,请参考与PolityLink相关的开发者故事,了解关于创办政治门户网站PolityLink的思考。

目前来看,GraphQL在日本语中的信息还很少,缺乏有关如何建立GraphQL服务器的知识。在这里,我们将介绍为什么PolityLink选择采用GraphQL以及在运营GraphQL服务器时的一些实施技巧。

为什么选择了 GraphQL?

网络应用程序接口的必要性

PolityLink通过整理并提供法案、委员会会议记录、议员等各种信息。这些数据是动态收集的,为了能够获取所需信息,需要整理和存储数据。

在网络上提供的数据中,有些数据是动态修改的,或者需要通过自定义查询获取的数据,存储在数据库管理系统(DBMS)中被认为是高效的。在DBMS方面,开源实现如MySQL和PostgreSQL在关系型数据库中非常知名。

顺便说一下,如果想要利用用户积累的信息,我们是否应该公开这些数据库管理系统(DBMS)的端口,让用户能够直接执行SQL查询,这样会更加方便吗?

实际上,这有几个问题。我会随意列举一下,例如:

    • DBをそのまま公開してしまうと、DBに脆弱性があるときにすぐに弱点を突かれてしまうおそれがある。

 

    • 後からデータベースのスキーマを変えると、今までアクセスしていたユーザーが困るので変えにくくなる。

 

    • 任意のSQL文の実行を許してしまうと、計算が重いクエリを投げられて負荷がかかるおそれがある。

 

    ユーザーがアクセスできるリソースの制御をデータベース上で行う必要がある。

考虑到这种担忧,即使仅仅出于公开数据的目的,限制并整理可被外部访问的资源后提供,被认为是非常方便的做法。在这种情况下,一种常见的实现方法是通过称为Web API的外部数据交互接口(实际上是在Web上的端点)进行数据交流。

Web API 是指

    1. 如果希望将数据自由地提供给外部的工程师或服务,

 

    如果需要独立地实现Web服务的后端和前端,

需要在Web API中实现。在此期间,Web API开发人员希望设计一个只提供用户可以访问的有用信息并且用户易于访问的Web API。为此,需要事先定义在何处可以访问以及可以获取到什么内容。接下来将讨论第二种情况,即如何设计一个在前端调用的Web API。

REST API 的问题所在

PolityLink的实施方针是首先将前端和后端实现松耦合。这是因为,虽然不仅限于PolityLink,但随着近年来前端技术的迅速发展,独立实现前端和后端可以获得比紧密耦合实现更高的灵活性。具体解释在此不提供。

此外,PolityLink还将其服务的支柱之一设定为用户可以自由使用汇总的数据。为了实现这一点,积累的数据需要以数据的形式公开一个Web API以供使用,而PolityLink服务本身作为该Web API的使用示例的机制则有一个优点,即可以在使用Web API的同时进行改进。

在将前端和后端分别实现的应用程序中,前端和后端之间的数据接口通常以称为REST API的规范来实现,这是目前广泛使用的方法。

让我们看一下PolityLink目前提供的服务。在PolityLink的会议记录页面上,不仅提供会议记录信息,还同时发布所讨论的法律案信息、发言者的议员信息、委员会信息和新闻信息。另一方面,委员会页面上发布会议记录信息,法律案页面上也同样发布会议记录信息。通过这样做,用户可以从不同角度了解政治信息。

此外,PolityLink还有一个备受推荐的功能,称为”时间线页面”。

image.png

在上述页面中,将会议记录、法案等各种资源的信息集合到一个页面上。如果用二部图来整理实现的页面和后端数据库中管理的资源的关系,会是这样的。

image.png

因此,在后端数据库中,会议记录、法律草案和议员等信息被定义为不同的模型,并在前端的各种页面上进行选择和整合异构数据来显示,这就是 PolityLink。因此,在开发时定义了实现这样的网页API的需求。

在这种情况下,有哪些实现后端Web API的方法呢?如果要定义为REST API,我认为有两种主要的方法。

    1. 在前端页面中,通过分别在独立的终端点上实现会议记录和法案,并分别从前端调用。

(+)简洁的终端点
(-)需要多次调用终端点来创建一个页面
(-)前端页面中可能会返回不必要的信息
(+)只需扩展一个终端点就可以向资源中添加新信息

为每个前端页面创建专用终端点。

(-)复杂的终端点
(+)只需一次请求即可创建一个页面
(+)预先定义资源可以确保数据的准确获取
(-)需要扩展所有相关的终端点才能向资源中添加新信息。

在实现REST API时,根据上述描述,我详细列举了其优点和缺点。可以说,除非能够非常巧妙地实现,否则实现上会出现上述的缺点。

这个问题的根本原因是,REST API本身并没有具体的实现指南,后端工程师通常会基于自己的喜好来实现。同时,REST API的最佳实践是按照资源将终端点分开,然而在现代,前端变得越来越复杂,需要构建横跨多个资源的单一前端。此外,用户需要跨多个资源获取信息的需求也在增加,因此传统的REST API框架难以应对这种情况。

GraphQL 的出现

image.png

因此,提出的解决方案是GraphQL这个规范。RedHat提供的解释作为GraphQL的日本语资料总结简洁。

在PolityLink上,您可以使用GraphQL Playground来尝试实际查询。

image.png

GraphQL 提供了一个简洁的 Web API,从客户端的角度来看,当客户端请求所需资源时,它会提供一个返回 JSON 格式数据的单一端点。对于客户端来说,当始终向相同的端点发送查询时,返回与该查询对应的 JSON 数据,因此可以相当容易地预测返回值。这样一来,就可以避免在前端对数据进行格式转换等繁琐操作,减轻了前端工程师的工作负担。

在 PolityLink 的 Web API 提供中,我们认为定义预先的模式将有助于在顶层依据该模式唯一确定端点的输入和输出。此外,通过定义请求所需的对象字段,可以有选择性地获取只包含该信息的数据。例如,如果要在 GraphQL 中实现时间线页面,由于在 GraphQL 框架中解决了前面提到的 REST API 的缺点,则会有以下效果。

    1. 如果使用GraphQL实现的话

(+) 简洁的端点
(+) 一次请求即可创建一个页面
(+) 返回的值只包含所请求的字段,可准确获取数据
(+) 只需更改资源的模式即可向资源中添加新信息

在实际的时间轴页面中,通过一个GraphQL查询从单一的GraphQL端点获取了构成此页面所需的所有信息,并且由于架构变更会即时反映在文档中,所以可以很容易地通过查看文档来更改查询。

让我们将之前的内容与各自设计中的实施优缺点整理一起。

星取り表リソースごとのREST APIページごとのREST APIGraphQLエンドポイントの複雑さ+-+1つのページを作る時のリクエストの回数-++必要なフィールドだけレスポンスに含まれるか?-++スキーマ変更の容易性+-+

或许我们正在倾向于使用GraphQL,但是随着服务规模的扩大以及接触API的工程师数量的增加,我们认为GraphQL作为标准化Web API将会逐渐展现出其优势。因此,PolityLink决定选择GraphQL。

GraphQL查询的实例

在GraphQL中,您可以执行以下两种类型的查询。在PolityLink中,我们只公开了查询部分的可用形式,用于一般用户的使用。

    • query: 読み出すためのクエリ (SQLにおける Select に対応する)

 

    mutation: 書き換えるためのクエリ (SQLにおける Update / Insert / Delete に対応する)

前端工程师可以通过 HTTP 请求来请求“需要哪个对象(相当于 SQL 中的表)的哪些字段”,而无需编写 SQL 语句,从而仅获取所需的对象和字段。例如,以下是用于请求2020年1月20日提交的三个法案名称的查询。

query {
  Bill(filter: {submittedDate: {year: 2020, month: 1, day: 20}}) {
    name
  }
}

结果可以以JSON格式获得。

{'data': {'Bill': [{'name': '特定複合観光施設区域の整備の推進に関する法律及び特定複合観光施設区域整備法を廃止する法律案'},
   {'name': '地方交付税法及び特別会計に関する法律の一部を改正する法律案'},
   {'name': '平成三十年度歳入歳出の決算上の剰余金の処理の特例に関する法律案'}]}}

在这里需要注意的是,返回的 JSON 格式的结构与发送的查询完全相同。在 data 键中存在一个 Bill 对象,其内容是一个以 name 为键的对象数组。这是因为我们在查询中请求返回 Bill 对象的每个 name,所以只返回符合条件的 Bill 的 name 字段作为数组,这样就可以这样考虑。

以下是参考的时间轴页面所使用的查询。

    query($timelineId: ID!){
        politylink {
            Timeline(filter:{id:$timelineId}){
                date {year, month, day}
                totalBills
                totalMinutes
                totalNews
                bills {
                    id
                    name
                    billNumber
                    isPassed
                    aliases
                    totalNews
                    submittedDate {formatted}
                }
                minutes {
                    id
                    name
                    topics
                    totalNews
                    startDateTime {year, month, day, formatted}
                }
                news {
                    title
                    url
                    isPaid
                    publisher
                    thumbnail
                    publishedAt {year, month, day, formatted}
                }
            }
        }
    }

在这种情况下,您可以请求涉及多个对象的嵌套信息。可以说,返回值是按层次结构组织的 JSON 对象与 JavaScript 中描述的现代前端非常兼容。

GraphQL 架构的管理方法是什么?

接下来,让我们考虑一下实现这样查询的模式。

在当前的描述中,尽管前端和客户端变得更加便利,但我们认为实施这样的后端可能会变得更加繁琐。这是因为与后端数据库相比,Web API端的实施受到更严格的限制,因此我们认为数据从后端数据库映射到Web API的过程会变得更加麻烦。考虑到这一点,我认为前端和客户端的负担只是转嫁给了后端工程师,会引发一些担忧。

然而,当从零开始设计这样的Web API时,如果GraphQL的数据结构(即模式)已经确定,则可以考虑自动配置与之对应的数据库。实际上,GRANDstack的基本想法就是实现了这一点。

GRANDstack 引发大家的注意

GRANDstack提供了一个使用Neo4j作为后端数据库,并根据模式自动构建GraphQL服务器的样板库。

image.png

GRANDstack 包括了图中所示的4个系统。

    1. GraphQL: 在Web API模型中使用GraphQL。

 

    1. React前端:用于在GRANDstack上实现前端。

 

    1. Apollo:GraphQL服务器的Node.js实现。

 

    Neo4j数据库:存储着GraphQL的后端。

在PolityLink中,我们使用GraphQL、Apollo和Neo4j数据库。目前,PolityLink的前端使用名为Gatsby的站点生成器生成为静态HTML,因此React前端与GRANDstack分开使用。

在協同前端工程師的情況下,後端工程師定義Web API規範作為負責分界點。在GraphQL的世界中,這對應到一個schema。在PolityLink中,我們的schema是在GitHub存放,您可以參考那裡來查看可以執行的查詢。

image.png

如果将其定义为 schema.graphql,则 GRANDstack 的优点在于它可以自动将其与 Neo4j 数据库的节点-边结构相关联以适应该 GraphQL 模式。尽管在后端中,Neo4j 作为数据库运作,但无需显式定义 Neo4j Cypher 查询。因此,理想情况下应该能够使用无代码方式创建 GraphQL API。

在实现中,查询接口是通过GraphQL定义的,而其后端是由图数据库Neo4j驱动的。数据的添加和删除是通过GraphQL的mutation进行的,不直接从外部访问Neo4j。

在这里需要注意的一点是,GraphQL 并不一定需要后端是图形数据库。通常,在创建 Web API 时,有很多选择可供选择。

    • データベースの選択肢: RDBMS, NoSQL, Graph Database, NewSQL ……

 

    Web API 設計の選択肢: REST, GraphQL, ……

在这些中间,GRANDstack被认为是选择Neo4j + GraphQL的组合并提供模板的选项。

需要对GRANDstack进行扩展,以满足PolityLink的要求。

刚才,我提到了理想情况下可以无需编码提供GraphQL API,但实际在PolityLink中使用时,需要进行一些扩展。下面将介绍具体内容。

在GRANDstack中,针对简单的查询操作或者CRUD操作,会自动定义一个模式。关于自动生成的定义,请参考GraphQL模式生成和扩展。

然而,我想要查询这个类别中包含了多少个元素。在这种情况下,需要在模式定义文件 graphql.js 中编写一个能够返回这种值的 GraphQL 字段。由于 GRANDstack 使用 Neo4j 作为后端数据库,因此在这种情况下需要使用 Neo4j 的查询语言 Cypher 来编写查询。

提供自定义查询的统计功能

在PolityLink中,我们在每位议员详细信息页面上提供了以下信息:每位议员参加了多少个委员会,在活动(即发言记录)方面有多少条记录。由于原始功能并不提供计算此类信息的功能,所以我们在schema.graphql中进行了如下定义。

"""
国会議員
"""
type Member{
    id: ID!

    /* 中略 */

    totalMinutes: Int! @cypher(
        statement: """MATCH (this)-[]-(n:Minutes)
        WITH COUNT(n) AS count
        RETURN count""")
    totalActivities: Int! @cypher(
        statement: """MATCH (this)-[]-(n:Activity)
        WITH COUNT(n) AS count
        RETURN count""")
}

通过在 statement 上写入 cypher 查询作为指令,您可以在执行查询时自动执行该 cypher 查询,并将执行结果集成到 GraphQL 的字段中进行获取。与 Neo4j 的 cypher 查询不同之处在于,您需要将当前选择的节点描述为 “this”。

提供自定义查询以进行删除操作。

由于这个功能只在PolityLink内部用于开发目的,所以外部用户无法使用。但由于没有删除所有属于资源的元素的查询,所以我们就这样定义了它。

type Mutation {
    DeleteAllMembers: [ID!]! @cypher(
        statement: """
        MATCH (n:Member)
        WITH n, n.id AS id
        DETACH DELETE n
        RETURN id
        """
    )
    DeleteAllElections: [ID!]! @cypher(
        statement: """
        MATCH (n:Election)
        WITH n, n.id AS id
        DETACH DELETE n
        RETURN id
        """
    )
}
schema{
    query: Query
    mutation: Mutation
}

添加字段到资源的查询是在被定义为type的字段上追加的形式,但是当需要添加新的查询或者变更的时候,需要创建一个名为Mutation的type,并在其中描述查询。在这种情况下,因为这是一个更新查询,所以我们定义了一个名为Delete的mutation方法。

为了确保ID的唯一性,我们需要对其进行限制。

PolityLink 为了更新已经注册的对象的字段,可能会使用 GraphQL 在更新数据时对应于 SQL 中的 Update 的 merge mutation 查询。然而,在 GRANDstack 中执行 merge mutation 时,如果对象的内容在变更之前和之后有所改变,就会出现新对象的复制行为。为什么不是更新而是复制呢?这是因为在进行 merge 之前无法判断要合并的元素和当前要合并的元素是否相同。

为了防止这种情况发生,在后端数据库Neo4j中,我们对id施加了唯一约束。在这里,我们指定相同id的对象类型之间要求唯一性,这样在合并相同id的元素时,会覆盖并保存。为了实现这一点,我们始终在GRANDstack启动时执行该数据库初始化命令。

实施的详细内容如下。

export const initializeDatabase = (driver) => {
  const resources = ["Member", "Election", "Diet", "Law", "Bill", "Committee", "Minutes", "Url", "Timeline", "News"]
  const initCypher = resources.map(key => `CREATE CONSTRAINT ON (n:${key}) ASSERT n.id IS UNIQUE`)

  const executeQuery = (driver, cypher) => {
    const session = driver.session()
    return session
      .writeTransaction((tx) => tx.run(cypher))
      .then()
      .finally(() => session.close())
  }

  initCypher.forEach(cypher => 
    executeQuery(driver, cypher).catch((error) => {
      console.error('Database initialization failed to complete\n', error.message)
    })
  )
}

这也是一个例子,为了实现这个功能,需要了解Neo4j的知识。使用GRANDstack时,也有一些地方很难进行修改,因为你不了解后端数据库的结构。

总结

    • GraphQL: ユーザーが便利に使える Web API の新しい規格であり、異種データを組み合わせたフロントエンドページの実装に効果的。

 

    • GRANDstack: Neo4j で GraphQL APIを立てるスターターキット

GraphQL をみんなが使いこなせれば、今後需要が高まると考えられます。
ただし Neo4j の Cypher クエリを書く必要があるなど、工夫をしなければならないところもあります。

PolityLink は攻めた技術選択をすることで、 GovTech / CivicTech にイノベーションを巻き起こしていきます!!

PolityLink の提供する GraphQL エンドポイント は、データ解析やGraphQLの練習等の目的で自由にご利用いただけます。

广告
将在 10 秒后关闭
bannerAds