ラベル Hibernate の投稿を表示しています。 すべての投稿を表示
ラベル Hibernate の投稿を表示しています。 すべての投稿を表示

2012年4月28日土曜日

例外処理

データベースアクセスの際に使われる決まり文句にはトランザクション境界を明示する begin, commit/rollback の他、例外を捕まえるための tyr {..} catch {..} [final {..}] があります。前者については @Transactional アノテーションや『トランザクションを“Declarative”に管理する』で紹介した“Declarative transaction management(宣言的トランザクション管理)”が使えます。一方、例外処理においても Spring Framework には便利な機能が用意されています。

HandlerExceptionResolver
その一つが HandlerExceptionResolver です。リファレンスの“16.11 Handling exceptions”には「Spring の HandlerExceptionResolver 実装は、コントローラー実行中に発生した予測不能の例外を処理する」とあります。

このインターフェースを使えば、Spring に DAO レイヤーから上げられた例外を特定のビューに解決させることができます。例えば以下のような感じです。

HibernateExceptionResolver.java
HandlerExceptionResolver を実装したカスタム例外リゾルバーです。同インターフェースに定義された resolveException() メソッドを実装しています。このメソッド内では、受け取った例外を instanceof で判別し、戻り値として ModelAndView に特定のビューをセットしています。必要とあらば例外情報をログに記録するなどの処理を挟むこともできます。このクラスでタッチしない例外の場合は null を返します。

尚、このクラス(HibernateExceptionResolver)では、抽象クラス AbstractHandlerExceptionResolver の作法を参考に HandlerExceptionResolver の他、Ordered を実装しています。order の値が大きいほど優先順位が低くなります。デフォルトとして Integer.MAX_VALUE を設定しています。
package wrider.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.hibernate.HibernateException;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

public class HibernateExceptionResolver 
    implements HandlerExceptionResolver, Ordered {
  
  private int order = Integer.MAX_VALUE;
  
  public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    ModelAndView mav = new ModelAndView();
    
    if (ex instanceof HibernateException) {
      mav.addObject("pageTitle", "Sorry");
      mav.setViewName("ex/dae");
      return mav;
    }
    else {
      return null;
    }
  }
  
  public int getOrder() {
    return this.order;
  }

}

上記リゾルバーを applicationContext.xml に登録します。
<!-- Exception Resolver -->
  <bean class="wrider.controller.HibernateExceptionResolver"/>

SimpleMappingExceptionResolver
もしも、これといった例外処理は必要なく、単純に特定のビューに解決するだけでいいのであれば SimpleMappingExceptionResolver が簡単です。以下のような感じで applicationContext.xml に SimpleMappingExceptionResolver を登録し、例外クラスとビュー名を設定するだけです。
<!-- Exception Resolve & Translate -->
  <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
      <map>
        <entry key="org.hibernate.HibernateException" value="ex/dae"></entry>
      </map>
    </property>
  </bean>

@ExceptionHandler
また、コントローラークラス/メソッドに @ExceptionHandler アノテーションを付ける方法もあります。以下のような感じです。

AccountController.java(抜粋)
HibernateException が上がってきたら HibernateExceptionHandler()メソッドで処理するように定義しています。
@ExceptionHandler(org.hibernate.HibernateException.class)
  public ModelAndView HibernateExceptionHandler(org.hibernate.HibernateException ex, HttpServletRequest request) {
    ModelAndView mav = new ModelAndView();
    mav.setViewName("ex/dae");
    return mav;
  }

DAO メソッドのスリム化
トランザクションや例外に関する設定、処理を本体コードの外側に定義することで、本体コードを大幅にスリム化できます。例えば、以前作った ProfileManagement.javagetUserProfile()メソッドは以下のようになります。

使用前
public UserProfile getUserProfile(IdCard idCard) {
    
    final String HQL = 
      "from UserProfile as up " + 
      "where up.email = :email and up.password = :password";
    
    /*
     * Boilerplate pattern
     */
    
    UserProfile result = null;
    Session session = txManager.getSessionFactory().getCurrentSession();
    Transaction tx = session.getTransaction();
    
    try {
      tx.begin();
      
      result = (UserProfile)session.createQuery(HQL)
            .setParameter("email", idCard.getEmail())
            .setParameter("password", idCard.getPassword())
            .uniqueResult();
      
      tx.commit();
      
    } catch (Exception e) {
      tx.rollback();
      System.out.println("error occured: " + e.toString());
    }
    
    return result;
    
  }

