スクリプトを非同期に実行することができます。

メッセージングミドルウェア Rabbit MQ が起動している必要があります。Rabbit MQ は Wagby に同梱されていません。別途インストールを行ってください。

Wagby の ScriptCodeRunner ジョブを非同期に実行することができます。

アプリケーションを用意する

詳細画面のオリジナルボタンからジョブの実行を非同期で行うサンプルを紹介します。

はじめに"画面遷移 > 独自ボタンから外部コマンドを呼び出す"の手順でオリジナルボタンの設定を行ってください。

なおサンプルコードではモデル名を model1 としますので、開発者は ShowModel1_Original1.js というスクリプトを用意します。

上の説明ページに記載のとおり、次のスクリプトを用意することで myjob.js の内容を非同期に実行することができます。

function process() {
  // メッセージキューにジョブを登録する
  var registryService = p.appctx.getBean("JfcjobstatusRegistryService");
  var HashMapClass = Java.type("java.util.HashMap");
  var jobparamMap = new HashMapClass();
  jobparamMap.put("filename", "myjob.js");
  registryService.sendJobMessage(null, null, "ScriptCodeRunner", jobparamMap, p);

  // 元の詳細画面へ戻す (model1の詳細画面を想定)
  var pkey = p.request.getParameter("pkey");
  return "redirect:/showModel1.do?pkey="+pkey;
}
  • JfcjobstatusRegistryServiceクラスが提供するsendJobMessageメソッドでジョブメッセージの送信を行います。
  • ジョブメッセージ送信時、内部ではWagbyが提供するジョブ状態管理テーブル jfcjobstatus へ状態が設定されるようになっています。開発者は直接、この管理テーブルを操作することはありません。
  • sendJobMessageメソッドの第1引数と第2引数はそれぞれ RabbitMQ の "exchange" と "routingkey" に対応します。exchange を null としたとき、routing key はキュー名に対応します。exchange, routingkey ともに null を指定すると、デフォルトのキュー名である wagbyJobQueueが使われます。上のサンプルコードはそのようになっています。
  • デフォルトのキュー名以外を用いる場合は、"環境 > アプリケーション" で定義した "使用するキューの名前" "ジョブを受信するキューの名前" を指定してください。
  • 第3引数はジョブ名となります。ScriptCodeRunnerはスクリプトを実行するジョブです。
  • 第4引数はジョブパラメータとなります。ジョブパラメータ filename に、実行するスクリプトファイル名を指定することができます。
  • 第5引数にはActionParameterを指定します。サンプルコードのように p を指定してください。

メッセージキューの設定

"環境 > アプリケーション > メッセージキュー" の設定を行います。"ダウンロード" の説明を参考に行ってください。

キュー名の未指定時(空白設定)は、標準ではダウンロード用と同じ "wagbyJobQueue" という名前のキューを利用します。

参照連動自モデル保存は、参照先モデルの値をコピーして自モデルに保存する仕組みです。そのため、運用中に参照先モデルの値が変わった場合でも、自モデルにコピーされた値は(コピーした時点の値が)維持されます。

例として顧客モデルとサポートモデルを用意します。[詳細...]
ここで顧客モデルの会社名を変更したとき、自分(顧客モデル)を参照連動しているモデル(ここではサポートモデル)を検索し、自モデル保存の値を書き換えるというスクリプトを紹介します。

非同期に参照先モデルの値に更新する方法により、システムに負荷をかけずに(ゆっくりと)データの整合性を維持することができるようになります。

サンプルの解説

非同期ジョブの自動登録

customerモデルの "画面 > スクリプト > コントローラ > 画面:更新 > 実行タイミング:更新の実行" に次のスクリプトを指定します。内部では myjob2.js を実行するようにしています。更新処理本体は myjob2.js が行います。またジョブパラメータで自分自身の主キーを渡しています。

function process() {
  var registryService = p.appctx.getBean("JfcjobstatusRegistryService");
  var HashMapClass = Java.type("java.util.HashMap");
  var jobparamMap = new HashMapClass();
  jobparamMap.put("filename", "myjob2.js");
  jobparamMap.put("param:customerid", ExcelFunction.TEXT(customer.customerid,"0"));
  registryService.sendJobMessage(null, null, "ScriptCodeRunner", jobparamMap, p);
}
  • 実行のタイミングはヘルパではなくコントローラとします。この画面 (customerの更新処理) はこのまま終わらせますが、このタイミングでジョブを登録します。そのあと非同期で再び自分 (customer) の更新を行うことになります。これをヘルパのタイミングで行ってしまうと、非同期ジョブがすぐに実行されてしまったとき、現在実行中の customer の更新ロックと競合し、ロックエラーとなる可能性があるため、これを避けています。
  • コントローラで実行されるスクリプトであるため、画面遷移処理の指定は不要です。Wagbyの標準動作にのっとり、更新後、自動的に詳細画面に遷移します。

更新を行うスクリプト

