kintone における Promise の書き方の基本

著者名:kintone エバンジェリスト 村濱 一樹 (External link)

目次

はじめに

今までにJavaScriptカスタマイズをしたことがある方は、「Promise」について聞いたことや使ったことがあると思いますが、プログラミングに慣れていないと難しい概念だと思います。
すでにPromiseに関する記事はこのcybozu developer network内にいくつか公開されています。
ですが、「Promiseって結局どういう風に書くんだっけ?」というときのために、書き方について重点的にこの記事でまとめたいと思います。

Promise に関する既存の記事

過去のPromiseに関する記事はこちらです。

Promise を使う利点

kintoneでPromiseを使う利点は大きく2つあります。

  • レコード作成時などに、処理を待ってからレコードを保存できる。(同期的処理)
    「あるアプリAのレコードを保存時、アプリBのレコードを取得し、その値を利用」というようにレコードの保存時などにkintone APIを使って他のデータを取得したり変更したり、同期的に処理できます。
    Promiseに対応しているイベントは、 イベントを参照ください。

  • コールバック関数を使った記述と比較してPromiseを用いたほうが簡素に記述できる。
    レコード詳細画面の表示時などは同期処理は不要なのですが、コールバック関数を使った方法より簡素に記述できるため、Promiseに対応しているイベントなどでは基本的にはコールバック関数方式よりもPromise方式をおすすめします。

Promise の書き方の基本

次の例を考えてみます。

  • 例)見積もりアプリ(カスタマイズするアプリ)のレコード保存時、商品アプリ(アプリID: 1)から商品A(レコードID: 1)の⾦額を取得し、その値を見積もりアプリに登録する。

Promise を利用する(一回)

  • Promiseをつかって同期処理をする。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    (() => {
      kintone.events.on('app.record.create.submit', (event) => {
        // 商品アプリからデータ取得する
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp) => {
          // 商品アプリから取得したデータを見積もりアプリのフィールドに代入して保存
          event.record.価格.value = Number(resp.record.価格.value);
          return event;
        });
      });
    })();

このようにkintone.api()はコールバック関数を省略するとPromiseオブジェクトが戻り値になります。

1
kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}); // これでPromiseオブジェクトが生成される

それをreturnしてあげることによってkintone側でapp.record.create.submit時など、処理を待ってくれるしくみをkintoneは持っています。
Promiseオブジェクトをreturnしないと処理をまってくれないので注意しましょう。
逆にいえば、レコード詳細ページなど、処理を待たせる必要がなければPromiseオブジェクトのreturnは必須でないです。

また、Promiseが正常終了した場合はthen()で結果を受け取ることができます。
エラーが発生した場合は、catch()を使うことでデータ取得に失敗したときなどの制御ができます。

  • thencatchを使った例は次のとおりです。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        // 商品アプリからデータ取得する(thenを使うことで resp の中にデータが格納される)
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp) => {
          // 商品アプリから取得したデータを見積もりアプリのフィールドに代入して保存
          event.record.価格.value = Number(resp.record.価格.value);
          return event;
        }).catch((resp) => {
          // エラー表示をする
          event.error = resp.message;
          return event;
        });
      });
    })();

Promise を利用する(複数回)

問題はここからです。
なんとなくPromiseを使っている方にとっては、複数Promiseを利用する方法がわからない方も多いと思います。
そのため、ここでは整理しながらみていきましょう。

  • 例)レコード保存時のイベントで、商品アプリ(アプリID: 1)から商品A, B, C(レコードID: 1, 2, 3)の金額を取得して、その合計を見積もりアプリ(カスタマイズするアプリ)に登録する。

前述の例に加え、さらに取得するレコードを増やしてみます。実際にはレコード一括取得APIを使えば1回のAPI呼び出しで済みますが、今回はProimseの説明のために1レコードずつ合計3レコード取得します。

  • 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
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        const record = event.record;
        record.合計.value = 0;
        // 商品アプリからデータ(レコードID: 1) を取得する
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp1.record.価格.value);
    
          // 商品アプリからデータ(レコードID: 2) を取得する
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2});
    
        }).then((resp2) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp2.record.価格.value);
    
          // 商品アプリからデータ(レコードID: 3) を取得する
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3});
    
        }).then((resp3) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp3.record.価格.value);
    
          // 最後にeventをreturnすることで反映される
          return event;
        });
      });
    })();