使用後
決まり文句の排除に加え、setParameter()メソッドで個々にパラメータをセットする代わりに、setProperties()メソッドを使い HQL のプレースホルダーに、IdCard オブジェクトのフィールド値を割り当てています。
public UserProfile getUserProfile(IdCard idCard) {
    
    final String HQL = 
      "from UserProfile as up " + 
      "where up.email = :email and up.password = :password";
    
    return (UserProfile)txManager.getSessionFactory().getCurrentSession()
            .createQuery(HQL)
            .setProperties(idCard)
            .uniqueResult();
  }

実験
試しに上記コードの where up.email = :email .. の部分を up.mail に変えて例外を発生させてみました。すると上のほうで紹介したどの方法でも、図のような「ごめんなさい画面」が表示されました。

ある処理に関連する処理を一つのコードにまとめて書くより、特定の処理を専門的に扱うコードの連携として設計した方が、一つ一つのコードの目的が明確になりますし、重複やミス、見落としの確率も下がることを実感しました。何より、作りたい本来の機能に集中できます。Spring のようなフレームワークを使うことの本質を垣間見たような気がします。

2012年4月27日金曜日

トランザクションを“Declarative”に管理する

データベースにアクセスする際の“boilerplate(決まり文句)”にはうんざりさせられます。しかし、begin, commit/rollback といった決まり文句は、トランザクションの境界線をはっきりさせるために必要です。また、例外を捕まえて rollback するために try {..} catch {..}を使います。これもまた面倒です。

Declarative Transaction Management
Spring Framework はそうした面倒を緩和してくれる仕掛けを持っています。“Declarative Transaction Management(宣言的トランザクション管理)”もその一つです。これを使うと、「どの例外が発生したら rollback するか(declarative rollback rules)」とか「ダーティリードやファントムリードを許すか(isolation level)」とか「現在のトランザクションをサスペンドして、新しいトランザクションを生成するか(transaction propagation)」といったことを“宣言”できます。

宣言は XML で記述する方法と、@Transactional アノテーションの属性として記述する方法があります。今回、もちろん @Transactional は使いますが、種々の設定は XML で行うことにします。

その前に『Hibernate でデータアクセス(3)』で作成した hibernate.cfg.xml から、以下の行を削除しておきます。

<property name="hibernate.current_session_context_class">thread</property>

というか、JTA を使わないのであればこの項目は厳禁みたいです。以下のような書き込みが初歩的な相談事として巷に溢れていました。

宣言
リファレンス“11.5.1 Understanding the Spring Framework's declarative transaction implementation”に、Spring の宣言的トランザクションは AOP Proxy を介して実現されているとあります。実際、AOP と同様に AdvicePointcut を定義して、それらを Advisor で結び付けるというのが基本になります。

では、11.5.2 Example of declarative transaction implementation 以降の例と説明に従いながらトランザクションを宣言していきます。

名前空間
まずは、<tx:advice/> などの要素を利用するための名前空間を追加します。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    :
  xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:aop="http://www.springframework.org/schema/aop"
    :
  xsi:schemaLocation="
    :
  http://www.springframework.org/schema/tx 
  http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
  http://www.springframework.org/schema/aop 
  http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
    :

<tx:advice/>
この advice にトランザクションに対する各種属性を記述していきます。トランザクション属性を適用したいメソッド名を <tx:method/> タグの name で指定します。デフォルト設定のままでいい場合は、その他の記述は必要ありません。例えば <tx:method name="*"/> みたいな感じです。
read-only
トランザクションがリードオンリーの場合は true にします。
timeout
トランザクションがタイムアウトするまでの時間を秒数で指定します。
rollback-for
ロールバックの切欠となる例外クラスを指定します。複数の場合はカンマ区切り。@Transactional アノテーションの説明には、Throwable のサブクラスでなければならないと記述されています。FQCN(完全修飾クラス名)で書いた方が無難かも。
no-rollback-for
rollback-for の逆の意味です。
propagation
トランザクションの伝播(propagation)に関する設定です。11.5.7 Transaction propagationREQUIRED, REQUIRES_NEW, NESTED の動きが解説されています。

また、IBM developerWorks の記事“Transaction strategies: Understanding transaction pitfalls”は、propagation と他の設定を組み合わせた際の動作を知る参考になると思います。同じ read-only + propagation.REQUIRED でも JDBC と JPA では内部の動きが違うんですね。