実際の更新処理を行うスクリプト myjob2.js を説明します。(ファイル名は変更できます。作成したファイルは customize/webapp/WEB-INF/script/__job フォルダに配置してください。)

function process() {
  /* 1件の customer データを取得 */
  var customerEntityService = p.appctx.getBean("CustomerEntityService");
  var customer = 
    customerEntityService.findById(ExcelFunction.TOINT(customerid));

  var criteriaConverter = p.appctx.getBean("SupportCriteriaConverter");
  var criteria = criteriaConverter.defaultCriteria();

  var SupportMeta = Java.type("jp.jasminesoft.wagby.model.support.SupportMeta");
  var supportMeta = new SupportMeta();
  criteria.eq(supportMeta.customername, customer.customerid);
  criteria.ne(supportMeta.companyname, customer.companyname);/*会社名が異なるデータのみ*/

  var LockUtils = Java.type("jp.jasminesoft.jfc.core.util.LockUtils");
  var supportDaoHelper = p.appctx.getBean("SupportDaoHelper");

  var supportEntityService = p.appctx.getBean("SupportEntityService");
  var list = supportEntityService.find(criteria);
  if (list != null && list.size() > 0) {
     // list から 1 つずつオブジェクトを取り出す
     for (var i=0; i<list.size(); i++) {
       var support = list.get(i);
       try {
         LockUtils.lock(support, supportDaoHelper);
         support.companyname = customer.companyname;
         supportEntityService.update(support); /* 更新 */
       } catch (e if e instanceof PessimisticLockException) {
         print("lock error " + e);
       } catch (e) {
         print("error " + e);
       } finally {
         LockUtils.release(support, supportDaoHelper);
       }
     }
  }
}
  • ジョブパラメータに指定した主キーで customer のデータを取得します。ジョブパラメータで渡した param: に続くキー名を、そのままオブジェクトとして利用できます。
  • customer モデルの更新は行わないため、findById メソッドの第二引数は指定しない(この場合は false になる)とします。つまり更新のためのロックは取得しません。
  • customerモデルのcustomerid項目の値を使ってsupportの検索を行います。
  • 複数の検索結果が戻されることを前提に、1件ずつロックして顧客名を更新していきます。

ロック失敗への対応

上のスクリプト myjob2.js で、更新対象の support のロック取得に失敗したときは、この support モデルの更新は行われません。つまり確実にすべての support モデルを更新するという保証はありません。
これは非同期処理で support モデルの更新を行っている最中に(別の利用者が)support モデルの更新を行っていれば、それを優先したためです。

そうではなく、書き換えが必須であるという要件で、ロック取得が成功するまでリトライさせるようにしたコード例を示します。 ロックエラーが発生した場合は5秒スリープし、ロックエラーが発生したデータを再度更新するようにしています。

function process() {
  /* 1件の customer データを取得 */
  var customerEntityService = p.appctx.getBean("CustomerEntityService");
  var customer = 
    customerEntityService.findById(ExcelFunction.TOINT(customerid));
  
  var criteriaConverter = p.appctx.getBean("SupportCriteriaConverter");
  var criteria = criteriaConverter.defaultCriteria();

  var SupportMeta = Java.type("jp.jasminesoft.wagby.model.support.SupportMeta");
  var supportMeta = new SupportMeta();
  criteria.eq(supportMeta.customername, customer.customerid);
  criteria.ne(supportMeta.companyname, customer.companyname);/*会社名が異なるデータのみ*/

  var ArrayList = Java.type("java.util.ArrayList");
  var Thread = Java.type("java.lang.Thread");
  var supportEntityService = p.appctx.getBean("SupportEntityService");
  var list = supportEntityService.find(criteria);
  if (list != null && list.size() > 0) {
    while (true) {
      var lockfailed = new ArrayList();
      updateSupports(supportEntityService, customer, list, lockfailed);
      if (lockfailed.size() == 0) {
        break;// ロック失敗がなくなれば終了する。
      }
      print("lock failed, continue " + lockfailed.size());
      Thread.sleep(5000); // 5000 ms 待つ
      list = lockfailed;// 失敗したオブジェクトを指す
    }
  }
}

function updateSupports(supportEntityService, customer, list, lockfailed) {
  var LockUtils = Java.type("jp.jasminesoft.jfc.core.util.LockUtils");
  var PessimisticLockException = Java.type("javax.persistence.PessimisticLockException");
  var supportDaoHelper = p.appctx.getBean("SupportDaoHelper");

  // list から 1 つずつオブジェクトを取り出す
  for (var i=0; i<list.size(); i++) {
    var support = list.get(i);
    try {
      LockUtils.lock(support, supportDaoHelper);
      support.companyname = customer.companyname;
      supportEntityService.update(support); /* 更新 */
    } catch (e if e instanceof PessimisticLockException) {
      print("lock error " + e);
      lockfailed.add(support);
    } catch (e) {
      print("error " + e);
    } finally {
      LockUtils.release(support, supportDaoHelper);
    }
  }
}
Wagby Developer Day 2018