GraphQL和REST的比较

对于我们大多数人来说,在设计API和Web应用程序时,我们会想到REST架构。仅仅想到范式转换到其他东西上,就会被看作是一种毫无意义的头痛,不会产生任何净效益和大量工作。

通过不得不自己研究这种转变,我想我会分享我在这个过程中学到的一些东西。我将尝试解释如何使用GraphQL并突出显示两种方法之间的差异。

什么是GraphQL?

GraphQL以多种方式描述,“ 用于访问数据的统一接口。”,“用于在单个端点上公开数据模式的查询语言。“,” 服务器端运行时,用于使用数据定义的类型系统执行查询。”。

坦白地说,所有这些不同的描述都可能会增加最初关于使用它的内容、位置和原因的混淆。从表面上看,所有这些描述都指向同一个东西,而不是对GraphQL的基本存在给出另一个乏味的描述。我认为用图片的形式更容易理解。

下面是GraphQL查询的示例。


在这里,我们使用用户的“id”查询用户数据。关于查询和响应的第一件事是它们是相同的,这是因为GraphQL是键入的,这意味着你要求的是你得到的。我们只是简单地说“用这个ID给我这个用​​户,然后将他们的名字,帖子和粉丝告诉我”。

现在,为了帮助您了解GraphQL查询,我们可以将其与使用下面的REST端点获得相同结果的方式进行比较。


在这里,我们必须定义3个独立的端点才能访问相同的数据。现在你们中的一些人会说你可以通过在第一个请求中返回整个用户对象来将其减少到一个端点,这将被视为过度获取数据。如果我们想象我们有一个需要只显示用户名列表的UI,那么使用这个单一的整体REST请求来获取所有用户意味着我们必须对响应数据执行一些客户端处理以获得我们的内容原本想要的。

这里要注意的一个主要关键区别是,使用GraphQL,您可以了解每个查询可用的对象和数据,以便指定要从服务器返回的数据的形状。使用REST,您无法真正定义返回给您的内容,因为服务器会决定这一点。

因此,我们可以将GraphQL视为一个单一端点,在该端点上我们有一组可执行查询,这些查询映射到我们希望服务器公开的数据模式。


好吧,对GraphQL进行描述的糟糕尝试可能仍然没有任何意义,所以让我们运行一些代码示例,并启动我们自己的小API来比较两者。我创建了一个小的节点应用程序,您可以从GitHub得到它。在这个应用程序中,我们将重新创建上述图像中显示的示例。所有这些都遵循howtographql.com团队的精彩介绍指南。

CameronNewby/graphqlvsrest

我们要做的第一件事是创建并启动本地mongoDB服务器。之后,使用mongoose为我们的用户数据定义模型。

'use strict';

const mongoose = require('mongoose');

exports = module.exports = create;

function create() {

    let postSchema = new mongoose.Schema({
        title: { type: String },
        content: { type : String },
        comments: [{
            user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
            comment: { type: String}
        }]
    }, {strict: false});

    let userSchema = new mongoose.Schema({
        name: { type: String },
        age: { type: Number },
        posts: [postSchema],
        followers: [{type: mongoose.Schema.Types.ObjectId, ref: 'User'}]
    }, {strict: false});
    
    return mongoose.model('User', userSchema, 'users');
}

我还创建了一个简单的数据库管理器类,它可以启动和停止本地MongoDB服务器,并插入一堆虚拟数据供我们运行查询。

接下来,我们将使用express创建一个非常简单的REST API,它将具有与上图中所示的大致相同的端点。每个人都会调用数据库来获取一堆模拟数据。

'use strict'

module.exports = create;

function create(app) {
  let db = app.services.db;

  return {
    setupRoutes
  };

function setupRoutes(router) {
    router.route('/user').get(getUser);
    router.route('/user/all').get(getAllUsers);
    router.route('/user/:name/posts').get(getUserPosts);
    router.route('/user/:name/followers').get(getUserFollowers);
    router.route('/user/create').post(createUser);

    return router;
  }

  function getUser(req, res) {
      let { name } = req.query;

      db.models.User.findOne({name: name}, function (err, doc) {
          if (err) {
              return res.status(400).send('Error was given when trying to fetch user with name' + name);
          }
          return res.json(doc);
      }).lean();
  }

  function getAllUsers(req, res) {
    db.models.User.find({}, function (err, docs) {
        if (err) {
            return res.status(400).send('Error was given when trying to fetch all users');
        }
        return res.json(docs);
    }).lean();
  }

  function getUserPosts(req, res) {
    let { name } = req.params;

    db.models.User.findOne({name: name}, function (err, doc) {
        if (err) {
            return res.status(400).send('Error was given when trying to fetch users post with name' + name);
        }
        return res.json(doc);
    }).lean().select('posts');
  }

  function getUserFollowers(req, res) {
    let { name } = req.params;

    db.models.User.findOne({name: name}, function (err, doc) {
        if (err) {
            return res.status(400).send('Error was given when trying to fetch users post with name' + name);
        }
        return res.json(doc);
    }).lean().select('followers').populate('followers');
  }

  function createUser(req, res) {
    let { user } = req.body;

    db.models.User.create(user, function (err, doc) {
        if (err) {
            return res.status(400).send('Error was given when trying to create new user.');
        }
        return res.json(doc);
    });
  }
}