因みにこの記事にもある Unit of Work の考え方については Hibernate Core リファレンス“13.1.1. Unit of work”が参考になります。
isolation:
トランザクションの分離(isolation)レベルに関する設定です。コミット前のデータでもかまわない場合は READ_UNCOMMITTED, 最低限コミットされていなければならない場合は READ_COMMITTED, トランザクション内での繰り返しリードでデータが変わると困る場合は REPEATABLE_READ, 他のトランザクションの影響は絶対に許さない場合は SERIALIZABLE, その辺のことはデータソースに丸投げで構わない場合は DEFAULT という感じになると思います。ロックやパフォーマンスにも関わる部分なので慎重に考えたい項目です。

以上を勘案して次のように設定してみました。「change」で始まるメソッドでは、全ての例外が rollback のトリガーとなるよう設定していますが、もっと条件を細かくしてもいいかもしれません。
:
  <bean id="txManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager"
    p:sessionFactory-ref="sessionFactory">
  </bean>
  
  <tx:advice id="noRollBackTxAdvice" transaction-manager="txManager">
    <tx:attributes>
      <tx:method name="get*" read-only="true" isolation="READ_UNCOMMITTED" propagation="REQUIRED"/>
    </tx:attributes>
  </tx:advice>
  <tx:advice id="rollBackTxAdvice" transaction-manager="txManager">
    <tx:attributes>
      <tx:method name="change*" read-only="false" isolation="READ_COMMITTED" propagation="REQUIRES_NEW"
        rollback-for="java.lang.Throwable"/> 
      <tx:method name="*"/>
    </tx:attributes>
  </tx:advice>
    :

<aop:pointcut/>
pointcut と次の advisor は <aop:config/> 内に定義します。記述方法は AOP の規則に則ります。

以下のコードでは更新系トランザクションメソッドの pointcut に「wriderChangeOperation」、参照系に「wriderGetOperation」、ストアドプロシージャを呼び出すメソッドの pointcut に「wriderMakeOperation」という ID を割り当てています。

<aop:config>
    <aop:pointcut 
      expression="execution(* wrider.service.*.get*(..))" 
      id="wriderGetOperation"/>
    <aop:pointcut 
      expression="execution(* wrider.service.*.make*(..))" 
      id="wriderMakeOperation"/>
    <aop:pointcut 
      expression="execution(* wrider.service.*.change*(..))" 
      id="wriderChangeOperation"/>

<aop:advisor/>
advisor で上記 advice と pointcut を紐付けます。

<aop:advisor advice-ref="noRollBackTxAdvice" pointcut-ref="wriderGetOperation"/>
    <aop:advisor advice-ref="rollBackTxAdvice" pointcut-ref="wriderMakeOperation"/>
    <aop:advisor advice-ref="rollBackTxAdvice" pointcut-ref="wriderChangeOperation"/>
  </aop:config>

@Transactional アノテーションの有効化
リファレンス“11.5.6 Using @Transactional”に従って @Transactional を利用できるようにします。

<tx:annotation-driven transaction-manager="txManager"/>

次回は DAO レイヤーの boilerplate な部分を排除します。

2012年4月23日月曜日

Hibernate でデータアクセス(3)

前回、前々回に続き Hibernate と c3p0 を使うための設定を行います。

リファレンス“11.3 Understanding the Spring Framework transaction abstraction”の説明と例に倣いながら Data Source, Session Factory, Transaction Manager を applicationContext.xml に定義します。まずは Data Source から。

Data Source
Data Source には c3p0 コネクションプールを使用します。

pooledDataSource ビーン
applicationContext.xml 内に“pooledDetaSource”というビーンを定義し、class 属性で ComboPooledDataSource を指定しています。

このビーン定義内では基本的なプロパティ driverClass, jdbcUrl そしてdescription を設定しているだけです。詳細な設定はクラスパスに配置した c3p0-config.xml に記述しています(もちろん上記ビーン定義内に記述することも可能です)。
<!-- 'Pooled' DataSource Configuration -->
  <bean id="pooledDataSource" 
  class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"
    p:driverClass="com.mysql.jdbc.Driver"
    p:jdbcUrl="jdbc:mysql://[host]/[db]?useUnicode=true&characterEncoding=utf-8"
    p:description="C3P0 Pooled Data Source">
  </bean>

c3p0-config.xml
データベース接続に関するプロパティ(user, password)とコネクションプールの上限、下限に関するプロパティ(maxPoolSize, minPoolSize)を設定しています。maxStatements は、グローバル PreparedStatement キャッシュの上限です。
<?xml version='1.0' encoding='utf-8'?>
<c3p0-config>
  <default-config>
    <property name="user">demo</property>
    <property name="password">demo</property>
    <property name="initialPoolSize">2</property>
    <property name="minPoolSize">2</property>
    <property name="maxPoolSize">5</property>
    <property name="acquireIncrement">1</property>
    <!-- Global PreparedStatement Cache -->
    <property name="maxStatements">200</property>
  </default-config>
