データベース操作を行うServiceクラスとDaoクラスのカスタマイズ方法を説明します。

ServiceクラスとDaoクラスはモデル毎に用意されます。

Serviceクラスは内部でDaoクラスを利用します。Serviceクラスはトランザクション境界となっており、実際のデータベース操作はDaoクラスが行います。Daoクラスはそれ以外にキャッシュの制御や問合せのためのCriteriaの作成を行います。

図1 ServiceクラスとDaoクラス
Wagby R6 では「プロセスビーン」というクラスが担っていましたが、R8 では Service/Dao 層に分離されました。なお R8 でも一部でプロセスビーンが残っているところがありますが、今後のバージョンアップに伴い失くしていきます。そのためプロセスビーンを用いたカスタマイズは非推奨となっています。

ORMフレームワークHibernateを利用したクラスとなっています。CRUD処理は一つのDaoクラスで実現します。 またデータベースへの問合せはHibernateが提供する「CriteriaクエリAPI」を使います。SQLを直接、生成しているところはありません。

図2 Daoクラスのインタフェース

Daoクラスの利用例を示します。

// 主キー1000のデータを取得
Customer customer = customerDao.get(1000, true);

// データを更新
customer.setName("ジャスミン太郎");
customerDao.update(customer);

// データを登録
Customer customer2 = new Customer(customer);
customer2.setCustomerid(1001);
customerDao.save(customer2);

// 主キー2000のデータを削除
customerDao.deleteById(2000, true);

ロックについて

上の例では、customerDao.get メソッド、ならびにdeleteById メソッド呼び出し時に、第二引数を true としています。この場合、処理のタイミングで悲観ロック処理を適用します。ロックをかけた場合、同データが更新されれば、トランザクション終了のタイミングでロックは解除されますが、更新しない場合はロックが残ったままとなってしまいます。参照のみの処理で、更新する必要がないデータを取得する場合は、get メソッドの第二引数を false としてください。

後述する、EntityService の findById メソッドの第二引数も、同じ動作となります。

トランザクション境界となるクラスです。インタフェースを実装したクラスでは、各メソッドに @Transactional アノテーションが付与されています。

メソッド開始時に自動的にトランザクションが開始されます。正常終了でコミットされます。Exception発生時には自動的にロールバックされます。

図3 Serviceクラスのインタフェース

EntityService, Dao, Helper はメソッドの外側で以下のようにインスタンス変数を宣言することで取得することができます。

EntityService

@Autowired
@Qualifier("CustomerEntityService")
protected JFCEntityService<Customer, Integer> customerEntityService;

Dao

@Autowired
@Qualifier("CustomerDao")
protected JFCHibernateDao<Customer, Integer> customerDao;

Helper

@Autowired
@Qualifier("CustomerHelper")
protected EntityHelper<Customer, Integer> customerHelper;

各クラスの型パラメータの第一引数は対象クラスになります。 第二引数は主キーの型になります。 整数型の場合は Integer、文字列型の場合は String などと指定します。

WagbyではHibernateが提供するCriteriaを使ったデータベース検索を実現しています。 さらにEclipseなどのIDEを使ったコード補完の実現と、タイプミスを防ぐ目的で Meta クラスを自動生成しています。

Criteriaを利用したコード例を示します。

CustomerMeta meta = new CustomerMeta();
DetachedCriteria criteria = DetachedCriteria.forClass(Customer.class);
// customerid を検索するための Criteria を組み立てる(数値の範囲検索)。
criteria.between(meta.customerid,
        condition.getCustomerid1jshparamAsInteger(),
        condition.getCustomerid2jshparamAsInteger());
// deptname を検索するための Criteria を組み立てる(部分一致検索)。
criteria.like(meta.deptname, condition.getDeptname());
// 繰返し項目 email を検索するための Criteria を組み立てる。
criteria.like(meta.email, condition.getEmail());
// 繰り返しコンテナ内の項目 rdate を検索するための Criteria を組み立てる。
criteria.between(meta.rdate, condition.getRdate1jshparam(),
        condition.getRdate2jshparam());
// 会社名(降順)、顧客ID(昇順) でソートします。
// SQL : ORDER BY companyname DESC, customerid ASC
// Order はhibernateが提供するクラスです(org.hibernate.criterion.Order)
criteria.addOrder(
       new Order[] {Order.desc(meta.companyname.name()),
               Order.asc(meta.customerid.name())});
