[kintone] 複数のレコード間で項目の値を共有する方法 [コピペOK]

やりたいこと

Kintoneでは、フィールドの値はレコードごとに管理されているため、レコードAとレコードBで同じ値を共有することはできない。関連レコードというフィールドもあるが、あくまで他レコードの情報を表示させるだけで値を共有しているわけではない。

値を共有したい例として以下のアプリをもとに考えてみる。

  • プロジェクトを管理するアプリであり、1つのプロジェクトは四半期ごとに区切られた複数のレコードを持つ。備考欄に記載される情報は年間を通して参照されるべき内容も多く含まれる。そのため、同じプロジェクトであれば、どのレコードから見ても同じ備考を参照できるようにしたい。
  • プロジェクト名は手入力されるため誤った名前のままKintoneに登録される場合がある。
    表記のずれた状態だと、何かと不自由があるため、1回の修正によって、同じプロジェクトの全てのプロジェクト名も修正されるようにしたい。

    実装方法

    1. レコードの詳細画面が表示されたタイミング(app.record.detail.show)で、同じプロジェクトIDを持つ全てのレコードが保持する備考を取得し、表示中のレコードの末尾に動的追加する。
      ⇒ Kintoneの仕様として、詳細表示の状態でフィールドの値を更新しても画面に反映されない。
      ⇒ よって実現不可
    2. アプリ「プロジェクト」とは別にアプリ「プロジェクト-備考リスト」を作成する。レコードが作成、編集されたタイミングで「プロジェクト – 備考リスト」に対しても更新をかける。
      ⇒ 「アプリを新規に起こさなければならない」「実装コストが高すぎる」
      ⇒ よって不採用
    3. レコードが編集されたタイミングで、関連するすべてのレコードも同時に更新を行う。
      ただし、指定したフィールド以外は更新をしないように気を付ける。
      ⇒ 意外と簡単に実装できた。
      ⇒ 以下のソースコードでは、方法3.について実装したものある。

    フィールド定義

    各フィールドは以下のように定義されているとする。

    同じ「PROJECT_ID」を持つ、全てのレコードに対して「PROJECT_NAME」「NOTICE_TABLE」の更新を行う。

     表示名  フィールドコード
     レコード番号  レコード番号
     プロジェクトID  PROJECT_ID
     プロジェクト名  PROJECT_NAME
     担当者ID  MANAGER_ID
     担当者名  MANAGER_NAME
     開始日付  BEGIN_DATE
     終了日付  END_DATE
     備考  NOTICE_TABLE

    ソースコード

    /**
     * 【機能】
     *   関連レコード間で一部のフィールドを共有化する
     * 【処理】
     *   フィールド「CONDITIONAL_FIELD_ID」の値が一致する全てのレコードに対して、
     *   フィールド「SHARE_DATA_FIELD_IDS」に指定した全てのフィールドの値を保存をしたレコードと同じ値で一括更新を行う
     *   関連レコードの一括取得、一括更新は REST APIを利用する
     * 【トリガー】
     *   レコード詳細画面で編集の保存ボタンを押下した時
     * 【カスタマイズが必要な定数】
     *   ・RECORD_ID_FIELD_ID
     *   ・CONDITIONAL_FIELD_ID
     *   ・SHARE_DATA_FIELD_IDS
     */
    (() => {
        'use strict';
    	/**
    	 * Kintone イベント:編集画面で保存が完了した時
    	 */
    	kintone.events.on('app.record.edit.submit.success', async (e) => {
    		await updateRelatedRecord(e);
    	});
    
     /**
      * ★★★ カスタマイズが必要な定数群 ★★★
      */
      // レコード番号のフィールドコード
      const RECORD_ID_FIELD_ID = 'レコード番号';
      // 関連レコード
      const CONDITIONAL_FIELD_ID = 'PROJECT_ID';
      // 共有化対象のフィールドコードのリスト
      const SHARE_DATA_FIELD_IDS = ['PROJECT_NAME', 'NOTICE_TABLE'];
    
    	/**
    	 * 関連レコードの一括更新を行う
    	 * @param {*} e 当該画面におけるレコードの情報
    	 */
    	async function updateRelatedRecord(e) {
    		// レコードを取得する
    		let records = await getRecords(e);
    		if (records.length == 0) {
    			return;
    		}
    		// レコードを加工する
    		records = modifyRecords(records, e);
    		// レコードを更新する
    		await updateRecords(records, e);
    	}
    
    	/**
    	 * REST APIを用いて、レコードの一括取得を行う
    	 * @param {*} e 当該画面におけるレコードの情報
    	 * @returns レコードのリスト
    	 */
    	async function getRecords(e) {
    		// 取得条件
    		let query = RECORD_ID_FIELD_ID + ' != @RECORD_NO@';
    		query += ' and '
    		query += CONDITIONAL_FIELD_ID + '="@CONDITION_VALUE@"';
    		query = query.replace('@CONDITION_VALUE@', e.record[CONDITIONAL_FIELD_ID].value);
    		query = query.replace('@RECORD_NO@', e.recordId);
    
    		// 取得項目:ディープコピー (SHARE_DATA_FIELD_IDSに要素が追加されないようにするため)
    		const fields = JSON.parse(JSON.stringify(SHARE_DATA_FIELD_IDS));
    		fields.push(RECORD_ID_FIELD_ID);
    		const body = {
    			app: e.appId
    			, query: query
    			, fields: fields
    		};
    		const response = await kintone.api(kintone.api.url('/k/v1/records.json', true), 'GET', body);
    		return response.records;
    	} 
    
    	/**
    	 * 更新用にレコードの値を書き換える
    	 * @param {*} records 変更前のレコードのリスト
    	 * @param {*} e 当該画面におけるレコードの情報
    	 * @returns 変更後のレコードのリスト
    	 */
    	function modifyRecords(records, e) {
    		// 共有化するフィールドの数だけループ
    		SHARE_DATA_FIELD_IDS.forEach((field_id) => {
    			let new_value = e.record[field_id].value;
    			// 共有対象となる他レコードの数だけループ
    			records.forEach((obj) => {
    				obj[field_id].value = new_value;
    			});
    		});
    		return records;
    	}
    
    	/**
    	 * REST APIを用いて、レコードの一括更新を行う
    	 * @param {*} records レコードのリスト
    	 * @param {*} e 当該画面におけるレコードの情報
    	 * @returns -
    	 */
    	async function updateRecords(records, e) {
    		// レコードをリクエスト用に加工する
    		records = records.map((obj) => {
    			const id = obj[RECORD_ID_FIELD_ID].value;
    			delete obj[RECORD_ID_FIELD_ID];
    			const record = obj;
    			return {
    				id : id
    				, record : obj
    			};
    		});
    		const body = {
    			app: e.appId
    			, records: records
    		};
    		await kintone.api(kintone.api.url('/k/v1/records.json', true), 'PUT', body);
    	}
    })();
    

    解説

    /**
     * Kintone イベント:編集画面で保存が完了した時
     */
    kintone.events.on('app.record.edit.submit.success', async (e) => { 
        await updateRelatedRecord(e); 
    });
    

    Kintoneには保存ボタンを押す直前「app.record.create.submit」と保存ボタンを押した直後「app.record.edit.submit.success」でイベントが用意されている。

    直前の場合、エラーが発生したとき(必須項目が未入力など)に、イベントを走らせないようにするというロジックを挟む必要があるため、直後のイベントを採用している。

     

    上記ソースコードをJavascriptプラグインとして導入するとき、編集すべき箇所は下記3点だけである。

     /**
      * ★★★ カスタマイズが必要な定数群 ★★★
      */
      // レコード番号のフィールドコード
      const RECORD_ID_FIELD_ID = 'レコード番号';
      // 関連レコード
      const CONDITIONAL_FIELD_ID = 'PROJECT_ID';
      // 共有化対象のフィールドコードのリスト
      const SHARE_DATA_FIELD_IDS = ['PROJECT_NAME', 'NOTICE_TABLE'];
    
    • RECORD_ID_FIELD_ID:更新対象のレコードを特定するために利用する。
    • CONDITIONAL_FIELD_ID:どのフィールドでグルーピングを行うかを指定する。一致条件が不等号だったり、一致させるフィールドを複数用意することはできない。
    • SHARE_DATA_FIELD_IDS:同時更新の対象としたい列を指定する。
    // 取得項目:ディープコピー (SHARE_DATA_FIELD_IDSに要素が追加されないようにするため)
    const fields = JSON.parse(JSON.stringify(SHARE_DATA_FIELD_IDS)); 
    fields.push(RECORD_ID_FIELD_ID);
    • 配列を「=」で代入した場合、シャローコピーになる。シャローコピーのままpush関数を呼ぶと、代入元の「SHARE_DATA_FIELD_IDS」にも要素が1つ追加される。
      そうするとmodifyRecord()において全ての関連レコードでレコードIDも更新してしまい、結果一つのレコードを何度も更新してしまう。
      それを回避するためにディープコピーを行っている。
     	/**
    	 * 更新用にレコードの値を書き換える
    	 * @param {*} records 変更前のレコードのリスト
    	 * @param {*} e 当該画面におけるレコードの情報
    	 * @returns 変更後のレコードのリスト
    	 */
    	function modifyRecords(records, e) {
    		// 共有化するフィールドの数だけループ
    		SHARE_DATA_FIELD_IDS.forEach((field_id) => {
    			let new_value = e.record[field_id].value;
    			// 共有対象となる他レコードの数だけループ
    			records.forEach((obj) => {
    				obj[field_id].value = new_value;
    			});
    		});
    		return records;
    	}
    

    コードとしては何も難しいことをしていないが、個人的に気になったことが2点ある。

    1. 更新対象のフィールドがグループフィールドに含まれていても動作するのだろうか
      ⇒ 問題なく動作する。グループフィールド上にあるフィールドにアクセスをするためにはobj[group_field_id][field_id].value となりそうな気もするが、ほかの        フィールドと同様にobj[field_id].valueで参照できる。
    2. 更新対象のフィールドがテーブルフィールドでも動作するのだろうか
      ⇒ 問題なく動作する。Kintoneではアプリやレコードと同様、テーブルの各行にも一意のIDが割り振られている。
      しかし、その行IDを一括更新時の「body」に含まなければ、既存の行はすべて削除されるという仕様である。
      テーブルにおいて、どの行が削除され、どの行が追加されたかなどの差分を判定するのはとても大変であるので、いったんすべての行を削除し
      参照元レコードから全ての行を追加するということをしている。
      行追加といっても、通常のテキストフィールドや日付フィールドと同様に「value」の代入だけで実現できてしまうので特別な処理は必要ない。

    Kintone : レコード更新におけるテーブル操作のテクニック

    更新する行の ID を指定することで、その行の特定のフィールドのみを更新できます。
    同じ行の他のフィールドを更新しない場合にはそのフィールドを省くことができます。
    既存の行の ID を省略した場合、対象行は削除されます。