</c3p0-config>

設定できるプロパティの詳細は c3p0 サイトの“Appendix A: Configuration Properties”に記載されています。

Session Factory
Hibernate セッションを取得するための Session Factory を構成します。class 属性にLocalSessionFactoryBean を指定しています。

sessionFactory ビーン
DataSource に前述の pooledDataSource を指定しています。Hibernate 関連の詳しいプロパティを <property name="hibernateProperties"> で設定することもできますが、今回は configLocation で示したファイル(/WEB-INF/conf/hibernate.cfg.xml)に記述します。
<bean id="sessionFactory" scope="singleton"
    class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"
    p:dataSource-ref="pooledDataSource"
    p:configLocation="/WEB-INF/conf/hibernate.cfg.xml">
  </bean>

hibernate.cfg.xml
SQL の方言(dialect)を吸収する hibernate.dialect に MySQLDialect を指定しています。また hibernate.show_sql を true にすることで Hibernate が発行した SQL 文をコンソールに出力させます。今回はアノテーションで OR マッピングを定義しているので <mapping/>要素には、対象クラス(UserProfile と IdCard)を登録しているだけです。
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
  <session-factory>
    <!-- Hibernate properties -->
    <property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
    <property name="hibernate.show_sql">true</property>
    <property name="hibernate.current_session_context_class">thread</property>
    <!-- Mapping -->
    <mapping class="wrider.model.UserProfile"/>
    <mapping class="wrider.model.IdCard"/>
  </session-factory>
</hibernate-configuration>

Transaction Manager
Spring トランザクションの基点となる Transaction Manager の定義です。

txManager ビーン
HibernateTransactionManager を使います。sessionFactory プロパティは前述の sessionFactory ビーンを参照しています。
<bean id="txManager"
    class="org.springframework.orm.hibernate4.HibernateTransactionManager"
    p:sessionFactory-ref="sessionFactory">
  </bean>

ビュー
最後にビューです。ログインフォーム(login.jsp)では <form:form/> 要素の modelAttribute 属性で“idCard”、アカウント作成フォーム(registrationForm.jsp)では“userProfile”を指定しています。

login.jsp(抜粋)
:
  <form:form action="login.html" method="POST" modelAttribute="idCard">
    <fieldset>
    <legend>ログイン</legend>
    <div class="items">
      <label class="item" for="email">メールアドレス</label>
      <div class="item_body">
        <input type="text" id="email" name="email" value="${idCard.email}"/>
        <form:errors path="email" cssClass="red"/>
      </div>
    </div>
    :

registrationForm.jsp(抜粋)
:
  <form:form action="register.html" method="POST" modelAttribute="userProfile">
    :

図のログイン画面から認証情報を送信すると コンソールに Hibernate セッションで発行された SQL 文が Hibernate: select userprofil0_.pid as pid0_, ... from UserProfile userprofil0_ where userprofil0_.email=? and userprofil0_.password=? のような感じで表示されます。認証が成功すると welcome/home.html に遷移します。アカウント作成が成功した場合も同様です。

尚、MySQL との接続の様子はコマンドラインで
mysqladmin -u [user] -p extended-status | egrep "connect"
あるいはMySQL クライアントから
show status like '%connect%';

のように打ち込めば確かめられます。

以上でミッション終了です。一旦データベースへの接続環境が整えば、Spring (や Hibernate)が持つ色々な機能が利用できるようになります。個人的には AOP の仕掛けを利用したトランザクション管理に興味が沸きました。そうしたものを含めていつか試したいと思います。

2012年4月21日土曜日

Hibernate でデータアクセス(2)

前回の続きです。

サービスレイヤー
DAO レイヤーと連携しながら、コントローラーの要求に応じたサービスを提供するレイヤーです。DAO レイヤーと同様(というか Spring Framework における開発の基本?)、インターフェースを定義し、その実装クラスを作ります。

AccountService.java(インターフェース)
新規アカウントを作成する makeAccount() と UserProfile を取得する getProfile() を定義してます。

package wrider.service;

import java.util.Map;

import wrider.model.IdCard;
import wrider.model.UserProfile;

public interface AccountService {
  
  public Map makeAccount(UserProfile userProfile);
  
  public UserProfile getProfile(IdCard idCard);

}

AccountServiceImpl.java(実装クラス)
@Service アノテーションでこのクラスがサービスコンポーネントであることを示しています。尚、ProfileManager インターフェースの実装クラス ProfileManagerImple には、@Repository アノテーションを付けてリポジトリーコンポーネントであることを示しています。