Promiseはthen()で処理を待つことができ、then()は繰り返し使えます。
少し難しいかもしれないですが、ここでも大事なのは、Promiseオブジェクトをreturnしているところです。
レコードIDが「1」の部分のみならず、レコードIDが「2」と「3」のところでもPromiseオブジェクトをreturnしているので、後続のthen()内で、レコードID「2」と「3」の取得結果を利用できます。
また、このように複数回Promiseを行う場合は、上から順番に処理がされます。

  • thenを使って連続して使う方法(上記を抜粋)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      // : 中略
      return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2});
        // ↑ここでPromiseオブジェクトをreturnしているので、次の行でthenが使える↓
      }).then((resp2) => {
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3});
        // ↑ここでPromiseオブジェクトをreturnしているので、次の行でthenが使える↓
      }).then((resp3) => {
      // : 中略
    
  • catchを加えてエラー制御
    エラー制御をするには前述のものと同様、最後にcatch()をつけることで可能です。
    レコード「1,2,3」のどの取得でエラーが起きてもこのcatch()を使ってエラー制御をできます。

     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
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        const record = event.record;
        record.合計.value = 0;
        // 商品アプリからデータ(レコードID: 1) を取得する
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp1.record.価格.value);
    
          // 商品アプリからデータ(レコードID: 2) を取得する
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2});
    
        }).then((resp2) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp2.record.価格.value);
    
          // 商品アプリからデータ(レコードID: 3) を取得する
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3});
    
        }).then((resp3) => {
    
          // 取得したデータを合計して見積もりアプリに代入
          record.合計.value += Number(resp3.record.価格.value);
    
          // 最後にeventをreturnすることで反映される
          return event;
    
        }).catch((resp) => {
    
          // エラー表示をする
          event.error = resp.message;
          return event;
    
        });
      });
    })();

コールバック関数との比較

上記のように複数のデータを取得するときに特にPromiseは便利です。
コールバック関数で複数のデータを取得する場合と比較すると差は明らかです。

  • 例)商品アプリ(アプリID: 1)から商品A, B, C(レコードID: 1, 2, 3)の金額を取得して、その合計を表示する(コールバック関数ではSubmit時に処理待ちができないので詳細レコード表示時とします)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    (() => {
      'use strict';
      kintone.events.on('app.record.detail.show', (event) => {
        // 商品アプリからデータ(レコードID: 1) を取得する
        kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}, (resp1) => {
          // 商品アプリからデータ(レコードID: 2) を取得する
          kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2}, (resp2) => {
            // 商品アプリからデータ(レコードID: 3) を取得する
            kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3}, (resp3) => {
              // 取得したデータを合計して表示
              alert(Number(resp1.record.価格.value) + Number(resp2.record.価格.value) + Number(resp3.record.価格.value));
            });
          });
        });
      });
    })();

このように、コールバック関数で書くと、複数APIを呼び出すときに関数が入れ子となってしまい、非常に読みづらくなってきます。
エラー処理をいれるとさらに複雑になります。

  • エラー処理も含めた例

     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
    
    (() => {
      'use strict';
      kintone.events.on('app.record.detail.show', (event) => {
        // 商品アプリからデータ(レコードID: 1) を取得する
        kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}, (resp1) => {
          // 商品アプリからデータ(レコードID: 2) を取得する
          kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2}, (resp2) => {
            // 商品アプリからデータ(レコードID: 3) を取得する
            kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3}, (resp3) => {
              // 取得したデータを合計して表示
              alert(Number(resp1.record.価格.value) + Number(resp2.record.価格.value) + Number(resp3.record.価格.value));
            }, (err3) => {
              // エラーを表示
              alert('error', err3.message);
            });
          }, (err2) => {
            // エラーを表示
            alert('error', err2.message);
          });
        }, (err1) => {
          // エラーを表示
          alert('error', err1.message);
        });
      });
    })();

上記のように複数のデータを取得したときなど複雑な記述にどうしてもなってしまいますので、コールバック形式よりもPromise形式をぜひ使うようにしましょう。

kintone.Promise の使い方

同期的処理をさせたい場合はPromiseをreturnすればよい、と前述しましたが、kintone.api()を使わなくとも明示的にPromiseを返却することもができます。
基本的にはkintone.api()でPromiseオブジェクトをreturnするとよいです。
ただし、処理が複雑な場合などはこちらのほうが見やすいため、覚えておくとよいでしょう。

ぜひこちらの例も確認してください。
kintone.Promise とは

  • kintone.PromiseでPromiseオブジェクト作成

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        // new kintone.Promise()とすることでPromiseオブジェクトを作成
        // 引数 resolve は成功したとき, reject はエラー処理を書くことができる
        return new kintone.Promise((resolve, reject) => {
          kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp) => {
            // 商品アプリから取得したデータを見積もりアプリのフィールドに代入して保存
            event.record.合計.value = Number(resp.record.価格.value) || 0;
            // returnではなく、成功時のハンドラresolveをつかってeventを返却する
            resolve(event);
          });
        });
      });
    })();

動作確認

