使用 TreeQL 加速你的 API 開發

Max發表於2019-01-21

什麼是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~

相關文章