自分のカスタマイズが「編集中にほかのユーザーがレコードを更新しました。」というエラーを引き起こしている可能性がある

背景・実現したいこと

 レコード保存時に「レコードを再読み込みしてください。編集中に、ほかのユーザーがレコードを更新しました。」というエラーが頻繁に発生しているとスタッフから連絡がありました。明らかにそのレコードを他のユーザーがいじっていないと思われる場合にもこのエラーが表示されることがあるとのことです。もしかしたら自分のJSカスタマイズがこのエラーを引き起こしている可能性があるのではと疑っています。僕が行ったカスタマイズはボタンを設置してユーザー選択フィールドに自分を簡単に追加できるというプログラムです。ただ何がこのエラーを引き起こしているのかがわからずに困惑しています。もし何か原因や解決策がわかる方は是非ご教授お願いします。

エラー情報 (開発者ツールのコンソール)

 

利用したソースコード

(function() {

    "use strict";

  /*
      「ユーザー選択」フィールドの横にログインユーザーを追加するためのボタンを設置する。
      このボタンを押せばログインユーザーがフィールドに登録されるので、何回も自分の名前をフィールドに打ち込む手間が省ける。
      事前にアプリのフォームにおいて「ユーザー選択」フィールドとその横にボタン用の「スペース」フィールドを用意しておく。
    */

  var userSelectionFieldCode = '返信対応者'; // 「ユーザー選択」フィールドのフィールドコード
    var addMeButtonSpaceFieldCode = 'addMeButtonSpace1'; // ボタンを設置するための「スペース」フィールドのフィールドコード

  // レコードの追加、編集で適用する
  var events = [
      'app.record.create.show',
      'app.record.create.change.' + userSelectionFieldCode,
      'app.record.edit.show',
      'app.record.edit.change.' + userSelectionFieldCode
    ];

  // イベントの取得
  kintone.events.on(events, function(event) {
      // console.log(event);
      // ボタンを作成
      var addMeButton = document.createElement('button');
      addMeButton.id = 'addMeButton';
      addMeButton.innerText = '+自分'; // ボタンに表示される文字を変更する場合はここを変える
      // ボタンのスタイル
      addMeButton.style.color = "white";
      addMeButton.style.backgroundColor = "orange";
      addMeButton.style.border = "none";
        addMeButton.style.borderRadius = "100vh";

      // ボタンをクリックした時の動作
      addMeButton.onclick = function () {
          // ログインユーザーの情報を取得
          var loginuser = kintone.getLoginUser();
          // 現在表示されているレコードの情報を取得(ボタンの中ではkintone.app.record.get()を使用しないといけない)
          var obj = kintone.app.record.get();
          var userSelectionValue = obj.record[userSelectionFieldCode].value; // これは辞書{code: '', name: ''}を要素とする配列
          var alreadyAdded = false; // 既に追加されているか否かを判定するためのフラグ
          for (var i = 0; i < userSelectionValue.length; ++i) {
              if (userSelectionValue[i]['code'] === loginuser['code']) {
                  alreadyAdded = true;
                  break;
              }
          }

          // まだ対象のユーザーが追加されていない場合のみ、そのユーザーを追加する
          if (!alreadyAdded) {
              obj.record[userSelectionFieldCode].value.push({code: loginuser['code'], name: loginuser['name']});
              // レコードの情報を書き換える(ボタンの中ではkintone.app.record.set()を使用しないといけない)
              kintone.app.record.set(obj);
          }
        }

      // ボタンをHTMLに追加
      var addMeButtonSpace = kintone.app.record.getSpaceElement(addMeButtonSpaceFieldCode);
      addMeButtonSpace.style.position = 'absolute'; // スペースとして挿入されているdivタグを親要素の左下に配置する
      addMeButtonSpace.style.bottom = '0%';
      addMeButtonSpace.style.left = '0%';
      addMeButtonSpace.innerHTML = ''; // 一旦空にしてからボタンを追加する
      addMeButtonSpace.appendChild(addMeButton);

      return event;
  });
})();

 

kz様

kinotneでは編集中以外にも「表示時」「保存時」などいくつかのタイミングがあるので,
どのタイミングでエラーが発生しているのかがわかると,原因の特定につながるかもしれません.

コードを拝見しましたが,ログインユーザーを追加するボタンを設置する,という内容で,
イベントハンドラにchangeイベントがありますが,
このコードではchangeイベントでボタンを設置する必要は無いように思います.
これが原因かはわかりませんが,試しに2つのchangeイベントをコメントアウトしてみるのもありかもしれません.

 