[servlet-name]-servlet.xml で <context:component-scan/> が有効になっている場合、Spring Framework は、@Autowired されたフィールドの型に該当するインターフェースの実装クラスを探して DI します。実装クラスに付けた @Service や @Repository は、この時の目印になります。

ProfileManager フィールドに @Autowired を付けて、 ProfileManagerImpl を DI しています。

makeAccount()メソッドは、コントローラーから受け取った UserProfile オブジェクトを profileManager の callAddUserProfile()メソッドに渡し、処理結果を Map で受け取ります。getProfile()メソッドは、コントローラーから IdCard オブジェクトを受け取り、profileManager の getProfile()を呼び出す際、パラメーターとして渡します。

package wrider.service;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import wrider.dao.ProfileManager;
import wrider.model.IdCard;
import wrider.model.UserProfile;

@Service
public class AccountServiceImpl implements AccountService {
  
  @Autowired
  private ProfileManager profileManager;
  
  public Map<String, Object> makeAccount(UserProfile userProfile) {
    return this.profileManager.callAddUserProfile(userProfile);
  }
  
  public UserProfile getProfile(IdCard idCard) {
    return this.profileManager.getUserProfile(idCard);
  }
  
}

コントローラー
テーブルスキーマの定義から DAO レイヤー、サービスレイヤーと遡って、ようやくコントローラーに辿りつきました。『CGLIB Proxy で捕捉エリアを拡大』までに散々手を加えた AccountController クラスに、先述した AccountService を利用するメソッドを追加します。

AccountController.java(抜粋)
まず、@Autowired で AccountService を DI しています。/account/login.html および /account/register.html に対する GET リクエストを受けた場合は、単純に各 URL に対応するビュー名を返すだけです。
package wrider.controller;
    :
@Controller
@SessionAttributes({"idCard", "userProfile"})
public class AccountController {
  
  @Autowired
  private AccountService accountService;
  
  @ModelAttribute("idCard")
  public IdCard makeIdCard() {
    return new IdCard();
  }
  
  @ModelAttribute("userProfile")
  public UserProfile makeUserProfile() {
    return new UserProfile();
  }
    :
  @RequestMapping(value="/account/login.html", method=RequestMethod.GET)
  public String login() {
    return "account/login";
  }
  
    :
  @RequestMapping(value="/account/register.html", method=RequestMethod.GET)
  public String register() {
    return "account/registrationForm";
  }
    :
}

login()メソッド:
ログインフォームからの POST リクエストで実行される login()メソッドは、引数として フォームへの入力データを含む IdCard とバリデーション結果を格納する BindingResult のほか、@ModelAttribute でアノテートされた UserProfile オブジェクトを受け取ります。これに accountService.getProfile(idCard) の実行結果がバインドされます。UserProfile オブジェクトは、@SessionAttributes に登録されているため、セッションが有効である間は維持されます。認証成功後、取得した UserProfile を ModelAndView オブジェクトに登録して /welcome/home.html にリダイレクトします。

@RequestMapping(value="/account/login.html", method=RequestMethod.POST)
  public ModelAndView login(
      @ModelAttribute("userProfile") UserProfile userProfile,
      @Valid IdCard idCard,
      BindingResult br) {
    
    ModelAndView mav = new ModelAndView();
    
    if (br.hasErrors()) {
      mav.getModel().putAll(br.getModel());
      mav.setViewName("account/login");
    }
    else {
      userProfile = accountService.getProfile(idCard);
      if (userProfile == null) {
        mav.setViewName("account/login");
      }
      else {
        mav.addObject("userProfile", userProfile);
        mav.setViewName("redirect:../welcome/home.html");
      }
    }
    return mav;
  }

尚、引数の順序について若干の注意が必要です。リファレンス“16.3.3.1 Supported method argument types”の最後に『BindingResult(またはError)引数は、モデルオブジェクト(引数)の直後に続かなければならない。Spring は、先行する各モデルオブジェクトのために BindingResult インスタンスを生成する』という旨が書いてあります。確かに、BindingResult が @Valid でアノテートした引数の後に記述されていないと「バリデーション結果を入れる場所が無い」と怒られます。

register()メソッド:
accountService.makeAccount(userProfile) の実行結果は、HashMap型で返されます。キー「result」には成功(true)/失敗(false)、「newpid」には割り当てられた pid(profileId)が入ります。アカウント作成が成功した場合は /welcome/home.html にリダイレクトし、失敗の場合は再度、アカウント作成フォームを表示します。

