Service/Dao、ヘルパ、SQLの利用

最終更新日: 2021年12月15日
R8 | R9

Serviceを利用する

Wagbyではモデルに対応したServiceクラスとDaoクラスを自動生成します。これは CRUD 処理 (*) に関するメソッドを提供します。

なお、Serviceは内部でDaoを利用しています。開発者はいずれを使うこともできますが、おおむね次の指針で使い分けを行なってください。

  • トランザクションスクリプトで利用する場合は Dao を使う。
  • それ以外のスクリプトで利用する場合は Service を使う。
データベースへの登録(Create)、読み込み(Read)、更新(Update)、削除(Delete)といった基本操作の総称です。
図1 ServiceクラスとDaoクラス

ここでは Service の使い方を説明します。開発者はスクリプト内で、次のようにしてServiceオブジェクトを取得することができます。(p.appctxはSpringが提供するDIコンテナです。)

var entityService = p.appctx.getBean(Service名);

Service名は "<モデルID>EntityService" となります。モデルIDはキャメル記法で表現します。例えば customer モデルに対するService名は "CustomerEntityService" になります。

データの取得と更新

具体的なコード例を紹介します。次のコードはデータを1件取得します。

var entityService = p.appctx.getBean("JuserEntityService");
var user = entityService.findById("user01"); /* 1件のデータを取得 */
print(user); /* デバッグ用ログ出力 */

続いて、取得したデータを更新する例を紹介します。

var entityService = p.appctx.getBean("JuserEntityService");
var user = entityService.findById("user01", true); /* 1件のデータを取得 */
print(user); /* デバッグ用ログ出力 */
user.name = "ジャスミン太郎";
entityService.update(user); /* 更新 */
  • findByIdメソッドの第二引数はロック処理を行うことを示しています。このコードは取得したデータを更新することを前提としているため、findById メソッド実行のタイミングでロックを取得しています。
  • Serviceクラスはトランザクション境界となっているため、updateメソッド呼び出しのタイミングでデータベースにコミットされます。update メソッドの実行によって取得したロックは解放されます。

注意

ロックについて:上記コードでは、findById で取得したロックは、その後の update メソッドの実行によって必ず解放されるため問題ありません。しかしロックを取得したが、update メソッドは実行しない場合、このスクリプトが終了してもロックは保持されたままとなります。そのため更新予定のないデータを findById メソッドで取得する場合、通常は第二引数を省略してください。この詳細と解決方法は後述する"ロックの取得と解放"をお読みください。

新規登録

Customer モデルの新規登録を行うスクリプトを紹介します。new 演算子で生成したストアモデルのオブジェクトをデータベースに登録することができます。

var customerClass = Java.type("jp.jasminesoft.wagby.model.customer.Customer");
// 空の状態の Customer オブジェクトを作成
var customer = new customerClass();
// 上記2行のコードを1行で記述することもできる。
//var customer = new Packages.jp.jasminesoft.wagby.model.customer.Customer();
 
var helper = p.appctx.getBean("CustomerHelper");
// helper#initialize() を利用することでリポジトリで定義した
// 初期値をセットする。
// 参考: https://wagby.com/wdn8/operation-script-init.html#initalize
helper.initialize(customer, p);
customer.customername = "ジャスミン太郎";
// EntityService を取得
var service = p.appctx.getBean("CustomerEntityService");
// customer データを登録
service.insert(customer);

例外 PessimisticLockException

サービス (EntityService) のメソッド呼び出し時に例外 PessimisticLockException が生じることがあります。例えば findById メソッドでデータを取得しようとしてこの例外が生じた場合、このデータは別の利用者がロックを取得していることを意味します。

悲観ロックを適用している場合です。

開発者は次の点に注意してスクリプトを作成するとよいでしょう。

無用なロックの取得を行わない。

findById メソッド呼び出し時に第二引数にtrueを指定するとロックを取得しようとします。しかし単に値を参照するだけの目的であればロックは不要です。(ロック取得は、その後にデータの更新を行うために行います。)その場合は第二引数にfalseを指定するとよいでしょう。

try-catch 節で処理を囲い、例外を補足する。

サンプルコードを示します。ロック取得失敗時のコードを catch 節に追加することができます。ここではコンソールに出力するのみとしています。

var PessimisticLockException = Java.type("javax.persistence.PessimisticLockException");
...
try {
  var entityService = p.appctx.getBean("JuserEntityService");
  var user = entityService.findById("user01"); /* 1件のデータを取得 */
  print(user);
} catch (e) {
  if (e instanceof PessimisticLockException) { /* ロック取得失敗 */
    print("lock error " + e);
  } else { /* その他の例外 */
    print("error " + e);
  }
  throw e;
} finally {
}

Daoを利用する

トランザクションスクリプトは通常、モデル定義で関連しているモデル間で値の変更に関するコードを記述します。しかし Dao クラスを使うことで、任意のモデルを操作することができます。

コード例

トランザクションスクリプトでは、次のコードを用いてDaoを取得することができます。

var dao = p.appctx.getBean(Dao名);

Dao名は "<モデルID>Dao" となります。モデルIDはキャメル記法で表現します。例えば customer モデルに対するDao名は "CustomerDao" になります。

具体的なコード例を紹介します。

var dao = p.appctx.getBean("JuserDao");
var user = dao.get("admin", true); /* 1件のデータを取得 */
print(user); /* デバッグ用ログ出力 */
user.name = "ジャスミン太郎";
dao.update(user); /* 更新 */

dao.get の第二引数はロックを取得することを示します。このあとの dao.update の実行によりロックは解放されます。

ワンポイント

Dao クラスはトランザクションが開始されている場合に利用することができます。(Dao 自身はトランザクション境界ではありません。つまり自分自身でトランザクションを開始しません。)一方、上で説明した Service クラスはトランザクション境界になります。この違いから、Daoを使える場所はトランザクションスクリプトに限られます。トランザクションスクリプトは呼び出しの前後でトランザクションの開始と終了が自動的に実行されるため、Daoによる更新処理は正しく処理されます。

トランザクションスクリプトではServiceとDaoのいずれも利用できますが、トランザクションが開始されていることがわかっているため、通常はDaoを使います。トランザクションスクリプト以外のスクリプトでは、Serviceを用いてください。(Dao はトランザクション管理機能を持ちません。Dao利用時にトランザクションが開始されていなければエラーが発生します。[詳細...])