そのエラーは、ユーザーAがレコードの編集開始をして保存する前に誰か(ユーザーAを含む)がレコードの中身を書き換えてしまった場合に起こります。

コードを拝見する限り、コードは関係ないと思われます。他に考えられる原因として、
①他のJavaScriptカスタマイズでそのアプリのレコードを更新(PUT)している
②edit.submit.successのイベントでカスタマイズJavaScirptのエラーが発生した場合、レコードは保存されるもののその後の画面推移がされず、もう一度保存しようとしてこのエラーが発生する
③プラグインに問題がある(「吹き出しプラグイン」というもので同様のことがあった例が過去にありました。特にフィールドのフォーム設定を変えた後にプラグインの設定画面を開かないでいると、プラグインの設定は古いフォーム設定のままになるので問題が起こりやすいです)
④レコードの編集中に別のユーザーが編集して保存している
⑤ユーザーAがレコードの詳細画面を開いたまましばらく放置し、その間にユーザーBがそのレコードを編集して保存→ユーザーAが「開いたまま放置していたため、ユーザーBが編集する前の古いレコード」から編集開始して保存する際にエラー
当社で一番多かったのがこのパターンでした。対策として導入したコードを添付しておきます。

(() => {
  'use strict';

  kintone.events.on('app.record.edit.show', (event) => {
  let appId = event.appId, recordId = event.recordId;
  let getRecord = {
    app: appId,
    id: recordId
  };

    setTimeout(() => {
    kintone.api(kintone.api.url('/k/v1/record', true), 'GET', getRecord, (resp) => {
      if (resp.record['$revision'].value === record['$revision'].value) return;
      if (confirm('現在編集しようとしているレコードは最新ではありませんページを再読み込みします')) location.reload();
    }, (error) => {
      console.error(error);
    });
  }, 1000);

    return event;
});
})();

TOさん、ご回答ありがとうございます。確かにchangeイベントは必要ありませんでした。

mls-hashimotoさん、ご回答ありがとうございました。原因の選択肢まで列挙して頂き感謝致します。

➀、➁についてですが、他のJSプログラムはkmailerのもののみでkmailerの中身までは把握していないので➀が原因かはわからない状態です。

➂についてですが、プラグインはTiSさんなどの代表的なプラグインを数個使用していますが…

➃、➄についてですがスタッフが言うには、自分しか編集していないはずのレコードでもこのエラーが発生し、かつレコードを出しっぱなしにしないで素早く保存しようとしても頻繁にこのエラーが発生するとのことでした。

とりあえず、kintone.app.record.setを使用しないで同じようなことができるプログラムに書き直して様子を見たいと思います。ご協力ありがとうございました。

 

mls-hashimotoさん

プログラムを書き直してみましたが、同じエラーが以前として発生しました。やはりmls-hashimotoさんが仰られたように僕が書いたプログラムには特別問題はなかったようです。

この件に関して1つ気になったことがあります。エラーメッセージには「ほかのユーザーがレコードを更新しました」とありますが、レコードの更新履歴を見ても誰もどこもいじっていないのでおかしいなと思っていましたが、 kintoneは編集画面を開いて何も変更せずに保存ボタンを押した場合もレコードを更新したことになってしまう ようですね。レコードの更新履歴には何も残らないものの、「更新者」フィールドと「更新日時」フィールドはきちんと変更されていました。そのためあるユーザーが誰か他の人が開いているレコードを変更せずとも編集画面に入って保存ボタンを押してしまえば、上のエラーメッセージが出てしまうようです。

願わくば編集画面に入っても何も変更を加えない場合は保存ボタンを押せないようにしてキャンセルボタンで編集画面を閉じるようにユーザーに促したいです。そのような実装をお考えになったことはございますでしょうか?

mls-hashimotoさん

何回もメッセージを書き込んでしまい申し訳ございません。

例えばapp.record.edit.showの時にevent.recordの値を保存しておいて、app.record.edit.submitの時にそのevent.recordと比較をするというのはどうでしょうか?下にプログラムを書いてみました。あまりkintoneのカスタマイズ自体慣れていないのですが、下のプログラムでどのような問題があるかあまり理解できていません。一応テスト用に作った小さなアプリでこのコードを試したところうまくいっているようでしたが…。

