使用 TreeQL 加速你的 API 开发

什么是TreeQL

tree-ql 是一个laravel扩展,希望能够从url中include你所需的资源,实现查询的所见既所得.

// http://api.test/posts/{slug}?include=content,user,comments  ↓

{
    "data": {
        "id": 1,
        "slug": "quisquam-asperiores-est-necessitatibus-et.",
        "title": "Quisquam asperiores est necessitatibus et.",
        "description": "Officiis nihil sunt ut veritatis.",
        "cover": "https://lorempixel.com/640/480/?63535",
        "comment_count": 11,
        "like_count": 11,
        "content": "Omnis quisquam dolorem quasi sequi veritatis quia dolorem sed. Ut non voluptatem beatae eum. ",
        "comments": [
          {
            "id": 303,
            "content": "Quasi dignissimos dolor tempore exercitationem.",
            "user_id": 2481,
            "post_id": 1,
            "like_count": 18,
            "reply_count": 9,
            "floor": 303,
          }
        ],
        "user": {
          "id": 1221,
          "nickname": "Ashleigh McKenzie",
          "avatar": "https://lorempixel.com/640/480/?29515"
        }
    }
}

更加深入的使用

// http://api.test/posts/{slug}?include=
// content,user,comments(sort_by:like_count){user,replies.user},is_like,select_comments

{
  "data": {
    "id": 1,
    "slug": "quisquam-asperiores-est-necessitatibus-et.",
    "title": "Quisquam asperiores est necessitatibus et.",
    "description": "Officiis nihil sunt ut veritatis.",
    "cover": "https://lorempixel.com/640/480/?63535",
    "comment_count": 11,
    "like_count": 11,
    "user_id": 1221,
    "content": "Omnis quisquam dolorem quasi sequi veritatis quia dolorem sed. Ut non voluptatem beatae eum.",
    "is_like": true,
    "comments": [
      {
        "id": 303,
        "content": "Quasi dignissimos dolor tempore exercitationem.",
        "user_id": 2481,
        "post_id": 1,
        "like_count": 18,
        "reply_count": 9,
        "floor": 303,
        "user": {
          "id": 2481,
          "nickname": "Garett O'Connell",
          "avatar": "https://lorempixel.com/640/480/?52652"
        },
        "replies": [
          {
            "id": 415,
            "comment_id": 303,
            "user_id": 2814,
            "content": "Odit magnam sed ut.",
            "call_user": null,
            "created_at": "2018-12-12 02:26:08",
            "updated_at": "2018-12-12 02:26:08",
            "user": {
              "id": 2814,
              "nickname": "Ted Dickinson",
              "avatar": "https://lorempixel.com/640/480/?19577"
            }
          }
        ]

      }
    ],
    "user": {
      "id": 1221,
      "nickname": "Ashleigh McKenzie",
      "avatar": "https://lorempixel.com/640/480/?29515"
    }
  },
  "meta": {
    "selected_comments": [
      {
        "id": 303,
        "content": "Quasi dignissimos dolor tempore exercitationem.",
        "user_id": 2481,
        "post_id": 1,
        "like_count": 18,
        "reply_count": 9,
        "floor": 303,
        "selected": 1,
        "created_at": "2018-12-12 02:25:55",
        "updated_at": "2018-12-12 02:25:55",
        "user": {
          "id": 2481,
          "nickname": "Garett O'Connell",
          "avatar": "https://lorempixel.com/640/480/?52652"
        }
      }
    ]
  }
}

你可能会发现和GraphQL比起来并不是真正的所见既所得,这是由于http请求url长度的限制,所以加入了default的概念, TreeQL会结合include和default来返回相应的资源.

安装

确保你的laravel版本在5.5以上,在项目目录下执行

composer require weiwenhao/tree-ql

该版本目前为alpha版本,不推荐用于商业生产环境,推荐用于个人项目

使用

由于tree-ql是一个laravel的扩展包,接下来会从laravel的角度进行切入,实际上如果你熟悉 dingo/api的include,你会更加适应这种开发模式.

我可以include什么东西?

由于include所见即所得,因此可以换个提问方式,我的response中可以返回些什么数据?

response中的数据可以分为4类, 既 columns,relations,each,meta.

columns 既我们数据库中的columns, 如 id,name,created_at,updated_at等

relations 既orm中的关联关系, 比如post资源的relation有一对一的 user,一对多的 comments, 具体的定义都在laravel的model中定义

each 可以理解为没有存储在mysql中,由程序员计算得来的column, 其和column是平级的, 比如 一个user是否点赞了一篇post, 那么在我们的post的response中可能会见到这样的数据 ↓

{
    "data": [
        {
            "id": 1,
            "slug": "quisquam-asperiores-est-necessitatibus-et.",
            "title": "Quisquam asperiores est necessitatibus et.",
            "description": "Officiis nihil sunt ut veritatis.",
            "is_like": true, // 该字段由程序员计算得来, 没有也不能存储在数据库中
        },
        {
            "id": 2,
            "slug": "quisquam-asperiores-est-necessitatibus-et.",
            "title": "Quisquam asperiores est necessitatibus et.",
            "description": "Officiis nihil sunt ut veritatis.",
            "is_like": false
        }
    ]
}

meta 用来存储一些无法存储在data中的数据, 最典型的例子既分页信息 ↓

{
  "data": [
    {
      "id": 285,
      "slug": "repellat-illo-molestias-quidem-ea-autem.",
      "title": "Repellat illo molestias quidem ea autem.",
      "description": "Sed harum.",
      "cover": "https://lorempixel.com/640/480/?12347",
      "comment_count": 8,
      "like_count": 14,
      "user_id": 2023
    },
    // ...
  ],
  "meta": {
    "pagination": {
      "per_page": 15,
      "total": 300,
      "current": 1,
      "next": "http://api.jianshu.test/api/posts?page=2",
      "previous": null,
      "last": 20
    }
  }
}

