2020年5月12日火曜日

ブログのテーマ変えた

前のテーマは4年くらい使っていたのだけど、フォントが大きすぎるのと、デザインが今時じゃなくなってきたので、変えてみました。シンプルで落ち着くデザインなので、しばらくこれで。

kintoneのデータをSQL文で加工する

最近kintone漬け。
といっても、新しい機能を追加していくような楽しい作業ではなく、運用中のアプリの不具合対応(デバッグ)と細かい改良ばかりで、少し疲れてきた。

そんな中で、kintoneのデータの加工・集計をラクにやる方法はないものか? と調べていたら、
kintone でSQLを使う
という記事を発見。
alasqlというJavascript用データベースを使うと、kintoneのデータをSQLで集計できるじゃん。これすごい。
SUM(Group By)で合計値を求めたり、複数のアプリのデータをJoinで組み合わせたりできる。
(逆に言うと、こんな簡単なことがkintone標準機能ではできなかったりするのだが…)

"kintone alasql"でググるとそこそこ記事が出てくるので、もしかしてkintoneガチ勢には当たり前の話?

早速、工数管理アプリに入っている工数明細をプロジェクト単位・個人単位でSUMする画面を作ってみたので、備忘のために記事に残しておく。

元データはこんな感じ。

Figure.1 集計元のkintoneアプリ
設計方針

データは、event.recordsではなくAPIで取得する(event.recordsは最大でも100件しかデータが取れないので、集計目的では使えない)。 APIは生で叩くのではなく、kintone-rest-api-clientを利用。(getAllRecordsWithCursorが最強。みんな使うといいと思う。)

データの表示にはjQuery DataTablesを利用する。DataTablesを使って多機能な一覧画面を作ろう! – cybozu developer network とかが参考になる。

動作環境

アプリ設定画面の「JavaScript / CSSでカスタマイズ」にて以下URLを指定。

https://js.cybozu.com/jquery/3.4.0/jquery.min.js
https://js.cybozu.com/datatables/v1.10.19/js/jquery.dataTables.min.js
https://unpkg.com/@kintone/rest-api-client@latest/umd/KintoneRestAPIClient.min.js
https://js.cybozu.com/datatables/v1.10.19/css/jquery.dataTables.min.css

また、http://alasql.org/ から alasql.min.js をDLし、PC用のJavaScriptファイルにアップロードしておく(alasqlのzipをダウンロードして解凍し、distディレクトリから取り出せば良い)。

もう1点、DataTablesの見栄えをkintoneに近づけるため、DataTablesをkintoneライクにする魔法のコード - Qiitaからkintone-datatables.cssをDLし、PC用のCSSファイルにアップロードしておくとよい(作者の方に感謝)。

サンプルコード HTML

アプリに一覧「プロジェクト別集計」を作成しておく。
レコード一覧の表示形式を「カスタマイズ」にし、以下のHTMLを記述。

サンプルコード js
/*
 * project_summary.js
 * 工数管理アプリのデータをプロジェクト別・個人別に集計
 * 
 * required: jquery.dataTables.min.js
 * https://js.cybozu.com/datatables/v1.10.19/js/jquery.dataTables.min.js
 * 
 * required: kintone-rest-api-client
 * https://unpkg.com/@kintone/rest-api-client@latest/umd/KintoneRestAPIClient.min.js
 * 
 * requeired: alasql.min.js
 * http://alasql.org/
 */
(function() {
  "use strict";
  kintone.events.on(['app.record.index.show'], function(event){
    if(event.viewName !== 'プロジェクト別集計') return;

    // Initialize kintone-rest-api-client
    const client = new KintoneRestAPIClient();

    var myTable;

    var appId = kintone.app.getId();
    var query = kintone.app.getQuery();
    // APIでデータ取得
    client.record.getAllRecordsWithCursor({
      app: appId,
      query: query,
    }).then(function(response){

      // kintoneのrecords(json)を、alasqlで扱える配列に変換
      function convertToRows(records) {
        var rows = records.map(function(record){
          var keys = Object.keys(record);
          var row = {};
          keys.map(function(key){
            switch (key) {
              case '担当者氏名':
                // '担当者氏名'フィールドからname(=氏名)を取り出す
                row['担当者氏名'] = record[key].value[0]['name'];
                break;
              case '作業担当部署':
                // '作業担当部署'フィールドからname(=部署名)を取り出す
                row['作業担当部署'] = record[key].value[0]['name'];
                break;
              default:
                // 他のフィールドはvalueを取り出す。数値の場合はNumberに変換
                row[key] = record[key].type === 'NUMBER' ? Number(record[key].value) : record[key].value;
            }
          });
          return row;
        });
        return rows;
      }
      var recordSet = convertToRows(response);

      // sum([工数]) by alasql
      var sqlResult = alasql(
          "SELECT [プロジェクトコード], [作業担当部署], [担当者氏名], SUM([工数計_人月]) AS [工数計_人月] FROM ? GROUP BY [プロジェクトコード], [作業担当部署], [担当者氏名]", 
        [recordSet]
      );

      // 表に表示するカラム(alasqlの出力結果の項目名)
      var columns = [
        'プロジェクトコード',
        '作業担当部署',
        '担当者氏名',
        '工数計_人月',
      ];

      // 表のタイトルを好きにいじれるよう、別変数で定義。このサンプルではあまり意味がない。
      var titles = {
        プロジェクトコード: 'プロジェクトコード',
        作業担当部署: '作業担当部署',
        担当者氏名: '担当者氏名',
        工数計_人月: '工数計',
      };

      // DataTables初期化
      myTable = $('#myTable').DataTable({
        // DataTableの設定値はお好みで。
        destroy: true,
        pageLength: 100,
        lengthChange: false,
        searching: false,
        info: false,
        data: sqlResult.map(function(record){
          return columns.reduce(function(data, column){
            data[column] = record[column];
            return data;
          }, {
            レコード番号: record['レコード番号']
          });
        }),
        columns: columns.map(function(column){
          return {
            title: this[column],
            data: column
          }
        }, titles)
      });
    });
  });
})();
動作イメージ

サンプルの動作結果はこんな感じ。

Figure.2 カスタマイズ結果


画像にモザイクがかかりまくっていて何がなんだか分からないが、結果はとても良好。パフォーマンスも今のところ申し分ない。

今回はSUMしか使っていないが、そのうちJoinも使ってみたい。