以前、レコード一括取得APIに関連するトピックで「500件に拡張されたGET/recordsの優位性!」という記事をお届けしたことがありました。このモチベーションは、やはりいかに効率的に速くデータ取得出来るかということです。これまであまり意識していませんでしたが、最近はバッチやJavaScriptによる画面操作でも500件取得1コールでは済まないような処理を要するようなケースも増えてきて、実行時間や操作性の観点から注意するようになってきました。

結論としては、GET/recordsのリクエスト時には「fields」の設定も有効活用して、リクエストを軽く速くしよう!
ということです。

GET/recordsを軽く速しうる要素

複数のAPIコールを要するケースで処理を軽く速くするためには、
1. コール数を極力減らす(トレードオフするポイントがあればそれを加味する)
2. 1コールあたりのデータ・通信量を減らす
3. ネットワーク等の実行環境のグレードを上げる
が考えられますが、3.はこれを必要であればやってくださいという内容になってしまいますので、今回はスキップです。1.は前回確認した通り、クエリに「limit 500」を明示してリクエストコール数を減らした方が有利だったので、そうしましょう。

今回は2.に注目したいと思います。

ちなみに、APIを並列にコールするというのも考えられますが、制限事項に「APIによる同時アクセス数はドメインごとに10が上限です。」とありますので、控えるようにした方が良さそうです。

1リクエストコールあたりのGET/recordsのデータ・通信量を減らす方法

1リクエストコールあたりのデータ量を減らすには、取得するレコード数とその中のフィールド数を減らすしかありません。改めて、レコード一括取得APIのドキュメントを見ると、それぞれを適切にコントロール仕組みがしっかり用意されています。リクエスト例も見直してみましょう。

GET /k/v1/records.json HTTP/1.1
Host: example.cybozu.com:443
X-Cybozu-Authorization: QWRtaW5pc3RyYXRvcjpjeWJvenU=
Authorization: Basic QWRtaW5pc3RyYXRvcjpjeWJvenU=
Content-Type: application/json
Content-Length: 234

{
    "app": 8,
    "query": "updated_time > \"2012-02-03T09:00:00+0900\" and updated_time < \"2012-02-03T10:00:00+0900\" order by record_id asc limit 10 offset 20",
    "fields": ["record_id", "created_time", "dropdown"]
}

アプリIDを指定する「app」の他に「query」、「fields」というパラメータが指定できます。これらが今回のポイントです!

レコード数を減らす方法

レコード数を減らすためには、queryでしっかり絞り込んでやります。絞り込みは取得レコード数の総数が減ることからコール数を減らすことにも直結しますし、並べ替えはその後の処理のしやすさに繋がってきますので、queryを使われている方は多いのではないでしょうか。適切かつ有効に活用していきましょう!

書き方に慣れずに苦労されている方は、「REST APIのGET/recordsにおけるクエリの書き方のコツ【一覧の絞り込み条件を再利用する方法】」や「Chromeデベロッパーツールでkintone.api()の動きを確認してみよう!」も参考にしてみてください。

フィールド数を減らす方法

今回の主題です!フィールド数を減らすためにfieldsも指定しよう!という話です。取得フィールド数を減らすというのは、取得時に必要な項目だけ取得するということです。例えば、次の処理につなげる際にレコードIDだけ取得しておけば十分なんてことも結構あります。何も設定しないと20フィールド設定したアプリであればデータが入っていようがいまいが、無条件に1レコードにつき20フィールド+ビルトインフィールド(レコード番号や作成者等)分の項目を取得することになります。しっかり指定しているサンプルも多くはないのと、リクエストコール数が少ない場合には顕在化しにくいので、意外と使われている方少ないのではないかと思いますが、単純に1/20のスピードになることはないとしても結構なシェイプアップになりそうです。

検証

500件に拡張されたGET/recordsの優位性!」の時と同様にkintone Café 東京 Vol.4のハンズオンで利用したアプリ(21項目、41,188レコード)を利用します。レコード取得の方法は、前回と同じJavaScriptによる単純取得とコマンドラインツールによるCSV出力に、PythonとNode.jsを追加し、fieldsに何も指定せずに全フィールド取得する場合とfieldsにレコードID($id)のみ指定する場合をそれぞれ比較します。