最后,让我们创建GraphQL端点和模式。为此,我们将使用express-graphql完成,它将通过HTTP提供我们的GraphQL API,并构建和定义我们将使用graphql-tools的模式。还有许多其他方法可以表示您的GraphQL架构,但是对于此示例,我希望尝试尽可能接近GraphQL SDL模型。我打算从解析器函数中分离出类型定义,希望能让它更容易理解。其他方式请看这里

首先,我们创建我们的类型定义,它们布局数据的结构。我们将它映射到我们之前定义的猫鼬模型。这里的前两个类型定义是特殊类型。每个GraphQL服务都必须具有查询类型(默认情况下不需要突变)。这些类型与常规对象类型相同,但它们是唯一的,因为它们定义了每个GraphQL查询的入口点。

    let typeDefs =`
        type Query {
            # Query user by their name
            user(name: String!): User!
            # Query all users in database
            allUsers(last: Int): [User!]!
        }
        type Mutation {
            # Create a new user in database
            createUser(name: String!, age: Int!): User!
        }
        ## User Schema
        type User {
            # Full name of the user
            name: String!
            # Exact age of user
            age: Int!
            # A list of posts the user has generated
            posts: [Post!]!
            # A list of followers the user has
            followers: [User!]
        }
        ## Post Schema
        type Post {
            # Title of post
            title: String!
            # Content body of the post
            content: String!
            # A list of comments on the post
            comments: [Comments]
        }
        ## Comments Schema
        type Comments {
            # The ID of user who made the comment
            user: ID!
            # Comment text
            comment: String!
        }
    `;

因此,使用'Query'类型,我们将定义一个用户函数,它将接收一个字符串并返回一个用户对象,'!' 表示我们返回的内容不能是null对象,它必须是我们稍后定义的“User”类型的对象。

user(name: String!): User!

其他类型是普通对象类型(它们对GraphQL不是特殊的),它们使用一系列支持的标量类型,这些类型表示数据的实际类型(String,Int,Date)。所以这里我们要创建基于我们存储在mongo中的数据结构的类型,并返回到客户端。'User','Post'和'Comments'类型定义将类似于我们的mongoose模式定义。

type User: {
  name: String!
  age: Int!
  posts: [Post!]!
  followers: [User!]
}

了解我们如何使“User”对象的上述GraphQL类型定义类似于下面的“User”mongoose模式。

let userSchema = new mongoose.Schema({
  name: { type: String },        
  age: { type: Number },        
  posts: [postSchema],        
  followers: [{type: mongoose.Schema.Types.ObjectId, ref: 'User'}]    });

GraphQL附带以下类型:

  • Int:带符号的32位整数。
  • Float:带符号的双精度浮点值。
  • String:UTF-8字符序列。
  • Booleantruefalse
  • ID:ID标量类型表示唯一标识符,通常用于重新获取对象或作为缓存的键。ID类型以与String相同的方式序列化; 但是,将其定义为ID表示它不是人类可读的。

GraphQL还允许您构建和创建自己的标量类型,因此虽然它们只支持开箱即用的基本标量类型,但您可以更轻松地构建自己的类型,有关GraphQL类型的更多信息,请参见此处

因此,使用我们编写的Query和Mutation方法的正式类型定义,下一步是使用这些定义来构建相应的函数/解析器,它将被调用以获取或更新数据。

    let resolvers = {
        Query: {
            user: async (parent, { name }, context, info) => {
                return await context.db.models.User.findOne({name: name}).lean().populate('followers').exec();
            },
            allUsers: async (parent, args, context, info) => {
                return await context.db.models.User.find({}).lean().exec();
            }
        },
        Mutation: {
            createUser: async (parent, { name, age }, context, info) => {
                return await context.db.models.User.create({name, age});
            }
        }
    }

