kintone.Promiseとは

著者名:武井 琢治

目次

information

Promiseの使い方やkintoneカスタマイズにおける非同期処理については、チュートリアルの次の記事を参照してください。

はじめに

この記事はkintoneカスタマイズ初心者の方向けに、kintone.Promiseについて説明する記事です。
本当の要点だけに焦点を絞り、一番優しい「やんわり記事」をめざして書いていきます。

処理の順番がうまくいかない時や、処理がうまく反映されない時に、問題解決のヒントになる内容です。

使いどころ

上図のように、kintoneのレコード保存イベントで使用することが多いです。

基本的な使い方の例

上図のように、売上レコードを作成したら、即在庫数に反映させるようなシステムを作成します。

アプリの準備

以下の3つのアプリを用意します。

商品マスター
フィールドコード フィールドタイプ 備考
商品名 文字列(1行)
単価 数値
売上管理
フィールドコード フィールドタイプ 備考
商品名 ルックアップ 商品マスターの商品名をコピー元のフィールドとする。
単価 数値 商品マスターの単価をコピーする。
売上数量 数値
在庫連携 ラジオボタン 未処理/連携済/エラー
在庫管理
フィールドコード フィールドタイプ 備考
商品名 ルックアップ 商品マスターの商品名をコピー元のフィールドとする。
単価 数値 商品マスターの単価をコピーする。
在庫 数値
評価額 計算 計算式に「単価 * 在庫」

アプリテンプレート

kintone.Promise超入門.zip をダウンロードし、環境に適用してください。
アプリテンプレートの使用方法は テンプレートファイルからアプリを作成する (External link) を参照してください。
適宜利用してください。(JavaScriptファイルは抜いてあります)

JavaScriptファイルの準備

売上管理アプリの「アプリの設定 > JavaScript / CSSでカスタマイズ」に以下のサンプルコードを設定します。
サンプルコードはエディタにコピーして、ファイル名を「sales.js」、文字コードを「UTF-8」で保存します。
ファイル名は任意ですが、ファイルの拡張子は「js」にしてください。

15行目のzaikoAppIdの値は、作成した在庫管理アプリのアプリIDに変更してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
 * kintone.Promiseを説明するサンプルプログラム
 * Copyright (c) 2016 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

(() => {
  'use strict';

  kintone.events.on('app.record.create.submit', (event) => {
    const record = event.record;
    // 在庫管理アプリのアプリID
    const zaikoAppId = 123;
    return new kintone.Promise((resolve, reject) => {
      // 商品名が一致する在庫を取得
      kintone.api(kintone.api.url('/k/v1/records', true), 'GET',
        {app: zaikoAppId, query: `商品名 = "${record.商品名.value}"`}, (resp) => {
          // 在庫から売上数量だけ差し引く
          const zaiko = resp.records[0].在庫.value - record.売上数量.value;
          if (zaiko < 0) {
            record.在庫連携.value = 'エラー';
            resolve(event);
          } else {
            const body = {
              id: resp.records[0].$id.value,
              app: zaikoAppId,
              record: {
                在庫: {
                  value: zaiko
                }
              }
            };
            kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body, () => {
              record.在庫連携.value = '連携済';
              resolve(event);
            });
          }
        });
    });
  });
})();

プログラムの解説

16
return new kintone.Promise((resolve, reject) => { ... })

kintoneのイベントにおいて、基本的に処理の最後はreturn event;とすることが多いかと思いますが、ここでは処理の最初にkintone.Promisereturnしています。

22
23
24
if (zaiko < 0) {
  record.在庫連携.value = 'エラー';
  resolve(event);

在庫が売上数量に満たない場合は、保存後の売上レコードのラジオボタンを「エラー」にしています。
resolvekintone.Promiseを使うためのおまじないです。
(具体的には成功したkintone.Promiseオブジェクトを生成し返却しますが、よく分からなければ「kintone.Promiseの最終地点に置くもの」でよいと思います)

35
36
37
38
kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body, () => {
  record.在庫連携.value = '連携済';
  resolve(event);
});

在庫レコードを上書きしたうえで、売上レコードのラジオボタンを「連携済」にしています。
resolveのおまじないはどの道をたどっても通るようにしましょう。

やりがちな失敗パターン

以下のサンプルコードでは「在庫連携」フィールドは更新されません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
 * 処理に失敗するサンプルコード
 * Copyright (c) 2016 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