@RequestMapping(value="/account/register.html", method=RequestMethod.POST)
  public ModelAndView register(
      @Valid UserProfile userProfile, 
      BindingResult br) {
    
    ModelAndView mav = new ModelAndView();
    
    if (br.hasErrors()) {
      mav.getModel().putAll(br.getModel());
      mav.setViewName("account/registrationForm");
    }
    else {
      
      HashMap<String, Object>notice = (HashMap<String, Object>)accountService.makeAccount(userProfile);
      
      if (Boolean.parseBoolean(notice.get(Items.RESULT).toString())) {
        mav.setViewName("redirect:/welcome/home.html");
      }
      else {
        mav.setViewName("account/registrationForm");
      }
      
    }
    
    return mav;
  }

次回は、Hibernate, c3p0 関連の設定を予定しています。

Hibernate でデータアクセス(1)

Spring のデータアクセスについてリファレンスを調べると“11.3 Understanding the Spring Framework transaction abstraction”に...
トランザクション抽象化のキーは、トランザクション戦略の概念である。トランザクション戦略は PlatformTransactionManager インターフェースにより定義される...

という記述があります。

HibernateTransactionManager
この PlatformTransactionManager の実装クラスを物色してみると HibernateTransactionManager というクラスがありました。説明を読んで特に惹きつけられたのが『このトランザクションマネージャは、transactional なデータアクセスのために単一の Hibernate SessionFactory を使用するアプリケーションに適しているが、トランザクション中の直接的な DataSource アクセスもサポートする。』の部分です。つまり、生 JDBC と Hibernate に一つの TransactionManager で対応できるということです。何だか面白そうです。

そこで今回はデータベースに MySQL 5.5 を使い、JDBC による Stored Procedure コールと、HQL(Hibernate Query Language)によるテーブルの参照を行います。

準備 - Hibernate ORM
Hibernate サイトから Hibernate ORM のリリースバンドルをダウンロードします。現時点の最新は hibernate-release-4.1.2.Final です。解凍すると hibernate-release-[version].Final/lib フォルダー下に 4 つのフォルダー(envers, jpa, optional, required)が現れます。今回は required 内の全てと、Connection pooling のために option/c3p0 内の全てを /WEB-INF/lib にコピーし、ビルドパスに登録します。

準備 - MySQL Connector/J
MySQL の Download サイトからのリンクを辿り mysql-connector-java-[version].zip をダウンロードし、解凍先フォルダーにある mysql-connector-java-[version]-bin.jar を上記と同様 /WEB-INF/lib にコピーし、ビルドパスに登録します。

準備 - Spring Framework(必要に応じて)
ところで Spring Framework 3.1.0 上で Hibernate と c3p0 を使ったときの不具合として #SPR-8924 が報告されています。同 3.1.1 で fix されているそうなので、必要ならば公式サイトから最新版を持ってきます。


テーブル
まずはテーブルの設計です。『DI による Validator の再利用』で作ったモデルクラスとコントローラーを雛形に、以下の CREATE TABLE 文で示すような単純なアカウント管理テーブルを考えてみました。

UserProfile.sql
create table UserProfile(
  pid mediumint not null autoutincrement unique,
  email          varchar(255) not null unique,
  password       varchar(255) not null,
  lastName       varchar(10) not null comment '名字',
  firstName      varchar(30) not null comment '名前',
  penName        varchar(255) comment '愛称',
  registeredDate date not null comment '登録日',
  lastLogin      timestamp not null comment '最終ログイン',
  constraint UserProfile_pkc primary key(pid),
  index UserProfile_idx1 using hash(email)
  );

主キーは pid で、mail にインデックスを指定しています。

ストアドプロシージャ
上記テーブルに対するアカウントの新規登録は以下のようなストアドプロシージャで行うことにします。

addUserProfile.sql
delimiter //
create procedure addUserProfile(
  IN inEmail varchar(255),
  IN inPassword varchar(255),
  IN inLastName varchar(10),
  IN inFirstName varchar(30),
  IN inPenName varchar(255),
  OUT result boolean,
  OUT newpid mediumint)
begin
  -- local valiable
  declare cnt int;
  
  -- cursor for checking prior to inserting received profile
  declare dispatch_cur cursor for
    select count(*) from UserProfile where email = inEmail;
  
  -- exit handler for ERROR: 1062 (ER_DUP_ENTRY)
  declare EXIT handler for SQLSTATE '23000'
    begin
      rollback;
      set result = false;
      set newpid = -1;
    end;
    
  start transaction;
  
  open dispatch_cur;
  fetch dispatch_cur into cnt;
  close dispatch_cur;
  
  if cnt = 0 then
    set result = true;
    insert into UserProfile set
      email = inEmail,
      password = inPassword,
      lastName = inLastName,
      firstName = inFirstName,
      penName = inPenName,
      registeredDate = now();
      
    select LAST_INSERT_ID() into newpid;
  else
    set result = false;
    set newpid = -1;
  end if;
  
  commit;
  