Criteria executableCriteria
        = criteria.getExecutableCriteria(getCurrentSession());
@SuppressWarnings("unchecked")
List<Customer> results = executableCriteria.list();

Metaクラス

項目名、テーブル名、列名の文字列を一括で管理します。Metaクラスを通してこれらの値を取得します。

図4 Metaクラス

CriteriaConverter

Wagbyは検索条件を保持するコンディションモデルを自動生成しています。コンディションモデルからCriteriaを取得するにはCriteriaConverterを使います。

CriteriaConverterの利用例を示します。

// CriteriaConverter インスタンスを作成
CustomerCriteriaConverter converter
        = (CustomerCriteriaConverter) p.appctx.getBean(
                "CustomerCriteriaConverter");
// Condition を Criteria に変換
DetachedCriteria criteria = converter.convert(condition, sortKey);
// Criteria を使って検索を実行
List<Customer> results = dao.findByCriteria(criteria);

メソッドの外側で以下のようにインスタンス変数を宣言することで CriteriaConverter インスタンスの取得することもできます。

/** CriteriaConverter インスタンスを作成 */
@Autowired
@Qualifier("CustomerCriteriaConverter")
protected CriteriaConverter<CustomerC> converter;
CriteriaConverterの型パラメータの引数はコンディションモデルになります。

ページング

Daoクラスが提供するfindByCriteriaメソッドはページング処理に対応しています。さらに現在のページを記憶する FinderContext クラスも提供しています。

findByCriteriaメソッドの利用例を示します。

CustomerCriteriaConverter converter
        = (CustomerCriteriaConverter) p.appctx.getBean(
                "CustomerCriteriaConverter");
DetachedCriteria criteria = converter.convert(condition, sortKey);
// 先頭の 10 件を取得
List<Customer> results = dao.findByCriteria(criteria, 0, 10);
// ...
// 次の 10 件を取得
results = dao.findByCriteria(criteria, 10, 10);

finderContextの利用例を示します。

// FinderContext インスタンスを作成
FinderContext<CustomerC> finderContext = new FinderContext<CustomerC>();
// Condition をセット
finderContext.setCondition(condition);
// ページサイズをセット( 0 をセットすると無制限となります)
finderContext.setPageSize(10);
// CriteriaConverter をセット
finderContext.setCriteriaConverter(
        (CustomerCriteriaConverter) p.appctx.getBean(
                "CustomerCriteriaConverter"));

// 先頭の 10 件を取得
List<Customer> results = dao.find(finderContext);

// 次の 10 件を取得
finderContext.next();
results = dao.find(finderContext);

// 最後のページのデータを取得
finderContext.last();
results = dao.find(finderContext);

件数の取得

Daoクラスが提供するcountメソッドの引数にfinderContextをセットすることで、件数を取得することができます。

// FinderContext インスタンスを作成
FinderContext<CustomerC> finderContext = new FinderContext<CustomerC>();
// Condition をセット
finderContext.setCondition(condition);
// CriteriaConverter をセット
finderContext.setCriteriaConverter(
        (CustomerCriteriaConverter) p.appctx.getBean(
                "CustomerCriteriaConverter"));

int size = dao.count(finderContext);

キャッシュ制御

Wagbyではパフォーマンス向上のために内部で積極的にキャッシュを用いています。 HibernateのInterceptor機能を利用しており、トランザクションコミットのタイミングでキャッシュをクリアするといった処理が自動的に行われます。

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

Zaiko.Key key = new Zaiko.Key();
key.setPkey1(xxx);/* xxx には、主キーの値が入ります */
key.setPkey2(yyy);/* yyy には、主キーの値が入ります */
Zaiko zaiko = zaikoEntityService.findById(key,true);/* 第二引数がtrueのときロックをかける */

Daoクラスでは getCurrentSession メソッドを使ってデータベースセッションを取得することができます。これを使ってSQLを直接、記述することもできます。

自動生成されたDaoクラスをカスタマイズしたMyDaoクラスを用意し、データを取得(get)したタイミングでデータベースのストアドプロシージャを呼び出す例を紹介します。

public class MyCustomerDao extends CustomerDao {

