2012年4月21日土曜日

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;
  }

}


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

0 件のコメント:

コメントを投稿