end//
delimiter ;

insert 前に email の重複チェックを行い、重複があれば OUT パラメータ result に false を、newpid には -1 をセットします。それでも ERROR: 1062 のエラーが発生したら EXIT HANDLER で捕まえてロールバックと上記と同様の OUT パラメーターの設定を行います。

モデルクラス
Hibernate ORM は、その名の通り ORM(Object-Relational Mapping)のフレームワークです。これからモデルクラス(IdCard, UserProfile)と上記テーブルのマッピングに取り掛かります。

IdCard.java
永続化のため Serializable インターフェースを implements しています。2 つのプロパティ(email, password)には、@Column アノテーションを付けて、テーブル側の unique, not null と合わせています。

また、クラス定義の先頭に @MappedSuperclass を付けています。Hibernate のリファレンス 5.1.6. Inheritance strategy の“5.1.6.4. Inherit properties from superclasses”にある注意書きよると、@MappedSuperclass としてマッピングされていないスーパークラスから継承したプロパティは無視される、とあります。

後でお見せしますがもう一つのモデルクラス UserProfile は、IdCard のサブクラスです。OR マッピングにおいてこの継承関係を有効にするため IdCard には @MappedSuperclass を付けたというわけです。

package wrider.model;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.MappedSuperclass;

import org.hibernate.validator.constraints.NotEmpty;

import wrider.annotation.CheckPassword;
import wrider.annotation.CheckEmail;

@MappedSuperclass
public class IdCard implements Serializable {
  
  private static final long serialVersionUID = 1L;
  
  @NotEmpty
  @CheckEmail
  @Column(unique = true, nullable = false)
  private String email;
  
  public String getEmail() {
    return this.email;
  }
  
  public void setEmail(String email) {
    this.email = email;
  }
  
  @NotEmpty
  @CheckPassword(min = 4, max = 32)
  @Column(unique = false, nullable = false)
  private String password;
  
  public String getPassword() {
    return this.password;
  }
  
  public void setPassword(String password) {
    this.password = password;
  }
  
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || !(o instanceof IdCard)) {
      return false;
    }
    
    IdCard other = (IdCard)o;
    if (email == null || password == null) {
      return false;
    }
    return email.equals(other.getEmail()) && password.equals(other.getPassword());
  }
}

UserProfile.java
前述の通りこのモデルクラスは IdCard を extends したものです。そのため UserProfile 自体に定義された 6 つのプロパティ(profileId, lastName, firstName, penName, registeredDate, lastLogin)に加え、継承した 2 つのプロパティ(email, password)も持つことになります。

後、profileId はテーブルの pid カラムに対応し、@Id @GeneratedValue(strategy = IDENTITY) アノテーションを付けることにより同カラムの auto_increment に対応しています。

package wrider.model;

import static javax.persistence.GenerationType.IDENTITY;
import static javax.persistence.TemporalType.DATE;

import java.io.Serializable;
import java.sql.Timestamp;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Temporal;

import org.hibernate.validator.constraints.NotEmpty;

import wrider.annotation.CheckPersonName;

@Entity
public class UserProfile extends IdCard implements Serializable {
  
  private static final long serialVersionUID = 1L;
  
  @Id
  @GeneratedValue(strategy = IDENTITY)
  @Column(name = "pid", unique = true, nullable = false)
  private long profileId;
  public long getProfileId() {
    return this.profileId;
  }
  public void setProfileId(long profileId) {
    this.profileId = profileId;
  }
  
  @NotEmpty
  @CheckPersonName(max = 10)
  @Column(unique = false, nullable = false)
  private String lastName;
  
  public String getLastName() {
    return this.lastName;
  }
  
  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
  
  @NotEmpty
  @CheckPersonName(max = 30)
  @Column(unique = false, nullable = false)
  private String firstName;
  
  public String getFirstName() {
    return this.firstName;
  }
  
  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }
  
  @CheckPersonName(max = 30)
  @Column(unique = false, nullable = true)
  private String penName;
  
  public String getPenName() {
    return this.penName;
  }
  
  public void setPenName(String penName) {
    this.penName = penName;
  }
  
  @Column(unique = false, nullable = false)
  @Temporal(DATE)
  private Date registeredDate;
  
  public Date getRegisteredDate() {
    return this.registeredDate;
  }
  
  public void setRegisteredDate(Date registeredDate) {
    this.registeredDate = registeredDate;
  }
  
  @Column(unique = false, nullable = false)
  private Timestamp lastLogin;
  
  public Timestamp getLastLogin() {
    return this.lastLogin;
  }
  
  public void setLastLogin(Timestamp lastLogin) {
    this.lastLogin = lastLogin;
  }
}