(() => {
  'use strict';

  kintone.events.on('app.record.create.submit', (event) => {
    const record = event.record;
    // 在庫管理アプリのアプリID
    const zaikoAppId = 123;

    kintone.api(kintone.api.url('/k/v1/records', true), 'GET',
      {app: zaikoAppId, query: '商品名 = "' + record.商品名.value + '"'}, (resp) => {
        // 在庫から売上数量だけ差し引く
        const zaiko = resp.records[0].在庫.value - record.売上数量.value;
        if (zaiko < 0) {
          record.在庫連携.value = 'エラー';
          return event;
        }
        const body = {
          id: resp.records[0].$id.value,
          app: zaikoAppId,
          record: {
            在庫: {
              value: zaiko
            }
          }
        };
        kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body, () => {
          record.在庫連携.value = '連携済';
          return event;
        });
      });
  });
})();

kintone.Promiseを使用せずに何とかしようとすると、多くの場合、このように書きたくなります。
この場合、売上レコードのrecord.在庫連携.valueは変更されずに「未処理」のままでレコード登録されてしまいます。

その理由は、以下のハイライトされた処理よりも先に、12行目のkintone.events.on('app.record.create.submit', (event) => {...})の処理が終わってしまうためです。
このように上から順番の処理にならないことを「非同期処理」と呼びます。

17
18
19
20
21
kintone.api(kintone.api.url('/k/v1/records', true), 'GET',
  {app: zaikoAppId, query: '商品名 = "' + record.商品名.value + '"'}, (resp) => {
    // 在庫から売上数量だけ差し引く
    const zaiko = resp.records[0].在庫.value - record.売上数量.value;
    // 省略
34
35
36
37
38
    kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body, () => {
      record.在庫連携.value = '連携済';
      return event;
    });
  });

return new kintone.Promise((resolve, reject) => { ... })を使用することで、上記のサンプルコードのような、上から順番に終わってくれない非同期処理の終了を待ってくれるのです。
「準備できたので、もう待ってくれなくてもいいですよ」の合図がresolveのおまじないというわけです。

その他の用途

以上が基本的な使用方法です。
以下は発展情報ですので「そういうのもあるのか」程度の認識で問題ありません。

その他の用途例

  • 繰り返し構文の中で非同期関数を使用し、すべてが完了してから、実行したい処理がある場合
  • 非同期関数の戻り値によった処理をしたい場合
  • kintone.Promiseがひとつでもresolveされたら、実行したい処理がある場合
  • then()によるメソッドチェーンでコールバック地獄を回避したい場合

複数のkintone.Promiseを待ち合わせする例

売上管理アプリでレコード編集する場合、在庫の書き換えをするだけでなく、当該レコードに「〇個売りました」というコメントを投稿する例を紹介します。

流れとしては、以下のようになります。

サンプルコード

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*
 * 複数のkintone.Promiseを待ち合わせするサンプルプログラム
 * Copyright (c) 2016 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

(() => {
  'use strict';

  kintone.events.on('app.record.edit.submit', (event) => {
    const record = event.record;
    // 在庫管理アプリのアプリID
    const zaikoAppId = 123;

    // 在庫管理の在庫を変更する
    const zaikoChange = new kintone.Promise((resolve, reject) => {
      // 商品名が一致する在庫を取得
      kintone.api(kintone.api.url('/k/v1/records', true), 'GET',
        {app: zaikoAppId, query: `商品名 = "${record.商品名.value}"`}, (resp) => {
          // 在庫から売上数量だけ差し引く
          const zaiko = resp.records[0].在庫.value - record.売上数量.value;
          if (zaiko < 0) {
            resolve('エラー');
          } else {
            const body = {
              id: resp.records[0].$id.value,
              app: zaikoAppId,
              record: {
                在庫: {
                  value: zaiko
                }
              }
            };
            kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body, () => {
              resolve('連携済');
            });
          }
        });
    });

    // レコードにコメントを登録する
    const comment = new kintone.Promise((resolve, reject) => {
      const body = {
        app: kintone.app.getId(),
        record: kintone.app.record.getId(),
        comment: {
          text: `${record.売上数量.value}個売りました`
        }
      };
      kintone.api(kintone.api.url('/k/v1/record/comment', true), 'POST', body, () => {
        resolve();
      });
    });

    return kintone.Promise.all([zaikoChange, comment]).then((results) => {
      alert('処理完了!');
      if (results[0] === '連携済') {
        record.在庫連携.value = '連携済';
      } else {
        record.在庫連携.value = 'エラー';
      }
      return event;
    });
  });
})();

プログラムの解説

