Browse Source

Docsify Auto Published

Willin Wang 6 years ago
parent
commit
f9afbcd312

+ 1 - 0
README.md

@@ -47,6 +47,7 @@ iOS 用户可以使用 `Safari` 将本书添加到主屏幕:
 ## CHANGELOG
 
 - TBD: //TODO: 文章越来越多, 得将文章整理整理, 把一些老旧的删除或者归档. 再更新一些内容, 完善原有章节使其更加详尽. 另外打算再换回 Gitbook, 一方面是利于SEO优化,另一方面是方便电子书导出。
+- 2018.2.28: 开设 GraphQL 专栏
 - 2017.06.26: 优化阅读体验,下一次回来的时候会自动跳转到上一次的位置继续阅读。欢迎 iOS 用户将本书添加到主屏幕
 - 2017.06.20: 将本书的网站从 `GitBook` 转移到了 `Docsify`
 - 2016.08.03: 该项目创建

+ 4 - 2
_sidebar.md

@@ -100,9 +100,11 @@
     - [基于接口快速搭建前后分离项目](experience/advanced/webapp-proxy.md)
     - [Vue + Koa前后端分离实践](experience/advanced/vue-webapp.md)
     - [手把手发布Vue组件](experience/advanced/vue-components.md)
-    - [MySQL向GraphQL迁移](experience/advanced/mysql-graphql.md)
-    - [GraphQL鉴权](experience/advanced/graphql-authorization.md)
     - [服务端代码打包](experience/advanced/webpack-server.md)
+  - GraphQL
+    - [MySQL向GraphQL迁移](experience/graphql/mysql.md)
+    - [GraphQL鉴权](experience/graphql/authorization.md)
+    - [DataLoader](experience/graphql/dataloader.md)
 - 思想篇
   - 能力
     - [新人成长](mind/capability/growth.md)

BIN
_static/experience/graphql/dataloader.png


experience/advanced/graphql-authorization.md → experience/graphql/authorization.md


+ 196 - 0
experience/graphql/dataloader.md