ヘルパを利用する

スクリプトで (Wagbyが自動生成した) ヘルパクラスを使うことができます。設計情報に記述した初期値や式を再利用できるだけでなく、s2p や p2s メソッドを用いたストアモデルとプレゼンテーションモデルの変換も再利用できます。

例を示します。quotation というストアモデルをプレゼンテーションモデルに変換し、それを po という変数に格納します。

var IPresentationHelper = Java.type("jp.jasminesoft.jfc.IPresentationHelper");
var helperClass = p.appctx.getBean("QuotationPHelper");
var po = helperClass.s2p(quotation, p, IPresentationHelper.SHOW);
print(po);
  • ヘルパクラスも DI コンテナである p.appctx の getBean メソッド経由で取得します。
  • jp.jasminesoft.jfc.IPresentationHelperインタフェースは定数 SHOW, UPDATE, CSVDOWNLOAD, CREATEOBJECT という4つのモードを提供します。通常は SHOW または UPDATE を使います。s2p メソッドに SHOW を渡すと表示用のプレゼンテーションモデルへ変換します。UPDATE を渡すと更新画面で利用できるプレゼンテーションモデルへ変換します。これには選択肢の候補が含まれています。[詳細...]

ロックの取得と解放

悲観ロック利用時の問題点

次のコードは条件によって更新処理を行うサンプルです。このコードはロックの処理に問題があります。(なお、ここでは悲観ロックを利用した場合についての説明となっています。

var entityService = p.appctx.getBean("JuserEntityService");
var user = entityService.findById("user01", true);
if (user.kind == 1) {
  user.name = "ジャスミン太郎";
  entityService.update(user);
}

具体的には条件に合致しない場合は update メソッドが呼び出されません。このとき、findById の第二引数に true を指定したためロックを取得していますが、このロックが解放されずにスクリプトが終わる可能性があります。

ロック解放が行われない場合、メニュー表示時またはログオフ時までは、このロックは保持されたままとなります。その間、他の利用者はこのデータを更新することができません。

Wagbyのロックは、メニュー画面表示またはログオフ時に、自動解放されます。

解決方法

findById メソッドでロックを取得するのではなく、LockUtils クラスが提供する update メソッドの直前でロックを取得します。

var LockUtils = Java.type("jp.jasminesoft.jfc.core.util.LockUtils");
var userDaoHelper = p.appctx.getBean("JuserDaoHelper");
var entityService = p.appctx.getBean("JuserEntityService");
var user = entityService.findById("user01");
if (user.kind == 1) {
  try {
    LockUtils.lock(user, userDaoHelper);/*ロック取得*/
    user.name = "ジャスミン太郎";
    entityService.update(user);
  } catch (e) {
    throw e;/*ロールバックのため*/
  } finally {
    LockUtils.release(user, userDaoHelper);/*ロック解放*/
  }
}
  • LockUtils クラスは Wagby が提供します。lock と release というメソッドを用意しています。
  • 第一引数には対象オブジェクトを指定します。第二引数には、対象オブジェクトのDaoHelperクラスを指定します。
  • catch 節では、この処理をロールバックさせるために、例外をスローするようにしてください。
  • LockUtils#lock メソッドでロックの取得ができなかったときは例外 PessimisticLockException が返されます。

例外 PessimisticLockException を補足したい場合

上のコードはすべての例外を throw してロールバックしていますが、PessimisticLockException を補足したい場合は次のように記述することができます。

var PessimisticLockException = Java.type("javax.persistence.PessimisticLockException");
...
try {
  LockUtils.lock(user, userDaoHelper);/*ロック取得*/
  user.name = "ジャスミン太郎";
  entityService.update(user);
} catch (e if e instanceof PessimisticLockException) {
  print("lock error " + e);
  throw e;
} catch (e) {
  print("error " + e);
  throw e;
} finally {
  LockUtils.release(user, userDaoHelper);/*ロック解放*/
}

楽観ロック利用の場合

楽観ロック利用時は findById() の第2引数は無視されます。そのため「引数なし」「true を指定する」「false を指定する」のいずれも動作に違いはありません。

クライテリアを利用する

Serviceを使って複数件のデータを取得する場合、クライテリアを使って任意の検索条件を指定できます。モデル設計で、検索条件を有効にしていない項目であってもクライテリアとして(条件を)指定することができます。

ただし、モデル定義で検索条件を有効にしていない場合、データベースにインデックスが作成されていません。必要に応じて、手動でインデックスを作成してください。

クライテリアを用意する

モデル毎に用意される CriteriaConverter を使います。コード例を示します。

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

モデルの定義で暗黙条件を指定していた場合、上の方法で取得した criteria は暗黙条件がセットされた状態となっています。(この回避策をこのあと説明します。)

メタクラスを使う

criteria を用いた検索条件の指定では、対象項目名は文字列ではなく、Wagbyが提供するメタクラスを使うことができます。例えば model1 モデルを定義するとメタクラス Model1Meta が自動生成されます。これは項目名と型の情報(メタデータ)を保持する特別なクラスです。メタクラスを使うことでスペルミスによる実行時エラーといった、デバッグしにくい問題を回避することができます。

メタクラスを使って criteria に検索条件を設定する例を示します。

var Model1Meta = Java.type("jp.jasminesoft.wagby.model.model1.Model1Meta");
var model1Meta = new Model1Meta();
criteria.eq(model1Meta.item1, 1000); /* item1 の値が 1000 のデータで絞り込む */

eq は等しい、という条件になります。他にもさまざまな条件を指定することができます。

検索する

Service が提供する find メソッドを利用するときに、第一引数にクライテリアを渡します。戻り値はリストとなっており、この中に(検索条件に合致した)複数のストアモデルが格納されています。

var list = Model1Service.find(criteria);
if (list !== null && list.size() > 0) {
  // list から 1 つずつオブジェクトを取り出す
  for (var i=0; i<list.size(); i++) {
    var m = list.get(i);
    print(m);
  }
}

並び替えを指定する [1]

Order クラスを使って、criteria に並び替え条件を追加することができます。例を示します。

var OrderClass = Java.type("org.hibernate.criterion.Order");
var order = OrderClass.desc(Model1Meta.start_date.name());
criteria.addOrder(order);/* 並べ替え条件の追加 */
/*criteriaを使って検索する*/
...
  • Orderクラスは、ascとdescメソッドを利用できます。それぞれ昇順、降順の指定です。
  • asc/descメソッドの引数は Meta クラスの項目名を使います。項目名.name() とすることで、適切な項目名が渡されます。Meta クラス利用時は、項目名の部分はキャメル記法を使わず、設計情報に記載した項目IDをそのまま使ってください。(この例では startDate ではなく start_date となっています。)
  • 上の例では一つの order を指定していますが、criteria.addOrder に order の配列を渡すことで、複数の並び替え指定を行うことができます。
  • 並び順を指定しなかった場合でも、主キーでの並び替えは必ず行われます。addOrder は、主キーによる並べ替えに続くルールとして扱われます。

並び替えを指定する [2]

Wagbyでは、主キーは必ずソートキーとして扱われます。すなわち SQL の ORDER BY 句に主キーが含まれます。

ワンポイント

主キーを ORDER BY に含めるのはページネーション機能に SQL の LIMIT を利用しているためです。ORDER BY の結果の行が一意とならない場合、改ページしても(ORDER BY のソートルールの曖昧さにより)同じデータが次のページに出現してしまうことがあります。これを避けるため、ORDER BY 句に主キーを含めています。このルールを無効にすることはできません。

この対応により、"LIMIT を使用するときは、結果の行を一意な順序に制約する ORDER BY 句を使用する" というデータベースのルールに準拠しています。

この制約を回避するために、CriteriaConverter クラスの defaultCriteria(Order[]) メソッドを使うことができます。開発者が引数に指定した並べ替え条件を優先し、主キーによる並び替えは ORDER BY 句の最後に行うようにすることができます。(つまり優先度を下げることで影響を抑えます。)

var OrderClass = Java.type("org.hibernate.criterion.Order");
var CustomerMetaClass = Java.type("jp.jasminesoft.wagby.model.customer.CustomerMeta");
var customerMeta = new CustomerMetaClass();
var orders = [
    OrderClass.asc(customerMeta.companyname.name()),
    OrderClass.asc(customerMeta.deptname.name())
];
var criteriaConverter = p.appctx.getBean("CustomerCriteriaConverter");
/* 開発者が並び順を指定する */
var criteria = criteriaConverter.defaultCriteria(orders);

暗黙条件を適用させないクライテリアを用意する

暗黙条件を適用させたくない場合は criteriaConverter を使わず、次のようにしてください。

var DetachedCriteria = Java.type("jp.jasminesoft.jfc.hibernate.DetachedCriteria");
var CustomerClass = Java.type("jp.jasminesoft.wagby.model.customer.Customer");
var criteria = DetachedCriteria.forClass(CustomerClass.class);

スクリプトでクライテリアをカスタマイズする

スクリプトでクライテリアをカスタマイズすることもできます。Wagbyの標準機能による検索では、複数の検索項目に検索条件を指定した場合は常にAND検索となります。例えば、ここでOR検索を行わせたい場合はCriteriaをカスタマイズすることで対応できます。

複合キーの場合

複合キークラスをストアモデルのインナークラスとして用意しています。
zaiko モデルが複合キーの場合は次のような記述となります。

var Zaiko = Java.type("jp.jasminesoft.wagby.model.zaiko.Zaiko");
var key = new Zaiko.Key();
key.pkey1 = xxx;/* xxx には、主キーの値が入ります */
key.pkey2 = yyy;/* yyy には、主キーの値が入ります */
var zaiko = zaikoEntityService.findById(key,true);/* 第二引数がtrueのときロックをかける */

例外発生によるロールバック

トランザクションスクリプト

トランザクションスクリプトでは return 文で任意の文字列を返すことで、処理をロールバックさせることができます。

BusinessLogicException

ヘルパ系のスクリプトでは、例外 BusinessLogicException をスローすることでロールバックさせることができます。この方法を説明します。

throw new Packages.jp.jasminesoft.jfc.core.exception.BusinessLogicException("エラーメッセージ");

ここで設定したエラーメッセージが画面に表示されます。

対象となるスクリプト

この方法が利用できるスクリプトは、ヘルパ系で、かつ session (Hibernateのsession) が null でない場合に有効です。(*)

例えばヘルパの登録処理または更新処理のスクリプトで、ある条件に合致した場合に BusinessLogicException をスローさせることで、このトランザクションをロールバックさせるといった動作を実現します。

適用する前に、if (session !== null) {...} で session が有効かどうかを確認してください。

トランザクション境界

Wagby のトランザクション境界とは、外部データベースに対して "begin transaction" 命令を発行するタイミングをいいます。この命令が発行されてから、再びデータベースに対して "commit" または "rollback" 命令が発行されるまでは「1つのトランザクション」として扱われます。この間データベースに対して行われた変更処理はすべてコミットされるか、またはすべてロールバックされるかのどちらかになります。

新規登録・コピー登録画面、更新画面、一覧更新画面

コントローラクラス内の doInTransaction メソッドがトランザクション境界となります。

図2 トランザクション境界(更新)

削除、アップロード更新画面

コントローラクラスから呼び出される EntityService がトランザクション境界となります。

図3 トランザクション境界(削除)

doInTransaction メソッドと EntityService の関係

EntityService が提供するメソッドは、まだトランザクションが開始されていない場合は新しくトランザクションを開始するが、すでにトランザクションが開始されている場合は(新しいトランザクションを開始せず)現在のトランザクションをそのまま利用するようになっています。

EntityService と Dao の関係

EntityService が提供するメソッドは、内部で Dao を呼び出します。EntitySerivce は上述したようにメソッド開始時に自動的にトランザクションが開始されます。メソッドが正常終了すれば自動的にコミットされ、例外(Exception)が発生すれば自動的にロールバックされます。一方 Dao はトランザクション管理機能を持ちません。Dao利用時にトランザクションが開始されていなければエラーが発生します。

一覧更新画面や親子同時更新画面での応用

一覧更新画面や親子同時更新画面は doInTransaction メソッドがトランザクション境界となっているため、保存時に(一意制約等のデータベースエラーとなった場合)処理がロールバックされ、編集画面に戻るようになっています。

また、コントローラの一覧更新のスクリプト「データベースコミット前」で例外を発生させると更新処理全体がロールバックされ、編集画面に戻るようになっています。すなわち修正したデータをすべてコミットさせるか、あるいはすべてロールバックさせるかをスクリプトでも制御できるようになっています。[詳細...]

トランザクションスクリプトでもServiceを利用するケース

通常、トランザクションスクリプトで他のモデルの値を操作する場合は Dao を使います。しかしここで説明するように、トランザクションスクリプトであっても Service を使う必要のある場合があります。次のような例を想定します。

  • 「売上伝票 (salesslip)」モデルと、「販売商品 (product4s)」モデルがある。
  • 売上伝票は、明細として、どの商品をいくつ販売したかを保持する。
  • 売上伝票を新規登録するタイミングで、販売商品の在庫数を減らしたい。(トランザクション)
図4 売上伝票モデルと販売商品モデル

売上伝票 新規登録時のトランザクションスクリプト

売上伝票モデルの明細(繰り返しコンテナ)に含まれる「商品コード」は、販売商品モデルへの参照項目となっています。この項目にトランザクションクリプトを記述します。

実行したい内容は、売上伝票登録時に、明細に含まれる(いくつ購入したか、という)数量を、商品在庫から減じることです。

図5 新規登録時のトランザクションスクリプト

このときのトランザクションスクリプトは次のようになります。(この時点では Dao も Service も使う必要はありません。)

var suryou = product4s.stock;
var syukko_num = precord.PNumber;
/*print("suryou="+suryou+",syukko_num="+syukko_num);*/
if (suryou - syukko_num < 0) {
    return precord.PName + "の在庫 "+suryou+" に対して "+syukko_num+" を出庫しようとしました。";
}
product4s.stock = suryou - syukko_num;
return null;
  • product4s.stockは、販売商品モデルの在庫数項目を指します。
  • precordは明細(繰り返しコンテナ)の1行を指します。明細行が3レコードあった場合、上のスクリプトは3回、実行されます。
  • 繰り返しコンテナprecordの、販売数量p_numberを、対応する(販売商品モデルの)在庫から減じています。
  • 項目名にはキャメル記法を適用します。p_numebrは、スクリプト中ではPNumberと表記します。
  • 処理が正常に終了した場合、nullを返します。

売上伝票 更新時のトランザクションスクリプト

次に更新処理を考えてみます。更新のスクリプトは、登録と同じではありません。

更新の場合は「一度、登録した値」と「更新画面で変更した値」の差をとって、在庫数を再計算する必要があります。 ここで、一度登録した値というのは、データベースに保存された値を指します。 すなわちデータベース上の値と、画面から再入力された値の差を求めるという処理になります。

更新対象のデータとの比較のために更新前のデータをデータベースから取得する場合は別トランザクションで取得する必要があります。

この対応のために、別トランザクションで get 処理を行う仕組み newTransactionEntityService を用いてください。コード例を示します。

図6 更新時のトランザクションスクリプト
var entityService = p.appctx.getBean("SalesslipEntityService");
var newTransactionEntityService = entityService.newTransactionEntityService();/*別トランザクションになる*/
var o_salesslip = newTransactionEntityService.findById(salesslip.id, false);/* salesslip.idは主キー */
var o_precords = o_salesslip.precord;
var o_precord;
for (var i=0; i<o_precords.length; i++) {
    o_precord = o_precords[i];
    if (o_precord.PNo == precord.PNo) {
        break;
    }
}
if (o_precord !== null) {
    var suryou = product4s.stock;
    var syukko_num = precord.PNumber - o_precord.PNumber;
    /*print("now suryou="+suryou+",syukko_num_diff="+syukko_num);*/
    if (syukko_num !== 0) {
        if (suryou - syukko_num < 0) {
        return precord.PName + "の在庫 "+suryou+" に対して "+syukko_num+" を出庫しようとしました。";
        }
        product4s.stock = suryou - syukko_num;
    }
}
return null;
  • サービスオブジェクトから、データベースの値を取得します。これを o_salesslip という変数に格納します。find は主キーを渡して1件のデータを取得します。
  • newTransactionEntityService メソッド経由で取得できるサービスはデータの取得専用です。登録/更新/削除処理などは行えません。(*)
  • forループを使って、現在、対象としている明細行とマッチするものを探します。
  • マッチした行について、画面からの入力値との差分を求めます。この差を改めて在庫数から減じています。

ワンポイント

この例では、一度登録した売上伝票の更新を認めていますが、実際の業務では更新を認めないというシナリオもあります。また更新ではなく、変更のための伝票を新規で登録させるというシナリオもあります。今回のコードは一つの例としてお読みください。

メモ

上の例では、データベースの値を取得するためにサービスオブジェクトを用いました。別のアプローチとして、SQL式を使ってデータベースの値を取得することもできます。

Spring の @Transactional(propagation = Propagation.REQUIRES_NEW) を利用しています。

制約

  • アップロード更新機能を用いるモデルには、newTransactionEntityService を適用することはできません。

トランザクションスクリプトで参照連動の解決を抑制する

上で説明した更新時のトランザクションスクリプトの説明を続けます。このスクリプトでは入力された値とは別に、現在データベースに保持されている値を Dao を使って取得しています。Dao を使うと、参照連動項目もすべて解決します。そのため、データベースに発行するSQLは参照連動の数に比例して増大します。

しかし今回の目的で利用する o_precord.PNo や o_precord.PNumber は参照連動項目ではありません。そこで Dao 利用時に、不要な参照連動項目の解決をスキップさせることでパフォーマンス向上を図ることができます。

DataBindingContext

DataBindingContextもWagbyが提供するクラスです。このクラスに、参照連動の解決を行う項目名を明示することができます。使い方の例を示します。

var DataBindingContext = Java.type("jp.jasminesoft.jfc.dao.DataBindingContext");
var dao = p.appctx.getBean("SalesslipDao");
var dataBindingContext = new DataBindingContext();
var targetItemSet = new java.util.HashSet();
targetItemSet.add("customerName");/* 解決したい参照連動項目 */
dataBindingContext.setTargetItemSet(targetItemSet);
var o_salesslip = dao.get(salesslip.id, true, dataBindingContext);/* 第3引数に指定 */

このように、dao の get メソッドの第3引数に、dataBindingContext を渡します。この引数が未指定の場合、すべての参照連動項目を解決します。

解決したい参照連動項目が繰り返しコンテナ内の項目の場合、コンテナ名は含めないでください。(コンテナ名の後ろの)項目名のみを指定します。

参照連動処理をスキップする

すべての参照連動処理をスキップする場合、空の dataBindingContext を渡します。

var DataBindingContext = Java.type("jp.jasminesoft.jfc.dao.DataBindingContext");
var dao = p.appctx.getBean("SalesslipDao");
var dataBindingContext = new DataBindingContext();
var targetItemSet = new java.util.HashSet();
dataBindingContext.setTargetItemSet(targetItemSet);/* 項目を明示しない */
var o_salesslip = dao.get(salesslip.id, true, dataBindingContext);/* 第3引数に指定 */

図4の更新用トランザクションスクリプトに、参照連動項目の解決をスキップするコードを加えた例を示します。

var DataBindingContext = Java.type("jp.jasminesoft.jfc.dao.DataBindingContext");
var dao = p.appctx.getBean("SalesslipDao");
var dataBindingContext = new DataBindingContext();
dataBindingContext.setTargetItemSet(new java.util.HashSet());/* 項目を明示しない */
var o_salesslip = dao.get(salesslip.id, true, dataBindingContext);/* 第3引数に指定 */
print(o_salesslip);/* コンソールに出力。確認用 */
var o_precords = o_salesslip.precord;
var o_precord;
for (var i=0; i<o_precords.length; i++) {
    o_precord = o_precords[i];
    if (o_precord.PNo == precord.PNo) {
        break;
    }
}
if (o_precord !== null) {
    var suryou = product4s.stock;
    var syukko_num = precord.PNumber - o_precord.PNumber;
    /*print("now suryou="+suryou+",syukko_num_diff="+syukko_num);*/
    if (syukko_num !== 0) {
        if (suryou - syukko_num < 0) {
        return precord.PName + "の在庫 "+suryou+" に対して "+syukko_num+" を出庫しようとしました。";
        }
        product4s.stock = suryou - syukko_num;
    }
}
return null;

トランザクションスクリプトでSQLを併用する

トランザクションスクリプト内で SQL を直接、実行することもできます。

ここでは上の題材を使って、Dao ではなく SQL を用いて明細データの数量を取得するコードを説明します。

try {
  if (session !== null) {
    /*旧データ読み込み*/
    var sql =
      "SELECT \"p_number\" FROM \"salesslip$precord\"" +
      " WHERE \"id\"=" + salesslip.id + " AND" +
      " \"precordjshid\"=" + (precord.PNo - 1);
    /*print("sql="+sql);*/
    var o_PNumber = session.createSQLQuery(sql).uniqueResult();
    /*print("o_PNumber="+o_PNumber);*/
    var suryou = product4s.stock;
    var syukko_num = precord.PNumber - o_PNumber;
    print("now suryou="+suryou+",syukko_num_diff="+syukko_num);
    if (syukko_num !== 0) {
      if (suryou - syukko_num < 0) {
        return precord.PName + "の在庫 "+suryou+" に対して "+syukko_num+" を出庫しようとしました。";
      }
      product4s.stock = suryou - syukko_num;
    }
  }
} catch (e) {
  e.printStackTrace();
}
return null;
  • 繰り返しコンテナは別テーブルとして用意されます。これは「モデル名$繰り返しコンテナ名」というテーブルになります。
  • 繰り返しコンテナID項目の物理項目名は常に「繰り返しコンテナ名jshid」となります。
  • 繰り返しコンテナテーブルの主キーは、親となるモデルの"主キー"と、"繰り返しコンテナ名jshid" の複合キーになります。
  • 「繰り返しコンテナ名jshid」は 0 から開始されます。そのため上のコードでは precord.PNo - 1 と、1を減じた値を使って参照しています。

SQL を利用する説明の詳細は "SQLを利用する" の節もあわせてお読みください。

Service内部で動作するスクリプトを用意する

Service の内部でさらにスクリプトを用意する方法もあります。一つのトランザクション内で、任意のモデルを操作することができます。

例えば model1 の登録、更新、削除時に、同一トランザクションでスクリプトを実行したい場合は次のスクリプトを用意することができま す。

図7 <モデルID>EntityService_UpdateRelatedModels<タイミング>Trasaction.js

登録

WEB-INF/script/model1/Model1EntityService_updateRelatedModelsInsertTransaction.js

更新

WEB-INF/script/model1/Model1EntityService_updateRelatedModelsUpdateTransaction.js

削除

WEB-INF/script/model1/Model1EntityService_updateRelatedModelsDeleteTransaction.js

これらのスクリプトは Designer からは設定できません。直接、ファイルを用意してください。

process 関数を作成することで、同一トランザクション内で動作するスクリプトを記述することができます。

function process() {
(ここに開発者独自のコードを記載してください。)
}

トランザクションスクリプトとの違い

トランザクションスクリプトは対象モデルを明示することで、スクリプトで直接、対象モデル(オブジェクト)を利用することができます。すなわち、対象モデル(オブジェクト)を別途、Dao を使って取得する必要はありません。その代わり、このモデルに紐づいていることが必要です。関連のないモデル(オブジェクト)を利用することはできません。

ここで説明した方法(EntityService の内部から呼び出すスクリプト)は、トランザクションが開始された状態であることがわかっています。そのため、スクリプト内で Dao を使って (1) Aモデルの取得(SELECT) (2) Bモデルの登録(INSERT) (3) Cモデルの更新(UPDATE) を記述すると、これらの処理はすべて同一のトランザクションとして扱われます。

"ヘルパ > 登録/更新/削除" スクリプトとの違い

図5からわかるように、ヘルパのスクリプトもまた EntityService のトランザクション内で実行されます。そのため上で説明した方法(EntityService の内部から呼び出すスクリプト)とヘルパのスクリプトは、トランザクション境界という視点では違いはありません。

細かい違いとして、あるモデルの更新時に(同じモデルの)他のデータの更新を行う処理を考えた場合、更新するデータの数だけ、そのモデルに関するヘルパのbeforeUpdateメソッドに紐づくスクリプトが呼び出されます。これが冗長である場合は、本方法が向いているといえます。

本方法が向いている例:

  • 有効/無効のフラグを「有効」にして保存した場合、「有効」になっている(同じモデルの)データを「無効」に更新する。つまり、有効データを常に1件のみにする。
  • 有効/無効のフラグが「有効」のデータを削除した場合、(同じモデルの)データの中から最新のデータを「無効」から「有効」に更新する。

SQLを利用する

SQLでデータを取得する

Wagbyが内部で利用しているデータベース操作のためのミドルウェアHibernateが提供するsessionオブジェクトを用いて、任意のSQLを実行することができます。トランザクションスクリプト内では、このsessionオブジェクトを利用できます。

次の例は、アカウントモデル(juser)の個数を求めるSQLを実行します。

try {
  if (session !== null) {
    var o = session.createSQLQuery(
      "SELECT COUNT(*) FROM \"juser\"").uniqueResult();
    print("o="+o);/* デバッグ用 */
  }
} catch (e) {
  e.printStackTrace();
}
  • createSQLQuery の戻り値の型は、お使いのデータベースによって異なります。Integer, Long, BigInteger などの可能性があります。[詳細...]
  • モデル名(英語)に "session" を使うことはできません。この名前は(暗黙オブジェクトとして)予約されています。
  • ここで用いているsessionは、Hibernateが提供するクラスです。sessionの利用は、Hibernateの使い方に準じます。
  • sessionは、トランザクションスクリプトでは常に有効です。それ以外のスクリプトでは、呼び出す場所によって無効の場合があります。よってトランザクションスクリプト以外の場所で用いる場合、上記例のようにsessionがnullかどうかの判定を加えるようにしてください。
  • トランザクションスクリプトで利用できるデータベースコネクション (session) はクローズする必要がありません。内部で自動的にクローズします。
  • 上の例では項目名やテーブル名を二重引用符で囲んでいますが、このルールはお使いのデータベースによって変わります。(内蔵DBであるHSQLDB利用時は、二重引用符で囲みます。)このため、開発機と本番機で異なるデータベースを用いる場合は、スクリプトファイルの変更を行う必要があるかも知れませんので、ご注意ください。

トランザクションスクリプト以外の場所で session を利用する

トランザクションスクリプト以外の場所で session を利用する必要がある場合は、まず session が使えるかどうかを確認してください。session が null の場合、スクリプト内で HibernateUtil.openSession() を使って取得します。例を示します。

var HibernateUtil = Java.type("jp.jasminesoft.jfc.app.HibernateUtil");
var session = HibernateUtil.openSession();
try {
  /* session を利用した処理を実装する */
  ...
} catch (e) {
  e.printStackTrace();
} finally {
  if (session !== null) {
    session.close();/* 忘れないこと */
  }
}

重要

開発者が独自にオープンしたコネクションは、必ずクローズ処理を行ってください。クローズを忘れると、利用できるデータベースセッションが不足し、運用途中で実行時エラーになります。

ストアドプロシージャを利用する

トランザクションスクリプトからストアドプロシージャを呼び出す例を紹介します。Hibernate の session オブジェクトが利用できる場合は次のとおりです。

try {
  // 戻り値がないパターン (更新なし、データ取得のみ)
  var call1 = "{CALL FOO()}";
  session.createSQLQuery(call1).executeUpdate();
  // 戻り値が1つのパターン (更新なし、データ取得のみ)
  var call2 = "{CALL BAR()}";
  var result = session.createSQLQuery(call2).uniqueResult();
  print(result);
  // 戻り値が複数のパターン (更新なし、データ取得のみ)  
  var call3 = "{CALL BAZ()}";
  var list = session.createSQLQuery(call3).list();
  print(list);
} catch(e) {
  e.printStackTrace();
}
  • ストアドプロシージャの戻り値が単一の場合は uniqueResult() を、複数の場合は list() を使います。

Hibernate の session オブジェクトを用意する

トランザクションスクリプト外でストアドプロシージャを呼び出す場合は、Hibernate の session オブジェクトを用意してください。

var HibernateUtil = Java.type("jp.jasminesoft.jfc.app.HibernateUtil");
var session = HibernateUtil.openSession();
try {
  var tx = session.beginTransaction();
  var call = "{CALL POWER(2, 3)}";
  var result = session.createSQLQuery(call).uniqueResult();
  print(result);
  tx.commit();
} catch(e) {
  e.printStackTrace();
  tx.rollback();
} finally {
  if (session !== null) {
    session.close();
  }
}
  • トランザクションスクリプト外の場合は Hibernate の session オブジェクトを直接取得し、トランザクションの開始と終了を明示的に行なったあと、session オブジェクトをクローズする必要があります。
  • 上で利用した POWER(2, 3) は 2の3乗を計算する関数で、内蔵データベース(HSQLDB)に組み込まれています。実際の利用時は、この部分を適切に変更してください。

IN/OUTパラメータを利用する

ストアドプロシージャの IN/OUT パラメータを利用することもできます。Hibernate が提供する JDBC プログラミング方法を利用します。

var JFCUtils = Java.type("jp.jasminesoft.jfc.JFCUtils");
var HibernateUtil = Java.type("jp.jasminesoft.jfc.app.HibernateUtil");
var Work = Java.type("org.hibernate.jdbc.Work");
var DbUtils = Java.type("org.apache.commons.dbutils.DbUtils");
var Types = Java.type("java.sql.Types");
var session = HibernateUtil.openSession();
try {
  /* トランザクションを開始する */
  var tx = session.beginTransaction();
  var out = null;
  var MyWork = Java.extend(Work, {
    execute: function(connection) {
      var st = null;
      try {
        // ストアドプロシージャ呼び出し (更新なし、データ取得のみ)
        st = connection.prepareCall("{CALL XXX(?, ?)}");
        // IN パラメータの指定(1番目のパラメータ)
        st.setInt(1, id);
        // OUTパラメータの指定(2番目のパラメータ)
        st.registerOutParameter(2, Types.INTEGER);
        st.execute();
        // OUTパラメータの取得
        out = st.getInt(2);
      } finally {
        DbUtils.closeQuietly(st);
      }
    }
  });
  session.doWork(new MyWork());
  print(out);//デバッグ用
  /* コミットする */
  tx.commit();
} catch (e) {
  e.printStackTrace();
  tx.rollback();
} finally {
  if (session !== null) {
      session.close();
  }
}
  • function(connection) {…} の内部は、一般的な JDBC プログラミングスタイルを利用できます。
  • 上の例では整数型となっていますが、実際のご利用では適切な型を指定してご利用ください。
  • WorkクラスはHibernateが提供します。Workクラスのexecuteメソッド内ではjava.sql.Connectionを利用できます。つまりJDBC APIを用いたLow Level Layerのプログラミングが行なえるため、開発者の自由度は高まります。
  • 上の例では Hibernate の session オブジェクトを直接、取得していますが、トランザクション境界内部で(すでに session が使える状態で)利用する場合は openSession と beginTranscation そして close 処理は割愛できます。

時間のかかるストアドプロシージャを呼び出す

Statement クラスが提供する setQueryTimeout を指定することができます。サンプルコードを示します。

var JFCUtils = Java.type("jp.jasminesoft.jfc.JFCUtils");
var HibernateUtil = Java.type("jp.jasminesoft.jfc.app.HibernateUtil");
var Work = Java.type("org.hibernate.jdbc.Work");
var DbUtils = Java.type("org.apache.commons.dbutils.DbUtils");
var Types = Java.type("java.sql.Types");
var HibernateUtil = Java.type("jp.jasminesoft.jfc.app.HibernateUtil");
var Work = Java.type("org.hibernate.jdbc.Work");
var DbUtils = Java.type("org.apache.commons.dbutils.DbUtils");
var Types = Java.type("java.sql.Types");
var session = HibernateUtil.openSession();
try {
  var tx = session.beginTransaction();
  var MyWork = Java.extend(Work, {
    execute: function(connection) {
      var st = null;
      try {
        st = connection.prepareCall("EXEC TEST1 ?, ?, ?");
        st.setQueryTimeout(10000); /* タイムアウトの設定 */
        st.registerOutParameter(3, Types.INTEGER);
        st.setInt(1, 4);
        st.setInt(2, 5);
        st.execute();
        print("result:[" + st.getInt(3) + "]");
      } catch (e) {
        e.printStackTrace();
      } finally {
        DbUtils.closeQuietly(st);
      }
    }
  });
  print("Original1 procedure execute start time: ", new Date());
  session.doWork(new MyWork());
  print("Original1 procedure execute end time: ", new Date());
  print("");
  tx.commit();
} catch (e) {
  e.printStackTrace();
  tx.rollback();
} finally {
  if (session !== null) {
      session.close();
  }
}
  • function(connection) {…} の内部は、一般的な JDBC プログラミングスタイルを利用できます。

SQLやストアドプロシージャでデータを更新する

SQLまたはストアドプロシージャで更新・削除を行う場合はロックとキャッシュに関する制御を開発者自身で記述する必要があります。

検索系のSQLを使う限りにおいては、ロックとキャッシュに関する制御は不要です。
更新を伴う場合は (SQLまたはストアドプロシージャの利用に代わって) Wagby が提供する Service の利用も検討するとよいでしょう。Service はロックの取得と解放、および検索結果のキャッシュ処理が自動的に行われるため、開発者はこれらを意識する必要がありません。

ロックの取得

更新したテーブルに対応した Wagby のモデルがあり、かつ悲観ロック方式を適用している場合、そのモデルのロックを取得する必要があります。この手順を踏まない場合、別の利用者がこのモデルの対象データを更新中かどうかを考慮せずにデータを更新してしまうため、データの整合性がとれなくなります。

更新するテーブルが Wagby の管理外(すなわち Wagby のモデルとして定義されていない)場合は、このロック取得は不要です。
Wagby の定義で、このモデルのロック方式を楽観ロックとすることができます。この場合も以下に説明するロック取得は不要です。ただし実行する SQL またはストアドプロシージャで一般的な楽観ロック方式の手順に従った対応を行なってください。(更新処理の前後に「バージョン管理用カラム」の値を取得し、この値が同一かどうかを確認します。同一であれば他者の更新はなかったと判断できます。さらに更新時には、バージョン管理用カラムの値をインクリメントしてください。)

model1 モデルの1件のデータのロックを取得するコードは次のとおりです。

var LockUtils = Java.type("jp.jasminesoft.jfc.core.util.LockUtils");
var model1DaoHelper = p.appctx.getBean("Model1DaoHelper");
var lockName = model1DaoHelper.getLockName();
...
try {
  LockUtils.lock(lockName, "1000", p);/*ロック取得*/
  ...
  // 更新処理
  ...
} catch (e) {
  throw e;/*ロールバックのため*/
} finally {
  LockUtils.release(lockName, "1000", p);/*ロック解放*/
}
  • LockUtils.lockメソッドおよびreleaseメソッドの第一引数はロック名になります。ロック名はXXXDaoHelper#getLockName()で取得してください。
  • LockUtils.lockメソッドおよびreleaseメソッドの第二引数は対象データの主キーの文字列表現となります。複合主キーの場合は$SEP$で連結した文字列としてください。詳細はWagbyが生成したコード <モデルID>Helper.java の getPrimaryKeyAsString メソッドをお読みください。(主キーの文字列表現は)このメソッドの戻り値と同じ結果となるようにしてください。
  • 複数のデータを更新する場合、主キーを個別に指定して複数回、lock メソッドと release メソッドを実行してください。
  • 他者がすでに当該データの更新画面を開いている場合、lock メソッドは失敗します。この場合は実行時例外 PessimisticLockException が生じます。catch 節で例外を判定することができます。例えば p.errors に更新失敗のメッセージを格納し、処理を中断させるというアプローチがあります。

モデル全体をロックする8.1.0

ストアドプロシージャですべてのデータを更新するため、モデル全体をロックしたいという場合は次のコードになります。

var LockUtils = Java.type("jp.jasminesoft.jfc.core.util.LockUtils");
var model1DaoHelper = p.appctx.getBean("Model1DaoHelper");
var lockName = model1DaoHelper.getLockName();
...
try {
  LockUtils.modelLock(lockName, p);/*ロック取得*/
  ...
  // 更新処理
  ...
} catch (e) {
  throw e;/*ロールバックのため*/
} finally {
  LockUtils.releaseModelLock(lockName, p);/*ロック解放*/
}
  • LockUtils.modelLockメソッドおよびreleaseModelLockメソッドの第一引数はロック名になります。ロック名はXXXDaoHelper#getLockName()で取得してください。
  • 対象モデルの1データでも他者が更新中の場合、modelLockメソッドは失敗します。
  • modelLockメソッドが成功すると、他者は一切このモデルの更新が行えなくなります。更新処理終了後、速やかにロックを解放するようにしてください。

キャッシュのクリア

更新したテーブルに対応した Wagby のモデルがある場合、そのモデルのキャッシュをクリアします。

Wagby は標準で、データベースから読み込んだ値をメモリに保持することでパフォーマンス向上を図っています。Wagby が提供する Service を使う場合、キャッシュのクリアは自動的に行われます。SQLを用いて更新した場合、キャッシュのクリア処理を開発者が行ってください。
更新したテーブルが Wagby の管理外(すなわち Wagby のモデルとして定義されていない)場合は、このキャッシュクリアは不要です。
Wagby の定義で、このモデルのキャッシュを無効にすることもできます。この場合もキャッシュクリア処理は不要となります。しかしこの方針は実行時のパフォーマンスに影響するため、慎重に判断してください。

model1 モデルのキャッシュクリアを行うコードは次のとおりです。

// ... データベースの更新が終了した ...
var CacheManagerClass = Java.type("jp.jasminesoft.wagby.app.CacheManager");
var cman = CacheManagerClass.getInstance(p);
cman.clearModel1();
  • CacheManagerClass のパッケージ名は適切に読み替えてください。
  • モデルのキャッシュをクリアするメソッドclear<モデルID>が、モデルごとに用意されています。このメソッドを実行してください。

ご注意ください - 同じモデルの操作はできません

対象と同じモデルのデータの操作を SQLまたはストアドプロシージャを使って操作することはできません。

その理由は、Wagby内部の「ストアモデル」が、最終的に Hibernate によって SQL に変換され、DB 更新されるためです。SQLやストアドプロシージャを使ってデータベースの値を書き換えても、そのあと Hibernate の update メソッドで、もともと Wagby が管理しているストアモデルの情報で上書き保存されてしまいます。

同じモデルの操作を行う要件であればストアドプロシージャではなく、Wagbyが提供するオブジェクトを操作するようにしてください。具体的にはストアモデルが提供している setter/getter メソッドを使って値を変更してください。

SQLやストアドプロシージャを使って別のモデルを操作することは問題ありません。

プログラムによるトランザクション管理

Wagbyは標準で Spring が提供する宣言的トランザクションを利用しています。これとは別に開発者が直接、トランザクション管理を行うことも可能です。(プログラマティックトランザクション)

具体的には Spring が提供する TransactionTemplate クラスを使って、トランザクション境界の中で動作するコードを記述することができます。この処理をスクリプトで実現することができますが、事前知識として Java で書く方法を知っておくとよいでしょう。この詳細は "Javaを用いたカスタマイズ > Service/Daoクラス > プログラムによるトランザクション管理" をお読みください。

例 コントローラクラスの拡張(独自ボタン)

customer モデルの詳細画面に独自ボタン(イベント名:Original1)を配置し、ボタン押下時に実行される処理として作成しています。
このスクリプトファイルは WEB-INF/script/customer フォルダに ShowCustomer_Original1.js として保存します。

function process() {
  var ExcelFunction = Java.type("jp.jasminesoft.util.ExcelFunction");
  var Jfcerror = Java.type("jp.jasminesoft.jfc.error.Jfcerror");
  // トランザクション処理結果の戻り値が必要な場合は
  // TransactionCallback クラスを利用します。
  // 今回は戻り値が不要なので TransactionCallbackWithoutResult クラス
  // を使っています。
  var TransactionCallbackWithoutResult = Java.type(
      "org.springframework.transaction.support.TransactionCallbackWithoutResult");
  // TransactionCallbackWithoutResult クラスの doInTransactionWithoutResult()
  // メソッドをオーバーライドします。
  var MyTransactionCallbackWithoutResult
      = Java.extend(TransactionCallbackWithoutResult, {
          doInTransactionWithoutResult: function(status) {
            // トランザクションが開始された状態で本メソッドが実行され、
            // メソッド内で Exception が発生すると自動的にロールバック
            // されます。Exception が発生せずに本メソッドが正常終了する
            // と自動的にコミットされます。
            var helper = p.appctx.getBean("JnewsHelper");
            var service = p.appctx.getBean("JnewsEntityService");
            // 1件目のデータ登録
            var jnews = new Packages.jp.jasminesoft.wagby.model.jnews.Jnews();
            helper.initialize(jnews, p);
            jnews.limitdate = ExcelFunction.TODAY();
            jnews.title = "お知らせ1";
            service.insert(jnews);
            // 2件目のデータ登録
            jnews = new Packages.jp.jasminesoft.wagby.model.jnews.Jnews();
            helper.initialize(jnews, p);
            jnews.limitdate = ExcelFunction.TODAY();
            jnews.title = "お知らせ2";
            service.insert(jnews);
          }
      });
  try {
    // Spring が提供する TransactionTemplate を使って、
    // @Transactional アノテーションを用いずにプログラム実装による
    // トランザクション機能を実現します。
    var transactionTemplate = p.appctx.getBean("RequiredTransactionTemplate");
    transactionTemplate.execute(new MyTransactionCallbackWithoutResult());
  } catch (e) {
    var error = new Jfcerror();
    error.content = "エラーが発生しました。" + e.message;
    p.errors.addJfcerror(error);
  }
  // 元の画面へ遷移する。
  var id = p.request.getParameter("customerid");
  return "redirect:showCustomer.do?customerid="+id;
}

このコードの解説はこちらをお読みください。(同じ処理を Java 言語で実装したコードを用意しています。)

TransactionCallback内でHibernateのセッションを取得する

TransactionCallbackWithoutResult や TransactionCallback 内部で Hibernate のセッションオブジェクトを取得することができます。

var session = p.appctx.getBean("sessionFactory").getCurrentSession();

もっと詳しく