接下来看看如何在laravel中进行定义

Resource的定义

tree-ql默认使用app下的Resources目录, 因此可能会有这样的目录结构

接下来以PostResource为例

<?php

namespace App\Resources;

use Weiwenhao\TreeQL\Resource;

class PostResource extends Resource
{
    /**
    * 从下面的 columns/relations/meta/each中抽取得来
    */
    protected $default = [
        'id',
        'slug',
        'title',
        'description',
        'cover',
        'comment_count',
        'like_count',
        'user_id'
    ];

    protected $columns = [
        'id',
        'slug',
        'title',
        'description',
        'cover',
        'comment_count',
        'like_count',
        'user_id',
        'content'
    ];

    protected $relations = [
        'user',
        'comments',
    ];

    protected $meta = [
        'selected_comments'
    ];

    protected $each = ['is_like'];

    public function isLike($item, $params)
    {
        return array_random([true, false]);
    }

    public function selectedComments($params)
    {
        $post = $this->getCollection()->first();

        $comments = $post->selectedComments;

        $resource = CommentResource::make($comments, 'user,replies.user');

        return $resource->getResponseData();
    }
}

Resource分为两部分, 类属性部分用来进行定义,除了default外,其余部分 columns/relations/meta/each 中定义的value 都可以在include中被引入.

而default中的定义则是从 columns/relations/meta/each 中已经定义的value进行抽取,default中的key,会被默认include进来,而不需要再url中显式的定义.

方法部分 目前的作用主要是回调函数, 且只有each和meta中定义的value 需要callback. callback命名的规则也很简单, 既将meta或者each中定义的值改为 小驼峰命名 作为方法名称即可.

each的callback有两个参数, 每一个resource下都有一个collection属性, 其中存放了该Resource下的资源数据, 其类型为Illuminate\Database\Eloquent\Collection ,collection中的每一个item都会被callback一次, 所以 上面 isLike的第一个参数为 Collection中的一个item, item既model

在Resource中通过调用 $this->getCollection()可以获取所有的数据

由于include支持params, 所以isLike的第二个参数为include中传递的params, 类型为array,格式为

$params = [
    'sort_by' => 'created_at',
    'order' => 'desc'
]

callback 中 return的值将会在response data中被原样展示

关于meta

meta不同于each, 每个include meta在其生命周期中只会被调用一次.且只有一个参数 既params. 其return的值也将在response meta中被原样展示

meta的另一个特点时,只有最外层的数据结构才存在meta, 即

{
    data: {},
    meta: {}
}

因此如果在更深层次的resource中进行include meta 那么会产生的行为时, 该meta数据,被拉到了最外层. 举个例子

include=meta1,post.meta2 那么返回的结果是

{
    data: {
        post: {}
    },
    meta: {
        meta1: {},
        meta2: {}
    }
}

这个行为我并不是很喜欢,所以在考虑更加合适的解决方案

Columns 中定义了orm select语句中可以被查询的数据,既类似这样的行为会使用columns

使用Resource

接下来看看PostController的index和show方法

    /**
     * Display a listing of the resource.
     * @return \Weiwenhao\TreeQL\Resource
     */
    public function index()
    {
        // $posts = Post::columns()->latest()->get(); 同样支持
        $posts = Post::columns()->latest()->paginate();

        // 等价于 return PostResource::make($post, request('include'))
        return PostResource::make($posts);
    }

    /**
     * Display the specified resource.
     *
     * @param  \App\Models\Post $post
     * @return \Weiwenhao\TreeQL\Resource
     */
    public function show($post)
    {
        return $resource = PostResource::make($post);
    }

上面的使用非常的简单, 唯一需要讲解的便是 columns() 这个查询构造器. 我不希望Post的查询一下查询出table中所有的column,而是根据url中include进行查询. 所以columns()会解析url中include并结合resource中的定义进行合适的select.

include的语法规则

已实例进行讲解

http://api.test/posts/{slug}?include=user 基础使用,在post的基础上 include 这篇post的作者

我想include PostResource中定义的更多的东西怎么办?

http://api.test/posts/{slug}?include=user,content,comments 使用逗号进行分割

我想引入comment中的user怎么办?

http://api.test/posts/{slug}?include=user,content,comments.user使用.进行嵌套

我想同时引入comment中的user和replies怎么做?

http://api.test/posts/{slug}?include=user,content,comments{user,replies} 使用 {}, 来代替.语法进行嵌套

在dingo/api中 你可能需要这么做 include=comment.user,comment.replies

我想对include的comments添加一些条件我应该怎么做?

http://api.test/posts/{slug}?include=user,content,comments(sort_by:created_at,order:desc){user,replies} 条件语法紧跟着comments, ()中包围的既params, 形式为 key1:value1,key2:value2

实际上 目前只有 each和meta支持回调. 后续会对columns和relations添加回调.到时params将会有更强大的作用

这就是 include的所有语法规则了, 理论上所有的语法规则都支持无限嵌套与任意组合

比如 include=a,b.c.d,c{b},c{b(f:b),a.b.c},c(b.a),c{f,b}.b(a:b).c

当然无论怎样的组合嵌套,你都无需担心n+1的问题

https://github.com/weiwenhao/tree-ql 欢迎Star~

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 4

apiato 也有相类似的功能

5年前 评论

@dreamfish apiato和dingo使用的是同一种东西,既transform. 实际上效果一般, 尤其不能避免n+1问题.

5年前 评论

我选择 GraphQL 来加速我的 API 开发。

5年前 评论
Jourdon

dingo 已经让我死心了

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!