JavaScript(kintone.api())による取得

/*
* dependencies: https://js.cybozu.com/jquery/1.11.3/jquery.min.js
*/
(function() {
  "use strict";

  kintone.events.on(['app.record.index.show', 'app.record.detail.show'], function(event) {
    if (!$('#timer')[0]) {
      var el;
      if (kintone.app.getHeaderMenuSpaceElement()) {
        el = kintone.app.getHeaderMenuSpaceElement();
      } else {
        el = kintone.app.record.getHeaderMenuSpaceElement();
      }
      $(el).append(
        $('<button>').prop('id', 'timer').addClass('kintoneplugin-button-normal').html('GET比較')
      );
    }

    $('#timer').click(function() {
      showSpinner();
      var app_id = kintone.app.getId();
      var condition = '';
      var limit = 500;
      var offset = 0;
      var fields = []; // 全フィールド取得:[] / レコードIDのみ取得:["$id"]

      console.time('kintone');
      getRecords(app_id, condition, limit, offset, fields, function(resp){
        console.timeEnd('kintone');
        alert('全てのレコードを取得しました(' + resp.records.length + '件)');
      },function(err_resp){
        console.timeEnd('kintone');
        alert('レコード取得時にエラーが発生しました');
      });
    });
    return event;
  });

  function getRecords(app_id, condition, lmt, ofs, fields, callback, errback, data) {
    var limit_num = (lmt === undefined) ? 100 : lmt;
    var limit = ' limit ' + limit_num;
    var offset = (ofs === undefined) ? '' : ' offset ' + ofs;
    var query = condition + limit + offset;
    if (!data) {
      var data = {
        records: []
      };
    }
    kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
      app: app_id,
      query: query,
      fields: fields
    }, function(resp) {
      data.records = data.records.concat(resp.records);
      if (resp.records.length < limit_num) {
        callback(data);
      } else {
        ofs = parseInt(ofs) + resp.records.length;
        getRecords(app_id, condition, limit_num, ofs, fields, callback, errback, data);
      }
    }, errback);
  }
})();

コマンドラインツール(Go製)による取得

・全フィールド取得の場合

time ./cli-kintone -a 10 -d kintone-cafe-tokyo-4 -t MsCgUh96ln8I94AnnIdJRDIyvbRfpql0sakQtKE8 > data_all.csv

・レコードIDのみ取得の場合

time ./cli-kintone -a 10 -d kintone-cafe-tokyo-4 -c "$id" -t MsCgUh96ln8I94AnnIdJRDIyvbRfpql0sakQtKE8 > data_recId.csv

Python2による取得

import httplib, urllib, urllib2, json, base64, os, sys, time

if __name__ == '__main__':
    # kintone情報
    kintoneDomain = "kintone-cafe-tokyo-4.cybozu.com"
    appId = "10"
    authToken = base64.b64encode("user:password")

    start = time.time()
    # レコードが500件以上への対応用パラメータ
    response = {
        'records': []
    }
    offset = 0
    limit = 500
    # レコードが500件以上への対応のためのループ
    while True:
        query = urllib.quote('limit ' + str(limit) + ' offset ' + str(offset))  + '&fields[0]=$id' # 全フィールド取得の際には「+」の前でコメントアウト
        query = "?app=" + appId + '&query='+query
        #print(query)
        headers = {"X-Cybozu-Authorization": authToken}
        connect = httplib.HTTPSConnection(kintoneDomain + ":443")
        connect.request("GET", "/k/v1/records.json" + query, {}, headers)
        res = connect.getresponse()
        responseText = res.read()
        #print(responseText)
        tmp_resp = json.loads(responseText)
        for record in tmp_resp['records']:
            response['records'].append(record)
        if len(tmp_resp['records']) < limit:
            break
        else:
            offset += limit
    elapsed_time = time.time() - start
    print ("kintone :{0}".format(elapsed_time)) + "[sec]"
    print("records size :"+str(len(response['records'])))

Node.jsによる取得

/* モジュールの読み込み */
var request = require('request');
var fs = require('fs');
var moment = require('moment');
var async = require('async');