実際にアプリを用意して、下記コードを実装しました。
レコード保存時に他のアプリと連携できます。

  • アプリの概要

    • 見積もりアプリのレコードを作成時、該当する商品を商品リストからデータ取得する。
    • 今回は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
    
    /*
     * kintone.Promise sample program
     * Copyright (c) 2019 Cybozu
     *
     * Licensed under the MIT License
    */
    (() => {
      'use strict';
    
      // 商品リストのアプリID
      const PRODUCT_APP = 80;
    
      kintone.events.on(['app.record.create.submit', 'app.record.edit.submit'], (event) => {
        const record = event.record;
    
        // レコード取得用の条件を用意
        const params1 = {app: PRODUCT_APP, id: record.商品ID_1.value};
        const params2 = {app: PRODUCT_APP, id: record.商品ID_2.value};
        const params3 = {app: PRODUCT_APP, id: record.商品ID_3.value};
    
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', params1).then((resp1) => {
          // 商品名_1, 価格_1に取得したデータを代入
          record.商品名_1.value = resp1.record.商品名.value;
          record.価格_1.value = Number(resp1.record.価格.value) || 0;
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', params2);
        }).then((resp2) => {
          // 商品名_2, 価格_2に取得したデータを代入
          record.商品名_2.value = resp2.record.商品名.value;
          record.価格_2.value = Number(resp2.record.価格.value) || 0;
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', params3);
        }).then((resp3) => {
          // 商品名_3, 価格_3に取得したデータを代入
          record.商品名_3.value = resp3.record.商品名.value;
          record.価格_3.value = Number(resp3.record.価格.value) || 0;
    
          // データを反映させる
          return event;
        }).catch(() => {
          // エラーが発生した場合はデータ初期化
          record.商品名_1.value = '';
          record.価格_1.value = '';
          record.商品名_2.value = '';
          record.価格_2.value = '';
          record.商品名_3.value = '';
          record.価格_3.value = '';
          alert('商品リストから検索できませんでした。');
          return event;
        });
      });
    })();

Column1: async/await で直感的に同期的処理を書く

Promiseの概念を覚えるのは結構面倒です。最近のJavaScriptではasync/awaitという書き方を使うことでもっと同期的処理をシンプルに書くことができるようになりました。
ただし、Internet Explorer 11など一部ブラウザーで動作しませんので、使うときは注意しましょう。
async/awaitの詳細については async function (External link) を確認してください。

  • 例)レコード保存時のイベントで、商品アプリ(アプリID: 1)から商品A, B, C(レコードID: 1, 2, 3)の金額を取得して、その合計を見積もりアプリ(カスタマイズするアプリ)に登録する。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', async (event) => {
        event.record.合計.value = 0;
    
        // 商品アプリからデータ(レコードID: 1) を取得する
        const resp1 = await kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1});
        // 商品アプリからデータ(レコードID: 2) を取得する
        const resp2 = await kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2});
        // 商品アプリからデータ(レコードID: 3) を取得する
        const resp3 = await kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3});
    
        // 取得したデータを合計して見積もりアプリに代入
        event.record.合計.value = Number(resp1.record.価格.value) + Number(resp2.record.価格.value) + Number(resp3.record.価格.value);
        return event;
      });
    })();

Column2: よくある間違い

Promiseを利用する際、次のように書いてしまうことがあるようです。
よくある間違いのパターンと修正点を紹介します。

Promise を使用しているのにコールバック地獄のような状態に陥るコード
  • 間違った例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        const record = event.record;
        record.合計.value = 0;
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
          record.合計.value = Number(resp1.record.価格.value);
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2}).then((resp2) => {
            record.合計.value += Number(resp2.record.価格.value);
            return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3}).then((resp3) => {
              record.合計.value += Number(resp3.record.価格.value);
              return event;
            });
          });
        }).catch((resp) => {
          event.error = resp.message;
          return event;
        });
      });
    })();

    複数回Promiseを用いる場合、then()の中にまたthen()を書いてしまうことで入れ子が深くなってしまいます。
    動作はしますが、わかりにくいので Promise を利用する(複数回)で説明しているように入れ子にせず書くことができます。
    次の正しい例と見比べてみてください。

  • 正しい例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    (() => {
      'use strict';
      kintone.events.on('app.record.create.submit', (event) => {
        const record = event.record;
        record.合計.value = 0;
        return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
          record.合計.value = Number(resp1.record.価格.value);
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2});
        }).then((resp2) => {
          record.合計.value += Number(resp2.record.価格.value);
          return kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3});
        }).then((resp3) => {
          record.合計.value += Number(resp3.record.価格.value);
          return event;
        }).catch((resp) => {
          event.error = resp.message;
          return event;
        });
      });
    })();
Promise を使用しているのに then メソッドチェーンができていないコード
  • 間違った例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    (() => {
      'use strict';
      kintone.events.on('app.record.detail.show', (event) => {
        let result;
        kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 1}).then((resp1) => {
          result = Number(resp1.record.価格.value);
        });
        kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 2}).then((resp2) => {
          result += Number(resp2.record.価格.value);
        });
        kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {app: 1, id: 3}).then((resp3) => {
          result += Number(resp3.record.価格.value);
        });
      });
    })();

上記も動作自体はしますが、それぞれのAPI呼び出しが並列に処理されてしまいます。
順番に実行したい場合は前述の正しい例のように、then()の中に次のAPIを呼び出す部分を書く必要があります。

デモ環境

デモ環境で実際に動作を確認できます。
https://dev-demo.cybozu.com/k/309/ (External link)

ログイン情報は cybozu developer network デモ環境で確認してください。

information

このTipsは、2019年2月版kintoneで動作を確認しています。