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

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

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

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

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 DbActionParameter 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 DbActionParameter 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 プログラミングとなるため、割愛します。

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

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

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

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

updateRelatedModelsInsertTransaction(E)

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

updateRelatedModelsUpdateTransaction(E)

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

updateRelatedModelsDeleteTransaction(E)

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

cascadeDelete(E)
Service/Daoクラスのカスタマイズを行う場合、ジャスミンソフトが提供する「カスタマイズ教育コース」の受講をご検討ください。カスタマイズ内容に応じた適切なアドバイスを実施しています。詳細はお問い合わせください。