    /** {@inheritDoc} */
    @Override
    public Customer get(Integer pkey) {
        getCurrentSession().doWork(new Work() {
            public void execute(Connection connection) throws SQLException {
                CallableStatement st = null;
                ResultSet rs = null;
                try {
                    // ストアドプロシージャ呼び出し
                    st = connection.prepareCall("{?= CALL POWER(2, 3)}");
                    st.execute();
                    rs = st.getResultSet();
                    if (rs.next())  {
                      System.out.println(rs.getInt(1)); // 結果の出力。
                    }
                } finally {
                    DbUtils.closeQuietly(st);
                    DbUtils.closeQuietly(rs);
                }
            }
        });

        return get(pkey, true);
    }
}

getCurrentSession() 経由で取得したデータベースセッションは、Spring の TransactionManager の制御下にあります。そのため開発者はデータベースセッションのクローズ処理を行わないようにしてください。しかし開発者が独自に用意した Statement や ResultSet についてはクローズ処理が必要です。この場合は DbUtils.closeQuietly を使うとよいでしょう。

ヘルパクラスの afterUpdate メソッドをオーバーライドして、このタイミングでストアドプロシージャ呼び出し(または何らかのSQL処理)を行うコードの例を紹介します。

   @Override
   public void afterUpdate(Model1 entity, final ActionParameter p) {

       org.hibernate.Session sess = jp.jasminesoft.jfc.app.HibernateUtil.openSession();

       try {
           sess.doWork(new Work() {
               (ストアドプロシージャ呼び出し、またはSQL処理。)
           });
       } finally {
           sess.close();
       }

       super.afterUpdate(entity, p);
   }

Wagby では TransactionManager 経由で取得された Hibernate Session は自動的にクローズされますが、上記のように HibernateUtil.openSession() を使って取得した Hibernate Session はコード開発者が明示的にクローズ処理を行う必要があります。

上と同じ枠組みですが、SQLを呼び出す例も示します。(正確には SQL ではなく、Hibernate が提供する HQL です。)

   @Override
   public void afterUpdate(Model1 entity, final ActionParameter p) {

       org.hibernate.Session sess = jp.jasminesoft.jfc.app.HibernateUtil.openSession();

       try {
           Object o = sess.createSQLQuery(
               "SELECT COUNT(*) FROM \"juser\"").uniqueResult();
           System.out.println(o);
       } finally {
           sess.close();
       }

       super.afterUpdate(entity, p);
   }

詳細は Hibernate プログラミングとなるため、割愛します。

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

具体的には Spring が提供する TransactionTemplate クラスを使って、トランザクション境界の中で動作するコードを記述することができます。

例 ヘルパクラスの拡張

Desinger のスクリプト設定で「ヘルパ > 実行のタイミング : 登録(初期データ作成)」が用意されています。これは登録画面を開く前処理としてスクリプトを記述できますが、トランザクション外の処理となっています。

そこでヘルパクラスを拡張し、次のように TransactionTemplate クラスを利用することができます。モデル model1 を例にしています。

public class MyModel1Helper extends Model1Helper {

   @Autowired
   @Qualifier("RequiredTransactionTemplate")
   protected TransactionTemplate requiredTransactionTemplate;

   public void initialize(Model1 model1, ActionParameter p)

       requiredTransactionTemplate.execute(
               new TransactionCallbackWithoutResult() {
                   @Override
                   protected void doInTransactionWithoutResult(
                           TransactionStatus status) {
                       // コードを記述する。model1 というオブジェクト(インスタンス)を利用できる。
                       // ActionParameter のインスタンス p も利用できる。
                   }
               });
   }

TransactionTemplate クラスは Spring が提供する、プログラムコードによるトランザクション管理を行うための仕組みです。一般的に、TransactionCallback を実装した無名クラスを作成します。

非検査例外(RuntimeExceptionの派生クラス)が発生することにより doInTransactionWithoutResult() メソッドの処理が終了した場合は、トランザクションは自動的にロールバックされます。

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

customer モデルの詳細画面に独自ボタン(イベント名:Original1)を配置し、ボタン押下時に実行される処理として作成しています。

package jp.jasminesoft.wagby.controller.customer;

import java.io.IOException;

import javax.servlet.ServletException;

import jp.jasminesoft.jfc.ActionParameter;
import jp.jasminesoft.jfc.core.exception.BusinessLogicException;
import jp.jasminesoft.jfc.error.Jfcerror;
import jp.jasminesoft.jfc.service.JFCEntityService;
import jp.jasminesoft.util.ExcelFunction;
import jp.jasminesoft.wagby.app.jnews.JnewsHelper;
import jp.jasminesoft.wagby.model.jnews.Jnews;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