(function() {
  'use strict';

  let data_before; // 編集画面を開いた時のレコードの値

    kintone.events.on('app.record.edit.show', function(event) {
      data_before = Object.assign({}, event.record); // オブジェクトをコピー
        return event;
    });

    kintone.events.on('app.record.edit.submit', function(event) {
      if (_.isEqual(data_before, event.record)) { // lodashというライブラリでオブジェクトの値を比較
            event.error = "何も変更していない場合は保存ボタンではなくキャンセルボタンを押してください";
      }
        return event;
    });
})();

kz さま

遅くなりました。申し訳ありません。

良い方法だと思いますが、提示されたコードでは問題が起きる可能性があるかと思います。例えば空白の文字列フィールドがあった場合、edit.showではvalueが「“”(空文字)」、edit.submitでは「undefined(未定義値)」となるため、これらを比較すると別の値となり、「どこかに空白のフィールドがあれば、何も変更してなくても変更した扱い」になるかと思います(create.showではundefinedなんですが、edit.showだけ…)。
また、今後のカスタマイズでも影響が出る可能性もあります(例として、当方では編集画面中は数値フィールドをカンマ区切りにし、保存時にカンマを抜く処理等と競合してしまいました)。

方法として、

①フィールドでループを回して、変更しているかどうかを一つずつ確認

以下のような形になるかと思います。ただし、以下のコードでは添付ファイルのみ、添付ファイルの如何に関わらずedit.showでもedit.submitでも空配列になるため、必ずtrue(変更していない)が返ります。ないと思いますが、添付ファイルのみの変更、または添付ファイルフィールドのみで構成されたアプリだと動作しません。また、こちらも同様にedit.showやedit.submitイベントで値を変える処理を行っている場合は競合します。その場合はそのフィールドのみ例外処理をする必要があります。

(() => {
  'use strict';

  let data_before;

  kintone.events.on('app.record.edit.show', (event) => {
  data_before = event.record;

    return event;
});

  kintone.events.on('app.record.edit.submit', (event) => {
  let record = event.record;
  let isEqual = Object.keys(record).every((fieldCode) => {
if (record[fieldCode].type === 'SUBTABLE') {
      if (data_before[fieldCode].value.length !== record[fieldCode].value.length) return false;

        let tableColumns = Object.keys(record[fieldCode].value[0].value).map((column) => column);

        return record[fieldCode].value.every((row, index) => {
        return tableColumns.every((tableFieldCode) => {
          if (!data_before[fieldCode].value[index].value[tableFieldCode].value && !row.value[tableFieldCode].value) {
            return true;
          } else {
            return _.isEqual(data_before[fieldCode].value[index].value[tableFieldCode].value, row.value[tableFieldCode].value);
          }
        });
      });
    } else {
      if (!data_before[fieldCode].value && !record[fieldCode].value) {
        return true;
      } else {
        return _.isEqual(data_before[fieldCode].value, record[fieldCode].value);
      }
    }
  });

    if (isEqual) event.error = '何も変更していない場合は保存ボタンではなくキャンセルボタンを押してください';

    return event;
});
})();

 

②編集開始時にアラート

「保存させない」という部分からは外れますが、私が先に投稿したコードでもある程度問題は解消するかと思います。更新日時や更新者と同様に「リビジョン(レコードのバージョンで、変更履歴の先頭にある番号)」というフィールドがあり、こちらも保存する度に更新されています。私が投稿したコードは「編集開始時に現在編集しているレコードのリビジョンと、レコードの最も新しいリビジョンが違う場合にページの更新を確認」という動作をするため、誰かが何も更新せず保存した場合でも更新の確認がされます。

追記:ユーザーAが編集中にユーザーBがレコードを何もせず保存→ユーザーAが保存する際にエラー、といった手順なので②は的外れでしたね。他に私が取っている方法として「レコードの編集状態を記録するアプリ」を作り、

・誰かが編集開始した時にそのアプリへレコード登録(POST)または既にレコードがあれば「○○さんが編集中」と表示
・編集のキャンセル(キャンセル後に戻るdetail.show)または保存成功時(submit.success)にそのアプリのレコードを削除

といった方法を取っています(スマートフォンのタスクキルで編集を中断するユーザーが多いのであまり上手くいっていません…)。

mls-hashimoto 様

ご返信ありがとうございます。またプログラムまでご教授頂き感謝します。

➀のプログラム大変参考になります。僕のプログラムでうまくいっているというのは勘違いだったようですね。1つ1つフィールドを見ないと駄目だとは思いませんでした。

mls-hashimoto 様から教えて頂いた「➁の編集開始時にアラートを出す」というのが現実的な対応だろうという気がしてきました。このプログラムを導入してみて少し様子を見たいと思います。

詳細なご回答ありがとうございました。