@@ -0,0 +1,196 @@
+# DataLoader
+
+源码: <https://github.com/facebook/dataloader>
+
+Dataloader 是由 Facebook 推出,能大幅降低数据库的访问频次,经常在Graphql场景中使用。
+
+## Dataloader机制
+
+主要通过2个机制来降低数据库的访问频次:`批处理` 和 `缓存`。
+
+### 批处理
+
+![dataloader](/_static/experience/graphql/dataloader.png)
+
+配合 MySQL 批量查询用户(User 表)的示例代码:
+
+```js
+const DataLoader = require('dataloader');
+// 自行封装
+const { query, format } = require('./mysql');
+
+/*
+用户信息 存储在 User 表 和 UserMeta 表中, 通过 uid 字段进行关联
+*/
+const UserLoader = new DataLoader((uids) => {
+  const sql = format('SELECT t1.*,t2.* FROM USERTABLE t1 LEFT JOIN USERMETATABLE t2 ON t1.uid = t2.uid WHERE t1.uid in (?)', [uids]);
+  return query(sql).then(rows => uids.map(uid => rows.find(row => row.uid === uid) || new Error(`Row not found: ${uid}`)));
+}, { cache: false });
+
+// Usage:
+const user1 = UserLoader.load(1);
+const user2 = UserLoader.load(2);
+const user3 = UserLoader.load(3);
+Promise.all([user1, user2, user3]).then(users => { 
+
+});
+// Or
+UserLoader.loadMany([1, 2, 3]).then(users => {  
+
+});
+```
+以上代码就仅会产生以下一条数据库查询语句:
+
+```bash
+ Executing (default): SELECT t1.*,t2.* FROM USERTABLE t1 LEFT JOIN USERMETATABLE t2 ON t1.uid = t2.uid WHERE t1.uid in (1, 2, 3);
+```
+
+### 缓存
+
+Load一次,DataLoader就会把数据缓存在内存,下一次再load时,就不会再去访问后台。
+
+DataLoader缓存的是promise,而不是具体数据。则意味着:
+
+```js
+let user1, user2;
+await user1 = UserLoader.load(1); 
+await user2 = UserLoader.load(1);
+assert(user1 !== user2)
+// true,这个容易理解 
+
+assert(UserLoader.load(1) === userLoader.load(1)) 
+// 还是true,因为是缓存promise
+```
+
+基础使用参考: <https://www.jianshu.com/p/fbd1257116b0>
+
+## 进阶使用
+
+以一个稍微复杂一点的嵌套分页查询为例(可以参考 Github API v4 进行研究学习)。
+
+```graphql
+{
+  repository(owner: "octocat", name: "Hello-World") {
+    pullRequest(number: 1) {
+      commits(first: 10) {
+        totalCount
+        edges {
+          node {
+            commit {
+              oid
+              message
+            }
+          }
+        }
+      }
+      comments(first: 10) {
+        totalCount
+        edges {
+          cursor
+          node {
+            body
+            author {
+              login
+            }
+          }
+        }
+      }
+      reviews(first: 10, before: "Y3Vyc29yOnYyOpHOABRzYg==", after: "Y3Vyc29yOnYyOpHOANFzxQ==") {
+        totalCount
+        edges {
+          node {
+            state
+          }
+        }
+      }
+    }
+  }
+}
+```
+
+该查询中包含多个分页(Connection)。
+
+### MySQL 分页查询
+
+常规查询:
+
+```sql
+SELECT count(1) as count FROM TABLE WHERE ?;
+SELECT * FROM TABLE WHERE ? LIMIT ? OFFSET ?;
+```
+
+需要两条查询完成一次分页,嵌套分页则根据条目(N)再进行 2*N 次查询。
+
+### CountLoader
+
+```js
+const CountLoader = new DataLoader((args) => {
+  const arr = args.map(([TABLE, WHERE]) => [md5(TABLE + JSON.stringify(WHERE)), TABLE, parseArgs(WHERE)]);
+  return query(arr.map(([CODE, TABLE, WHERE]) =>
+    format(`SELECT ? as code, COUNT(1) as count FROM ??${WHERE ? ' WHERE ? ' : ''}`, [CODE, TABLE, WHERE]))
+    .join(' UNION '))
+    .then(rows => arr.map(([CODE]) => {
+      const { count = 0 } = rows.find(row => row.code === CODE) || {};
+      return count;
+    }));
+});
+
+CountLoader.loadMany([
+  ['TABLE1',{uid: 1}],
+  ['TABLE2',{oid: 2}],
+  // ...
+]);
+```
+
+最终会拼成:
+
+```sql
+SELECT xxx as code FROM TABLE1 WHERE xxx 
+UNION SELECT xxx as code FROM TABLE2 WHERE xxx 
+-- ...
+```
+
+一条 SQL 查询,然后再分别根据 code 参数进行回填。
+
+### ComplexLoader
+
+复杂数据的 DataLoader 示例:
+
+```js
+/**
+ * TicketsLoader
+ * Each arg:
+ * {  time: {before, after}, // Int, Int
+ *    where, // obj: {1:1, type:'xxx'}
+ *    order, // 'DESC' / 'ASC'
+ *    limit // Int
+ * }
+ */
+exports.TicketsLoader = new DataLoader((args) => {
+  const result = args.map(({
+    time: { before, after }, where, order, limit
+  }) => {
+    let time = [];
+    if (before) {
+      time.push(format('createdAt < ?', [before]));
+    }
+    if (after) {
+      time.push(format('createdAt > ?', [after]));
+    }
+    if (time.length > 0) {
+      time = `AND ${time.join(' AND ')}`;
+    } else {
+      time = '';
+    }
+    let sql;
+    if (where) {
+      sql = format(`SELECT * from ?? WHERE ?${time} ORDER BY createdAt ${order} LIMIT ?`, [TICKETTABLE, where, limit]);
+    } else {
+      sql = format(`SELECT * from ?? WHERE 1=1${time} ORDER BY createdAt ${order} LIMIT ?`, [TICKETTABLE, limit]);
+    }
+    return query(sql);
+  });
+  return Promise.all(result);
+}, { cache: false });
+```
+

experience/advanced/mysql-graphql.md → experience/graphql/graphql.md