/* kintoneパラメータ */
var API_VERSION = '/k/v1/';
var DOMAIN = 'kintone-cafe-tokyo-4.cybozu.com';
var JSON_CONTENT = 'application/json';
var AUTH_VALUE = new Buffer('user:password').toString('base64');
var BASIC_AUTH_VALUE = new Buffer('id:password').toString('base64');
var auth_headers = { // ボディにJSONをセットしない(クエリ指定)時、ファイルアップロード時等のヘッダ
  'X-Cybozu-Authorization': AUTH_VALUE,
  'Authorization': 'Basic ' + BASIC_AUTH_VALUE
};
var content_headers = { // ボディにJSONをセットする時のヘッダ
  'X-Cybozu-Authorization': AUTH_VALUE,
  'Authorization': 'Basic ' + BASIC_AUTH_VALUE,
  'Content-Type': JSON_CONTENT
}

console.time('kintone');
getRecords(3471, '', 500, 0, ["$id"], function(resp){
  console.timeEnd('kintone');
  console.log(resp.records.length);
});

// レコード取得の関数
function getRecords(app_id, condition, lmt, ofs, fields, callback, data) {
  var limit_num = (lmt === undefined) ? 500 : lmt;
  var limit = ' limit ' + limit_num;
  var offset = (ofs === undefined) ? '' : ' offset ' + ofs;

  var query = condition + limit + offset;

  if (!data) {
    var data = {
      records: []
    };
  }

  simpleRequest('GET', "https://" + DOMAIN + API_VERSION + "records.json", content_headers, {
    app: app_id,
    query: query,
    fields: fields
  }, function(resp) {
    //console.log(resp);
    data.records = data.records.concat(resp.records);
    if (resp.records.length < limit_num || limit_num == 1) { callback(data); } else { ofs = parseInt(ofs) + resp.records.length; getRecords(app_id, condition, limit_num, ofs, fields, callback, data); } }); } /* kintone REST APIコール用のライトな関数 */ function simpleRequest(method, url, headers, body, callback) { var config; config = { method: method, url: url, headers: headers }; if (url.indexOf('file.json') >= 0 && method === 'POST') { // ファイルアップロード時はform形式のボディ
    //console.log("formData REQUEST:");
    config['formData'] = body;
  } else { // それ以外は通常JSON形式のボディ
    //console.log("JSON REQUEST:");
    config['json'] = body;
  }
  return request(config, function(err, response, body) {
    var e, json;
    if (err) {
      callback(err);
    }
    try {
      json = body;
    } catch (_error) {
      e = _error;
      callback(new Error(e, null));
    }
    if (typeof json !== 'object') {
      json = JSON.parse(json);
    }
    return callback(json);
  });
}

結果・考察

MacBook Air 13インチでの実行

全フィールド取得(fields指定なし) レコードIDのみ取得(fields[0]=$id)
JavaScript 1m52s 1m23s
コマンドラインツール 1m47s 1m36s
Python2 7m51s 1m55s
Node.js 6m25s 1m47s

・fieldsの指定は効果がある(効果幅にばらつきはあるが)
・Go製のコマンドラインツールは相対的には、流石の速さ。それに比べると、PythonとNode.jsの遅さ
・PythonよりNode.jsが速い

AWS Lambda(東京リージョン)での実行

実は今回PythonやNode.jsを追加したのは、AWS Lambdaでの利用を意図したものでした。Lambdaからkintoneへのアクセス時にPythonとNode.jsでどちらが速いかを知りたかったのです。そちらの結果も貼っておきます。コードはLambdaの構文に合わせて修正が必要な部分以外はそのままです。

全フィールド取得(fields指定なし) レコードIDのみ取得(fields[0]=$id)
Python2 1m20s 1m08s
Node.js 1m13s 1m06s

・やはりPythonよりNode.jsが速い
・Macでやった時に比べると、fileds指定の効果が薄らいだ

まとめ

fieldsの指定は環境や言語によってバラツキはあるものの効果はありました。PythonかNode.jsかで言うとややNode.jsが速い感じでしたが、この2つでは大差はない言えそうです。

 

 


株式会社ジョイゾー