Service/Daoクラス
最終更新日: 2021年3月8日
R8 | R9
ServiceクラスとDaoクラスはモデル毎に用意されます。
Serviceクラスは内部でDaoクラスを利用します。Serviceクラスはトランザクション境界となっており、実際のデータベース操作はDaoクラスが行います。Daoクラスはそれ以外にキャッシュの制御や問合せのためのCriteriaの作成を行います。
ORMフレームワークHibernateを利用したクラスとなっています。CRUD処理は一つのDaoクラスで実現します。
またデータベースへの問合せはHibernateが提供する「CriteriaクエリAPI」を使います。SQLを直接、生成しているところはありません。
Daoクラスの利用例を示します。
上の例では、customerDao.get メソッド、ならびにdeleteById メソッド呼び出し時に、第二引数を true としています。この場合、処理のタイミングで悲観ロック処理を適用します。ロックをかけた場合、同データが更新されれば、トランザクション終了のタイミングでロックは解除されますが、更新しない場合はロックが残ったままとなってしまいます。参照のみの処理で、更新する必要がないデータを取得する場合は、get メソッドの第二引数を false としてください。
トランザクション境界となるクラスです。インタフェースを実装したクラスでは、各メソッドに @Transactional アノテーションが付与されています。
メソッド開始時に自動的にトランザクションが開始されます。正常終了でコミットされます。Exception発生時には自動的にロールバックされます。
"リポジトリ > スクリプト > Service/Daoクラス、ヘルパ、SQLを利用する" では、スクリプトを使った場合の Service の利用例を説明しています。
EntityService, Dao, Helper はメソッドの外側で以下のようにインスタンス変数を宣言することで取得することができます。
各クラスの型パラメータの第一引数は対象クラスになります。
第二引数は主キーの型になります。
整数型の場合は Integer、文字列型の場合は String などと指定します。
SpringのApplicationContextインスタンスは、p.appctx で参照することができます。プログラム中では次のように扱うことができます。
EntityService は SpringのDIコンテナから取得します。これによって EntityService の動作で必要となる Dao や EntityHelper、ActoinParamterContainer などのインスタンスが自動的にセットされた状態で取得できます。
EntityService を用いたデータ操作のサンプルコードを紹介します。
検索条件を格納した「コンディションモデル」を finderContext にセットします。finderContext には CriteriaConverter が必要です。Criteria の詳細は次節で説明します。
主キーを指定してデータを1件取得することができます。
更新用メソッド (update) の第一引数にはストアモデルを指定します。第二引数は更新対象モデルがロック済みかどうかを指定します。第二引数に
Wagbyが提供する検索条件を格納するための「コンディションモデル」は、内部でHibernateが提供する「クライテリア」(Criteria)に変換されています。開発者はこのクライテリアを直接、操作したデータベース検索を行うことができます。この方法のメリットは、IDE (Eclipse) を使ったコード補完の実現と、Meta クラスの利用によるタイプミスの防止です。
クライテリアを利用したコード例を示します。
項目名、テーブル名、列名の文字列を一括で管理します。Metaクラスを通してこれらの値を取得します。
Wagbyは検索条件を保持するコンディションモデルを自動生成しています。コンディションモデルからCriteriaを取得するにはCriteriaConverterを使います。
CriteriaConverterの利用例を示します。
メソッドの外側で以下のようにインスタンス変数を宣言することで CriteriaConverter インスタンスの取得することもできます。
Daoクラスが提供するfindByCriteriaメソッドはページング処理に対応しています。さらに現在のページを記憶する FinderContext クラスも提供しています。
findByCriteriaメソッドの利用例を示します。
finderContextの利用例を示します。
Daoクラスが提供するcountメソッドの引数にfinderContextをセットすることで、件数を取得することができます。
Wagbyではパフォーマンス向上のために内部で積極的にキャッシュを用いています。
HibernateのInterceptor機能を利用しており、トランザクションコミットのタイミングでキャッシュをクリアするといった処理が自動的に行われます。
複合キークラスをストアモデルのインナークラスとして用意しています。
Daoクラスでは getCurrentSession メソッドを使ってデータベースセッションを取得することができます。これを使ってSQLを直接、記述することもできます。
自動生成されたDaoクラスをカスタマイズしたMyDaoクラスを用意し、データを取得(get)したタイミングでデータベースのストアドプロシージャを呼び出す例を紹介します。
ヘルパクラスの afterUpdate メソッドをオーバーライドして、このタイミングでストアドプロシージャ呼び出し(または何らかのSQL処理)を行うコードの例を紹介します。
Wagby では TransactionManager 経由で取得された Hibernate Session は自動的にクローズされますが、上記のように HibernateUtil.openSession() を使って取得した Hibernate Session はコード開発者が明示的にクローズ処理を行う必要があります。
上と同じ枠組みですが、SQLを呼び出す例も示します。(正確には SQL ではなく、Hibernate が提供する HQL です。)
詳細は Hibernate プログラミングとなるため、割愛します。
Wagbyは標準で Spring framework が提供する宣言的トランザクションを利用することができますが、これとは別に開発者が直接、トランザクション管理を行うことも可能です。(プログラマティックトランザクション)
具体的には Spring が提供する TransactionTemplate クラスを使って、トランザクション境界の中で動作するコードを記述することができます。
Desinger のスクリプト設定で「ヘルパ > 実行のタイミング : 登録(初期データ作成)」が用意されています。これは登録画面を開く前処理としてスクリプトを記述できますが、トランザクション外の処理となっています。
そこでヘルパクラスを拡張し、次のように TransactionTemplate クラスを利用することができます。モデル model1 を例にしています。
TransactionTemplate クラスは Spring が提供する、プログラムコードによるトランザクション管理を行うための仕組みです。一般的に、TransactionCallback を実装した無名クラスを作成します。
非検査例外(RuntimeExceptionの派生クラス)が発生することにより doInTransactionWithoutResult() メソッドの処理が終了した場合は、トランザクションは自動的にロールバックされます。
customer モデルの詳細画面に独自ボタン(イベント名:Original1)を配置し、ボタン押下時に実行される処理として作成しています。
TransactionCallbackWithoutResult や TransactionCallback 内部で Hibernate のセッションオブジェクトを取得する場合は、次のようにしてください。
接頭語 "My" を付与することで、自動生成されたServiceクラスを拡張することができます。
ここでは、在庫管理のトランザクション処理を実現するためのコード例を示します。業務のイメージは「リポジトリ > 業務ロジック > モデル間の計算(トランザクション)」をお読みください。
この文中で説明したトランザクションスクリプトを Java コードで記述した例は次のとおりです。
参照連動項目(参照先保存)および、外部キーモデルのために提供しているトランザクション処理用メソッドは次のとおりです。トランザクション処理を実現するためのカスタマイズコードはこれらのメソッドを利用してください。
サブデータベースへ接続を行う場合は、org.hibernate.Session の取得方法が異なります。
Wagby に同梱されているクラス HibernateUtil の openSession メソッドの第一引数に、サブデータベースを指定します。
カスタマイズコードで (ActionParameter のインスタンス) "p" が利用できる場合は次のようになります。
JSP などへカスタマイズコードを埋め込む場合は、ServletContext から (Springの) applicationContext を取得してください。
ServiceクラスとDaoクラス
なお R9 でも一部でプロセスビーンが残っているところがありますが、今後のバージョンアップで廃止予定です。
そのため、プロセスビーンを用いたカスタマイズは非推奨です。
Dao (Data Access Object)
// 主キー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);
ロックについて
EntityServiceにおけるトランザクション管理
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;
ApplicationContextから取得する
import jp.jasminesoft.jfc.service.JFCEntityService;
...
JFCEntityService<Customer, Integer> entityService =
(JFCEntityService<Customer, Integer>)p.appctx.getBean("CustomerEntityService");
true
の場合はロック済みです。false
の場合はロックされていない状態のモデルを(強制的に)保存します。
ワンポイント
EntityServiceのサンプルコード
コンディションモデルを使ったデータの検索
import jp.jasminesoft.jfc.service.JFCEntityService;
...
Customer[] customer_ary = null;
CustomerC cond = new jp.jasminesoft.wagby.model.customer_c.CustomerC();
((CustomerCHelper)p.appctx.getBean("CustomerCHelper")).initialize(cond, p);
cond.setName(name);// ここでは顧客名を検索条件に設定した。
JFCEntityService<Customer, String> entityService =
(JFCEntityService<Customer, String>)p.appctx.getBean("CustomerEntityService");
FinderContext<CustomerC> finderContext = new FinderContext<CustomerC>();
finderContext.setCondition(cond);
finderContext.setPageSize(-1);// 無制限
finderContext.setCriteriaConverter((CustomerCriteriaConverter)
p.appctx.getBean("CustomerCriteriaConverter"));
List<Customer> coll = entityService.find(finderContext);
if (coll != null && coll.size() > 0) {
customer_ary = (Customer[])coll.toArray(new Customer[0]);
}
データ1件の取得
import jp.jasminesoft.jfc.service.JFCEntityService;
...
Customer customer = null;
try {
JFCEntityService<Customer, Integer> entityService =
(JFCEntityService<Customer, Integer>)p.appctx.getBean("CustomerEntityService");
customer = entityService.findById(customerid, true);//主キーを指定。ロックを取得する。
} catch (Exception e) {
String errmsg =
DbBaseController.convertErrorMessage(e,
new String[][] {
{ "customerid", JFCUtils.getRValue("customer.customerid", p.locale) }
});
if (errmsg == null) {
errmsg = "";
}
Object[] ep = { errmsg };
p.errors.addJfcerror
(errorManager.getJfcerror("customer.error.dbaccess", ep, p.locale));
}
// このあと customer を更新することを想定。更新時に取得したロックは解放される。
// (省略)
更新
import jp.jasminesoft.jfc.service.JFCEntityService;
...
JFCEntityService<Customer, Integer> entityService =
(JFCEntityService<Customer, Integer>)p.appctx.getBean("CustomerEntityService");
// findById メソッドを経由してオブジェクトを取得、そのときにロックを取得した。
Customer customer = entityService.findById(customerid, true);//主キー
// customer オブジェクトの内容を変更
// (省略)
// 更新
entityService.update(customer);
//entityService.update(customer, false); 未ロックのオブジェクトを更新する場合。非推奨。
false
を指定すると他ユーザによるロック取得を無視して更新しますが、この方法は推奨できません。
上にあるように更新対象となるストアモデルを取得するときにロックをかけ、ロック済みのストアモデルをupdateメソッドの第一引数に指定するようにしてください。
クライテリアを利用する
// この方式で取得した criteria は、モデル定義で設定した暗黙条件も適用されない。
// (後述する CriteriaConverter を使う方式では、暗黙条件が適用される。)
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クラス
CriteriaConverter
// CriteriaConverter インスタンスを作成
CustomerCriteriaConverter converter
= (CustomerCriteriaConverter) p.appctx.getBean(
"CustomerCriteriaConverter");
// Condition を Criteria に変換
DetachedCriteria criteria = converter.convert(condition, sortKey);
// Criteria を使って検索を実行する。(daoオブジェクトは事前に用意されたものとする)
List<Customer> results = dao.findByCriteria(criteria);
// Service を利用することもできる。(serviceオブジェクトは事前に用意されたものとする)
//List<Customer> results = service.find(criteria);
/** CriteriaConverter インスタンスを作成 */
@Autowired
@Qualifier("CustomerCriteriaConverter")
protected CriteriaConverter<CustomerC> converter;
ページング
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<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);
件数の取得
// FinderContext インスタンスを作成
FinderContext<CustomerC> finderContext = new FinderContext<CustomerC>();
// Condition をセット
finderContext.setCondition(condition);
// CriteriaConverter をセット
finderContext.setCriteriaConverter(
(CustomerCriteriaConverter) p.appctx.getBean(
"CustomerCriteriaConverter"));
int size = dao.count(finderContext);
キャッシュ制御
複合キーの場合
zaiko モデルが複合キーの場合は次のような記述となります。
Zaiko.Key key = new Zaiko.Key();
key.setPkey1(xxx);/* xxx には、主キーの値が入ります */
key.setPkey2(yyy);/* yyy には、主キーの値が入ります */
Zaiko zaiko = zaikoEntityService.findById(key,true);/* 第二引数がtrueのときロックをかける */
Daoからストアドプロシージャを呼び出す
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);
}
}
ヘルパからストアドプロシージャを呼び出す
@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);
}
ヘルパからSQLを呼び出す
@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);
}
プログラムによるトランザクション管理
例 ヘルパクラスの拡張
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 も利用できる。
}
});
}
例 コントローラクラスの拡張(独自ボタン)
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;
}
}
TransactionCallback内でHibernateのセッションを取得する
org.hibernate.Session session = p.appctx.getBean("sessionFactory").getCurrentSession();
自動生成されたServiceを拡張する
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());
}
}
トランザクション処理用メソッド
登録処理時に関連データの更新を行う
updateRelatedModelsInsertTransaction(E)
更新処理時に関連データの更新を行う
updateRelatedModelsUpdateTransaction(E)
削除処理時に関連データの更新を行う
updateRelatedModelsDeleteTransaction(E)
外部キーモデル削除処理時に関連データの削除を行う
cascadeDelete(E)
サブデータベースへの Hibernate Session を取得する
サブデータベース
指定名
サブデータベース1 sessionFactory2 サブデータベース2 sessionFactory3 サブデータベース3 sessionFactory4 ActionParameterが利用できる場合
org.hibernate.Session sess
= jp.jasminesoft.jfc.app.HibernateUtil.openSession("sessionFactory2", p.appctx);
ActionParameterが利用できない場合
ServletContext sc = session.getServletContext();
WebApplicationContext appctx = WebApplicationContextUtils.getRequiredWebApplicationContext(sc);
org.hibernate.Session sess
= jp.jasminesoft.jfc.app.HibernateUtil.openSession("sessionFactory2", appctx);