每个解析器可以包含四个参数:

  • parent:上一个对象,通常不使用根查询类型的字段。
  • args:您在类型定义中注册的输入以及在GraphQL查询中提供给字段的参数。
  • context:一个值,提供给每个解析程序并保存重要的上下文信息,如当前登录的用户或访问数据库。
  • info:包含与当前查询相关的特定于字段的信息以及架构详细信息的值

在这种情况下,我们将在上下文中传递我们的数据库连接。我们解构的参数与我们之前在这些方法的类型定义中定义的参数相同。我们还定义了哪些参数是强制性的,哪些是可选的,使用' '符号,带符号的符号是解析器的必需参数。

因此,请从“ Query”类型返回“ user”类型定义。

user(name: String!): User!

我们已经定义了这个'user'查询接受一个名为name的参数,该参数必须是非空字符串。现在我们知道我们的旋转变压器的'args'参数将是以下形状的对象。

args: {
  name: 'John'
}

我们对每个解析器所做的就是接受给定的参数并使用它们对我们的mongoose模型执行查询,以获取或更新数据记录。

就是这样!我想我还没有详细介绍GraphQL模式的所有组件,但那是因为有很多其他很棒的描述和教程,我想鼓励人们为自己运行代码。用它作为学习和改进的基础。这里要注意的一个要点是,即使是嵌套模式,创建类型定义也不仅简单,而且需要多少代码来创建一个比REST对应方更具通用性的查询方法。

有关如何安装和运行应用程序的详细信息,请参阅GitHub项目上的README文件。


总而言之,让我们回顾一下使用GraphQL而不是REST的优缺点。

好处

1.不再多余的提取

REST最常见的问题之一是过度提取和提取不足。我们的意思是客户端调用固定端点,它返回由服务器定义的数据结构,因此客户端可能会收到它不想要的数据,或者可能需要多次请求才能获得它实际需要的所有数据。设计一个没有这个固有问题的REST API非常困难。

另一方面,GraphQL没有这个问题。在GraphQL中,服务器声明可用的资源,并且客户端询问当时需要什么。

2.架构定义和文档

GraphQL使用强类型系统,该系统概述了服务器可以返回的数据结构。使用SDL定义模式意味着客户端可以轻松确定他们可以接收的数据的形状以及他们可以进行的突变或查询,这样做可以让客户端开发人员独立工作,因为他们可以轻松查看他们可用的数据。

以这种方式定义模式允许您非常轻松地记录API,而在开发REST端点之后必须编写API文档之前,您的模式就像一种文档形式。

3.减少所需的端点数量

这可能是我最喜欢的一点。必须设计和构建多个具有多个端点的REST API,将其降低到一个似乎是开发人员时间的光荣胜利。

4.订阅

GraphQL订阅是一种将数据从服务器推送到选择从服务器侦听实时消息的客户端的方法。订阅类似于查询,因为它们指定要传递给客户端的一组字段,但不是立即返回单个答案,而是每次在服务器上发生特定事件时都会发送结果。

这允许您通过Web套接字通知客户端有关已执行或已完成的特定事件。

缺点

1.采用和支持

GraphQL是新的,因此它不仅仅具有REST所提供的时间天赋,因此它允许它被普遍采用,以及围绕其框架构建的大量支持和工具。但是对于GraphQL来说,情况正在快速变化,随着越来越大的名字采用这种技术,我们只会看到支持和工具的增长相匹配。

你是如何嵌入的?

这不是GraphQL的直接缺点,而是更多关于切换到它的过程。对于某些应用程序和API来说,转换到这种新的工作方式可能为时已晚。如果您已经拥有一个宏大的复杂路由结构或使用您的API的众多客户端,那么进行切换可能永远不会具有成本效益甚至实用性。但请记住,Facebook有这个确切的问题,并且首先是GraphQL的出现,所以在这方面总是可以进行切换,它的情况就是值得的。

这些只是我在将API从REST移动到GraphQL时发现的一些内容。如果您对两者之间的比较有任何更多的想法?请在评论中发布!

译自:https://medium.com/@cameron.m.newby/a-comparison-of-graphql-and-rest-e125d77fb329

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,657评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,662评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,143评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,732评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,837评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,036评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,126评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,868评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,315评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,641评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,773评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,859评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,584评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,676评论 2 351

推荐阅读更多精彩内容