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 カスタマイズライフの一助になれたら幸いでございます。