/**
 * ShowCustomerController のカスタマイズクラスです。
 *
 * @author JasmineSoft
 * @version $Revision$ $Date$
 */
@Controller
public class MyShowCustomerController extends ShowCustomerController {

    /** {@inheritDoc} */
    @Override
    public String do_original(ActionParameter p)
        throws IOException, ServletException, SecurityException {
        if ("Original1".equals(p.action)) {
            return do_myprocess1(p);
        }
        return null;
     }

    /**
     * オリジナル処理を実行します。
     * @param p {@link ActionParameter}
     * @return view
     */
    public String do_myprocess1(final ActionParameter p)
            throws IOException, ServletException {
        try {
            // Spring が提供する TransactionTemplate を使って、
            // @Transactional アノテーションを用いずにプログラム実装による
            // トランザクション機能を実現します。
            TransactionTemplate transactionTemplate = p.appctx.getBean(
                    "RequiredTransactionTemplate", TransactionTemplate.class);
            // トランザクション処理結果の戻り値が必要な場合は
            // TransactionCallback クラスを利用します。
            // 今回は戻り値が不要なので TransactionCallbackWithoutResult クラス
            // を使っています。
            transactionTemplate.execute(new TransactionCallbackWithoutResult() {
                /** {@inheritDoc} */
                @Override
                protected void doInTransactionWithoutResult(
                        TransactionStatus status) {
                    // トランザクションが開始された状態で本メソッドが実行され、
                    // メソッド内で Exception が発生すると自動的にロールバック
                    // されます。Exception が発生せずに本メソッドが正常終了する
                    // と自動的にコミットされます。
                    JnewsHelper helper = p.appctx.getBean(
                            "JnewsHelper", JnewsHelper.class);
                    @SuppressWarnings("unchecked")
                    JFCEntityService<Jnews, Integer> service
                            = (JFCEntityService<Jnews, Integer>) p.appctx
                                    .getBean("JnewsEntityService");
                    // 1件目のデータ登録
                    Jnews jnews = new Jnews();
                    helper.initialize(jnews, p);
                    jnews.setLimitdate(ExcelFunction.TODAY());
                    jnews.setTitle("お知らせ1");
                    service.insert(jnews);

                    // 2件目のデータ登録
                    jnews = new Jnews();
                    helper.initialize(jnews, p);
                    jnews.setLimitdate(ExcelFunction.TODAY());
                    jnews.setTitle("お知らせ2");
                    service.insert(jnews);
                }
            });
        } catch (Exception e) {
            Jfcerror error = new Jfcerror();
            error.setContent("エラーが発生しました。" + e.getMessage());
            p.errors.addJfcerror(error);
        }

        // 元の画面へ遷移する。
        String id = p.request.getParameter("customerid");
        return "redirect:/showCustomer.do?customerid=" + id;
    }
}
  • このサンプルコードは、開発者が用意した customer モデルの独自ボタン押下時に、システムが標準で提供する「お知らせ (jnews) モデル」への2件のデータ登録を行なうものです。
  • Spring が提供する TransactionTemplate を使って、@Transactional アノテーションを用いずにプログラム実装によるトランザクション機能を実現しています。
  • トランザクション処理結果の戻り値が必要な場合は TransactionCallback クラスを利用します。今回は戻り値が不要とし、TransactionCallbackWithoutResult クラスを使っています。
  • トランザクションが開始された状態で本メソッドが実行され、メソッド内で Exception が発生すると自動的にロールバックされます。Exception が発生せずに本メソッドが正常終了すると自動的にコミットされます。

接頭語 "My" を付与することで、自動生成されたServiceクラスを拡張することができます。

ここでは、在庫管理のトランザクション処理を実現するためのコード例を示します。業務のイメージは「リポジトリ > 業務ロジック > モデルをまたがる計算(トランザクション)」をお読みください。

この文中で説明したトランザクションスクリプトを Java コードで記述した例は次のとおりです。

package jp.jasminesoft.wagby.app.syukko;

import jp.jasminesoft.jfc.app.EntityHelper;
import jp.jasminesoft.jfc.core.exception.BusinessLogicException;
import jp.jasminesoft.jfc.service.JFCEntityService;
import jp.jasminesoft.wagby.model.syukko.Syukko;
import jp.jasminesoft.wagby.model.zaiko.Zaiko;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

