サポート > リポジトリ > 業務ロジック > Service/Daoクラス、ヘルパ、SQLを利用する
ja | en

スクリプトから(Wagbyが自動生成した)Service/Daoクラスを利用できます。またSQLを利用することもできます。 R7.6

Wagbyではモデルに対応した「サービスクラス」を自動生成します。これはトランザクション境界となっており、CRUD処理に関するメソッドを提供します。詳細は「Service/Daoクラス」をお読みください。

図1 ServiceクラスとDaoクラス

スクリプトでは、次のコードを用いてServiceオブジェクトを取得することができます。

var entityService = p.appctx.getBean(Serviceオブジェクト名);

サービス名は "<モデルID>EntityService" となります。モデルIDはキャメル記法で表現します。例えば customer モデルに対するServiceオブジェクト名は "CustomerEntityService" になります。

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

var entityService = p.appctx.getBean("JuserEntityService");
var user = entityService.findById("user01", true); /* 1件のデータを取得 */
stdout.println(user); /* デバッグ用ログ出力 */
user.name = "ジャスミン太郎";
entityService.update(user); /* 更新 */
  • サービスオブジェクトはトランザクション境界となっているため、updateメソッド呼び出しのタイミングでデータベースにコミットされます。
  • findByIdメソッドの第二引数はロック処理を行うことを示しています。詳細は「Wagby Developer Network(R7) > Javaを用いたカスタマイズ > Service/Daoクラス > ロックについて」をお読みください。

これにより、任意のモデルのデータを取得したり、更新することができます。

ロックについての注意:上記コードでは、findById で取得したロックは、その後の update メソッドの実行によって必ず解除されるため問題ありません。しかしロックを取得したが、update メソッドは実行しない場合、このスクリプトが終了してもロックは保持されたままとなります。この詳細と解決方法は後述する「ロックの取得と解除」をお読みください。

トランザクションスクリプトでは、このスクリプトを実行する前にトランザクションが開始されています。このとき、任意のモデルに関する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件のデータを取得 */
stdout.println(user); /* デバッグ用ログ出力 */
user.name = "ジャスミン太郎";
dao.update(user); /* 更新 */

Daoはトランザクション宣言を含みません。そのため dao.update メソッドの前にトランザクションは開始される必要があります。かつ dao.update メソッドのあとにトランザクションを終了する必要があります。

この制約から、Daoを使える場所はトランザクションスクリプトに限られます。トランザクションスクリプトは呼び出しの前後でトランザクションの開始と終了が自動的に実行されるため、Daoによる更新処理は正しく処理されます。

まとめ:トランザクションスクリプト内ではServiceオブジェクトとDaoのいずれも利用できます。トランザクションスクリプト以外のスクリプトでは、Serviceオブジェクトを用いてください。(更新系メソッドを実行するたびにデータベースはコミットされます。)
dao.get の第二引数はロックの取得を示します。詳細は「Wagby Developer Network(R7) > Javaを用いたカスタマイズ > Service/Daoクラス > ロックについて」をお読みください。

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

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

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

トランザクションスクリプト以外の場所で 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();/* 忘れないこと */
   }
}
開発者が独自にオープンしたコネクションは、必ずクローズ処理を行ってください。クローズを忘れると、利用できるデータベースセッションが不足し、運用途中で実行時エラーになります。
クローズ漏れの懸念がある場合は「Wagby Developer Network(R7) > Javaを用いたカスタマイズ > データベース > データベースのコネクションプーリング数を監視する」をお読みください。

スクリプトで (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);

stdout.println(po);
  • ヘルパクラスも p.appctx.getBean で取得します。
  • jp.jasminesoft.jfc.IPresentationHelperインタフェースは定数 SHOW, UPDATE, CSVDOWNLOAD, CREATEOBJECT という4つのモードを提供します。通常は SHOW または UPDATE を使います。s2p メソッドに SHOW を渡すと表示用のプレゼンテーションモデルへ変換します。UPDATE を渡すと更新画面で利用できるプレゼンテーションモデルへ変換します。これには選択肢の候補が含まれています。
ストアモデルやプレゼンテーションモデルは Wagby が提供する仕組みです。詳細は「Javaを用いたカスタマイズ」に用意されたスライドをお読みください。この内容を適切に理解するために、ジャスミンソフトが提供する技術セミナーを受講することを推奨します。

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

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

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 メソッドでロックを取得するのではなく、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) {
    LockUtils.release(user, userDaoHelper);/*ロック解放*/
  }
}
  • LockUtils クラスは Wagby に同梱されています。lock と release というメソッドを利用します。
  • 第一引数には対象オブジェクトを指定します。第二引数には、対象オブジェクトのDaoHelperクラスを指定します。

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

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

クライテリアを用意する

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

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

メタクラスを使う

モデル毎に用意される Meta クラスを使って、criteria に検索条件を設定します。コード例を示します。

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

eq は等しい、という条件になります。検索条件の詳細は「検索制御 > [応用] スクリプトで検索条件をカスタマイズする > 利用できる検索条件」をお読みください。

検索する

Service オブジェクト利用時は、第一引数にクライテリアを渡します。戻り値はリストとなっており、複数のストアモデルが格納されています。

var list = Model1Service.find(criteria);

if (list != null && list.size() > 0) {
  /* 処理 */
}

並び替えを指定する

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 の配列を渡すことで、複数の並び替え指定を行うことができます。

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

スクリプトでクライテリアをカスタマイズすることもできます。詳細は「検索制御 > [応用] スクリプトで検索条件をカスタマイズする」をお読みください。

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

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

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

トランザクションスクリプトではないですが、一連のトランザクションの処理内で呼ばれるスクリプト(例:ヘルパのスクリプト)で処理をロールバックさせることもできます。この方法を説明します。

BusinessLogicException

スクリプト内で BusinessLogicException をスローすることで、このトランザクションをロールバックさせることができます。

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

エラーメッセージは画面に表示されます。

対象となるスクリプト

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

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

トランザクションで、次のような例を想定します。

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

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

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

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

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

具体的なトランザクションスクリプトは次のようになります。

var suryou = product4s.stock;
var syukko_num = precord.PNumber;
/*stdout.println("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 を用いてください。コード例を示します。

図4 更新時のトランザクションスクリプト
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;
    /*stdout.println("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式を使ってデータベースの値を取得することもできます。詳細は「SQL式 > 保存前の値を保持する項目を用意する」をお読みください。
Spring の @Transactional(propagation = Propagation.REQUIRES_NEW) を利用しています。

図4で説明した更新時のトランザクションスクリプトの説明を続けます。このスクリプトでは入力された値とは別に、現在データベースに保持されている値を 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引数に指定 */

stdout.println(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;
    /*stdout.println("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 を直接、実行することもできます。

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

try {
    if (session != null) {
        /*旧データ読み込み*/
        var sql =
           "SELECT \"p_number\" FROM \"salesslip$precord\"" +
           " WHERE \"id\"=" + salesslip.id + " AND" +
           " \"precordjshid\"=" + (precord.PNo - 1);
        /*stdout.println("sql="+sql);*/
        var o_PNumber = session.createSQLQuery(sql).uniqueResult();
        /*stdout.println("o_PNumber="+o_PNumber);*/
        var suryou = product4s.stock;
        var syukko_num = precord.PNumber - o_PNumber;
        stdout.println("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を減じた値を使って参照しています。