在庫を連携する処理
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 在庫管理の在庫を変更する
const zaikoChange = new kintone.Promise((resolve, reject) => {
  // 商品名が一致する在庫を取得
  kintone.api(kintone.api.url('/k/v1/records', true), 'GET',
    {app: zaikoAppId, query: `商品名 = "${record.商品名.value}"`}, (resp) => {
      // 在庫から売上数量だけ差し引く
      const zaiko = resp.records[0].在庫.value - record.売上数量.value;
      if (zaiko < 0) {
        resolve('エラー');
      } else {
        const body = {
          id: resp.records[0].$id.value,
          app: zaikoAppId,
          record: {
            在庫: {
              value: zaiko
            }
          }
        };
        kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body, () => {
          resolve('連携済');
        });
      }
    });
});

前回のサンプルと違って、zaikoChange変数にkintone.Promiseを入れています。
その他の処理はほぼ変わらずですが、最後のresolve()で「エラー」や「連携済」の文字列を渡していることを押さえておいてください。

編集/保存した売上レコードにコメントを登録する処理
43
44
45
46
47
48
49
50
51
52
53
54
55
// レコードにコメントを登録する
const comment = new kintone.Promise((resolve, reject) => {
  const body = {
    app: kintone.app.getId(),
    record: kintone.app.record.getId(),
    comment: {
      text: `${record.売上数量.value}個売りました`
    }
  };
  kintone.api(kintone.api.url('/k/v1/record/comment', true), 'POST', body, () => {
    resolve();
  });
});

こちらも同じくcomment変数にkintone.Promiseを入れています。

kintone.Promise.allを使った処理
57
58
59
60
61
62
63
64
65
return kintone.Promise.all([zaikoChange, comment]).then((results) => {
  alert('処理完了!');
  if (results[0] === '連携済') {
    record.在庫連携.value = '連携済';
  } else {
    record.在庫連携.value = 'エラー';
  }
  return event;
});

この部分、よく見るとkintone.Promise.allになっています。
これにより、[zaikoChange, comment]、すなわち在庫連携処理とコメント投稿処理の両方が終わるまで、待ち合わせることができます。
処理が終わったら、「処理完了!」のアラートを表示します。

その後results[0]を参照して分岐していますが、このresults[0]には上記在庫処理のresolve()で渡した文字列が入っています。

したがって、渡されて来た文字列によって処理を分岐することが可能となります。
渡された文字列が「連携済」なら売上レコードの在庫連携を「連携済」にし、渡された文字列が「エラー」なら売上レコードの在庫連携を「エラー」にして処理を完了します。

then() を使った処理

同様の待ち合わせ処理は次のようにthen()でつなげることでも実現可能となります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
 * thenで繋いで複数のkintone.Promiseを待ち合わせするサンプルプログラム
 * Copyright (c) 2016 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

(() => {
  'use strict';

  kintone.events.on('app.record.edit.submit', (event) => {
    return new kintone.Promise((resolve, reject) => {
      const record = event.record;
      // 在庫管理アプリのアプリID
      const zaikoAppId = 123;
      // 在庫管理の在庫を変更する
      // 商品名が一致する在庫を取得
      kintone.api(kintone.api.url('/k/v1/records', true), 'GET',
        {app: zaikoAppId, query: `商品名 = "${record.商品名.value}"`}).then((resp) => {
        // 在庫から売上数量だけ差し引く
        const zaiko = resp.records[0].在庫.value - record.売上数量.value;
        if (zaiko < 0) {
          resolve('エラー');
        } else {
          const body = {
            id: resp.records[0].$id.value,
            app: zaikoAppId,
            record: {
              在庫: {
                value: zaiko
              }
            }
          };
          kintone.api(kintone.api.url('/k/v1/record', true), 'PUT', body).then(() => {
            // レコードにコメントを登録する
            const param = {
              app: kintone.app.getId(),
              record: kintone.app.record.getId(),
              comment: {
                text: `${record.売上数量.value}個売りました`
              }
            };
            kintone.api(kintone.api.url('/k/v1/record/comment', true), 'POST', param).then(() => {
              resolve('連携済');
            });
          });
        }
      });
    }).then((resp_) => {
      alert('処理完了!');
      if (resp_ === '連携済') {
        event.record.在庫連携.value = '連携済';
      } else {
        event.record.在庫連携.value = 'エラー';
      }
      return event;
    });
  });
})();

おわりに

kintone.Promiseの勘所について、一番簡単にお伝えする試みでしたが、いかがでしたでしょうか。
ここだけ押さえておけば、後はkintone.Promiseをさらに便利に使うもよし、あるいはまったく別な手段で作り上げるもよしと、活路を開くことができると思います。

皆様のすばらしいkintoneカスタマイズライフの一助になれたら幸いでございます。