/**
 * MySyukkoEntityService.
 *
 * @author JasmineSoft
 * @version $Revision$ $Date$
 */
public class MySyukkoEntityService extends SyukkoEntityService {

    /** ZaikoEntityService */
    @Autowired
    @Qualifier("ZaikoEntityService")
    protected JFCEntityService<Zaiko, Integer> zaikoEntityService;

    /** ZaikoHelper */
    @Autowired
    @Qualifier("ZaikoHelper")
    protected EntityHelper<Zaiko, Integer> zaikoHelper;

    /** {@inheritDoc} **/
    @Override
   protected void updateRelatedModelsInsertTransaction(Syukko syukko) {
       super.updateRelatedModelsInsertTransaction(syukko); // 既存処理の呼び出し

        // 在庫データの取得(第二引数をtrueとすることで悲観ロックを同時に行う)
        Zaiko zaiko = zaikoEntityService.findById(syukko.getShohinId(), true);

        // 業務処理 在庫チェック
        int suryou = zaiko.getSuryou();
        int syukkoNum = syukko.getSyukkoNum();
        if (suryou - syukkoNum < 0) {
            throw new BusinessLogicException("在庫 " + suryou + " に対して "
                    + syukkoNum + " を出庫しようとしました。");
        }

        // 業務処理 在庫数の調整処理
        zaiko.setSuryou(suryou - syukkoNum);
        zaikoEntityService.update(zaiko);

        // 更新処理の後処理(キャッシュの削除等)
        zaikoHelper.afterUpdate(zaiko,
                getActionParameterContainer().get());
    }
}
  • サービスクラスが、別のサービスクラスを利用することができます。必要なサービスクラスは Autowired アノテーションによって取得できます。このとき、同時にサービス名を Qualifier アノテーションで指定してください。
  • 上述の方法で自モデル以外のサービスクラスやヘルパクラスを取得する場合、型パラメータの第一引数は対象クラスになります。第二引数は主キーの型になります。整数型の場合は Integer、文字列型の場合は String などと指定します。
  • 更新系の処理を行った場合は、対象モデルのヘルパクラスの afterUpdate メソッドを呼び出してください。キャッシュの削除等、Wagbyが必要とする後処理をまとめて実行できます。(なお、自分のサービス、ここでは Syukko モデルについては記述不要です。自動生成されたコードで対応されているためです。)
  • 上の例では、findById メソッドの第二引数を true としています。この場合、処理のタイミングで悲観ロック処理を適用します。このあと、update メソッドを呼び出しているためロックは適切に解除されます。

トランザクション処理用メソッド

参照連動項目(参照先保存)および、外部キーモデルのために提供しているトランザクション処理用メソッドは次のとおりです。トランザクション処理を実現するためのカスタマイズコードはこれらのメソッドを利用してください。

登録処理時に関連データの更新を行う

updateRelatedModelsInsertTransaction(E)

更新処理時に関連データの更新を行う

updateRelatedModelsUpdateTransaction(E)

削除処理時に関連データの更新を行う

updateRelatedModelsDeleteTransaction(E)

外部キーモデル削除処理時に関連データの削除を行う

cascadeDelete(E)

なお、このタイミングで呼び出されるスクリプトを記述することもできます。登録/更新/削除時の関連データの処理は Java を使わなくともスクリプトで実現可能です。こちらの利用もご検討ください。[詳細...]R8.0.3

サブデータベースへ接続を行う場合は、org.hibernate.Session の取得方法が異なります。 Wagby に同梱されているクラス HibernateUtil の openSession メソッドの第一引数に、サブデータベースを指定します。

サブデータベース 指定名
サブデータベース1sessionFactory2
サブデータベース2sessionFactory3
サブデータベース3sessionFactory4

ActionParameterが利用できる場合

カスタマイズコードで (ActionParameter のインスタンス) "p" が利用できる場合は次のようになります。

org.hibernate.Session sess
   = jp.jasminesoft.jfc.app.HibernateUtil.openSession("sessionFactory2", p.appctx);

ActionParameterが利用できない場合

JSP などへカスタマイズコードを埋め込む場合は、ServletContext から (Springの) applicationContext を取得してください。

ServletContext sc = session.getServletContext();
WebApplicationContext appctx = WebApplicationContextUtils.getRequiredWebApplicationContext(sc);
org.hibernate.Session sess
   = jp.jasminesoft.jfc.app.HibernateUtil.openSession("sessionFactory2", appctx);