DAO レイヤー
具体的なデータアクセスを行うレイヤーです。Spring IoC の仕掛けを利用して、後述するサービスレイヤーと連携させるために、まずインターフェースを定義し、それに則って実装クラスを作るという手順と採ります。

ProfileManager.java(インターフェース)
2 つのメソッドを定義しています。ストアドプロシージャを呼び出して新しいアカウントを作成する callAddUserProfile()と UserProfile を参照する getUserProfile() です。

package wrider.dao;

import java.util.Map;

import wrider.model.IdCard;
import wrider.model.UserProfile;

public interface ProfileManager {
  
  public Map callAddUserProfile(UserProfile userProfile);
  
  public UserProfile getUserProfile(IdCard idCard);

}

ProfileManagerImpl.java(実装クラス)
ProfileManager インターフェースの実装クラスです。callAddUserProfile() は、DataSourceUtils を使って DataSource から Connection を取得し、CallableStatement でストアドプロシージャに対する IN/OUT パラメーターを設定し、実行しています。DataSource はトランザクションマネージャ(txManager) から取得したものです。尚、例外処理はごく簡易にしています。

一方 getUserProfile() は、トランザクションマネージャ(txManager)から取得した SessionFactory を介して Hibernate Session を取得後、createQuery()で HQL のプレースホルダーにパラメーターをセットし、クエリーを発行しています。

両メソッドとも @Autowired で txManager に注入した HibernateTransactionManager を使用して JDBC コネクションや Hibernate セッションを取得しています。

package wrider.dao;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.Types;
import java.util.HashMap;
import java.util.Map;

import org.hibernate.Session;
import org.hibernate.Transaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.orm.hibernate4.HibernateTransactionManager;
import org.springframework.stereotype.Repository;

import wrider.model.IdCard;
import wrider.model.UserProfile;

@Repository
public class ProfileManagerImpl implements ProfileManager {

  @Autowired
  private HibernateTransactionManager txManager;
  
  public UserProfile getUserProfile(IdCard idCard) {
    /*
     * Boilerplate pattern
     */
    final String HQL = 
      "from UserProfile as up " + 
      "where up.email = :email and up.password = :password";
    
    UserProfile result = null;
    Session session = txManager.getSessionFactory().getCurrentSession();
    Transaction tx = session.getTransaction();
    
    try {
      tx.begin();
      
      result = (UserProfile)session.createQuery(HQL)
            .setParameter("email", idCard.getEmail())
            .setParameter("password", idCard.getPassword())
            .uniqueResult();
      
      tx.commit();
      
    } catch (Exception e) {
      tx.rollback();
      System.out.println("error occured: " + e.toString());
    }
    
    return result;
  }
  
  public Map<String, Object> callAddUserProfile(UserProfile userProfile) {
    /*
     * Boilerplate pattern
     */
    final String STMT = "{call addUserProfile(?, ?, ?, ?, ?, ?, ?)}";
    
    HashMap<String, Object> result = new HashMap<String, Object>();
    
    try {
      Connection conn = DataSourceUtils.getConnection(txManager.getDataSource());
      
      CallableStatement cstmt = conn.prepareCall(STMT);
      
      cstmt.setString(Items.IN_EMAIL, userProfile.getEmail());
      cstmt.setString(Items.IN_PASSWORD, userProfile.getPassword());
      cstmt.setString(Items.IN_LASTNAME, userProfile.getLastName());
      cstmt.setString(Items.IN_FIRSTNAME, userProfile.getFirstName());
      cstmt.setString(Items.IN_PENNAME, userProfile.getPenName());
      
      cstmt.registerOutParameter(Items.RESULT, Types.BOOLEAN);
      cstmt.registerOutParameter(Items.NEWPID, Types.BIGINT);
      
      cstmt.execute();
      
      result.put(Items.RESULT, cstmt.getBoolean(Items.RESULT));
      result.put(Items.NEWPID, cstmt.getLong(Items.NEWPID));
      
      cstmt.close();
      conn.close();
      
    } catch (Exception e) {
      System.out.println("Error occured: " + e.toString());
    }
    
    return result;
  }

}


次回はサービスレイヤーから書く予定です。