GenericDaoとHibernateでパラメーター多相な仕組みを作ってみる

一回調べたけどお蔵入りになったのでQiitaに公開しとく

# 動機

* 同じコード、SQLをたくさん書きたくない
* RailsのActive RecordみたいなものをJavaでやってみたい

## GenericDaoについての資料

* [IBM developerWorks - DAOを繰り返すな！](https://www.ibm.com/developerworks/jp/java/library/j-genericdao/)
* [Generic DAO pattern with JDK 5.0](http://web.archive.org/web/20061010173637/http://blog.hibernate.org/cgi-bin/blosxom.cgi/2005/09/08)

結構昔の記事なので、実用性とかはよくわからない

## GenericDaoを 非使用/使用 の比較

実戦投入したことがないので予測です

| GenericDaoを？    | 使用する          | 使用しない          |
|:-----------------|------------------:|:------------------:|
| **ボイラープレートコード**（言語仕様上省く事ができない定型的なコード） | 減らせる | 頑張って書いて |
| SQLのチューニング | できる | できる |
| デバッグ          | めんどくさい | めんどくさい |

# サンプルとコーディング

## GenericDao（interface）

お手本通りinterfaceを定義する、アプリからデータベースに対して基本的な [CRUD処理](https://ja.wikipedia.org/wiki/CRUD) を行うようにする。

* 簡単な説明（ツッコミどころは明らかにあるのですが、面倒なので詳しくは書かない）。
    * **CREATE**: モデルのインスタンスを受け取ってデータベースにINSERT
    * **READ**: 主キーありならそれをキーにしてSELECT、無いなら全てSELECT
    * **UPDATE**: モデルのインスタンスを受け取ってデータベースにUPDATE
    * **DELETE**: 主キーを受け取ってデータベースからDELETE



* `T` ←　これはデータベースのテーブルにマップされたオブジェクトのクラスで、宣言時にわかります
* `PK` ←　これは主キーとなるもののクラスで、だいたいの場合 `String, Integer, Long` で宣言されるのではないでしょうか
    * 複数キー使いたいときはどうするんですかね

```java
import java.io.Serializable;
import java.util.List;

/**
 * This is the article 10 years ago, we should follow this
 * @see https://www.ibm.com/developerworks/jp/java/library/j-genericdao/
 */
public interface GenericDao<T, PK extends Serializable> {

	/** Persist the newInstance object into database */
	PK create(T newInstance);

	/**
	 * Retrieve an object that was previously persisted to the database using
	 * the indicated id as primary key
	 */
	T read(PK id);
	List<T> read();

	/** Save changes made to a persistent object. */
	void update(T transientObject);

	/** Remove an object from persistent storage in the database */
	void delete(PK id) throws Exception;
	void delete(T persistentObject) throws Exception;
}
```

## GenericDaoHibernateImpl(class)

* Springと連携して`SessionFactory` はDIで入れてもらいます
* `sessionFactory.getCurrentSession()` することで、取得したセッションを途中から使うことができるようにします
* `private Class<T> type;` を宣言しておいて、コンストラクタでそのクラスの実体を受け取ります
  * 後で実際のクラスを使用するためのトリックです

```java:GenericDaoHibernateImpl.java
import java.io.Serializable;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

public class GenericDaoHibernateImpl<T, PK extends Serializable> implements GenericDao<T, PK> {

	private Class<T> type;

	@Autowired
	private SessionFactory sessionFactory;

	public SessionFactory getSessionFactory() {
		return sessionFactory;
	}

	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	public GenericDaoHibernateImpl(Class<T> type) {
		this.type = type;
	}

	// Not showing implementations of getSession() and setSessionFactory()
	private Session getSession() {
		Session session = sessionFactory.getCurrentSession();
		return session;
	}

	@Transactional(readOnly = false, rollbackFor = RuntimeException.class)
	public PK create(T o) {
		return (PK) getSession().save(o);
	}

	@Transactional(readOnly = false, rollbackFor = RuntimeException.class)
	public void update(T o) {
		getSession().update(o);
	}

	@Transactional(readOnly = true)
	public T read(PK id) {
		return (T) getSession().get(type, id);
	}

	@SuppressWarnings("unchecked")
	@Transactional(readOnly = true)
	public List<T> read() {
		return (List<T>) getSession().createCriteria(type).list();
	}

	@Transactional(readOnly = false, rollbackFor = RuntimeException.class)
	public void delete(PK id) {
		T o = getSession().load(type, id);
		getSession().delete(o);
	}

	@Transactional(readOnly = false, rollbackFor = RuntimeException.class)
	public void delete(T o) {
		getSession().delete(o);
	}
}
```

## EmployeesDao - Model層サンプル

では実際にモデルと連携するためのDao(Data Access Object)を書きます

* サンプルとしてMySQLの以下のようなテーブルからCRUD処理を行うDaoを作ってみます
  * `emp_no` が主キーなので、これでSELECTなどできるはずです

### SQL

```sql
CREATE TABLE employees (
    emp_no      INT             NOT NULL,  -- UNSIGNED AUTO_INCREMENT??
    birth_date  DATE            NOT NULL,
    first_name  VARCHAR(14)     NOT NULL,
    last_name   VARCHAR(16)     NOT NULL,
    gender      ENUM ('M','F')  NOT NULL,  -- Enumeration of either 'M' or 'F'  
    hire_date   DATE            NOT NULL,
    PRIMARY KEY (emp_no)                   -- Index built automatically on primary-key column
                                           -- INDEX (first_name)
                                           -- INDEX (last_name)
);
```

### Model層

O/R Mapするためのクラスを書きましょう。Getter/Setterを書くのは止めてlombokで生成しましょう。

ポイント

* lombokで使用されるアノテーション `@Data` を使うとGetter/Setterが自動生成される
* JPAのアノテーションを使う必要がある、詳しくは以下のサイト 
  * [JPA関連アノテーションの基本として-その1-](http://etc9.hatenablog.com/entry/20090830/1251606823)
  * [JPA関連アノテーションの基本として-その2-](http://etc9.hatenablog.com/entry/20090831/1251728605)
* `Gender` については SQLでENUMが使用されているので、別クラスに分ける必要がある


```java:Employees.java
import java.io.Serializable;
import java.sql.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

import lombok.Data;
import lombok.NoArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@Table(name = "employees")
public class Employees implements Serializable {

	private static final long serialVersionUID = 1L;

	@Id
	@Column(name = "emp_no", unique = true)
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer empNo;

	@Column(name = "birth_date")
	private Date birthDate;
	
	@Column(name = "first_name")
	private String firstName;
	
	@Column(name = "last_name")
	private String lastName;
	
	@Column(name = "gender")
	@Enumerated(EnumType.STRING)
	private Gender gender;
	
	@Column(name = "hire_date")
	private Date hireDate;
}
```

### Dao

* 実際のDaoクラス
    * やっと楽できた感じがする
    * Daoをinterfaceとして用意しておき、実装はSpringで注入する（←すごい）

```java:EmployeesDao.java
public interface EmployeesDao extends GenericDao<Employees, Integer> {
}
```

### DaoのSpring設定

* DaoとSpring AOPとHibernateを設定するための前準備です
  * 本質的に必要なのは、 `<bean id="employeesDao" parent="abstractDao">` の部分のみです

```xml:applicationContext.xml
	<!-- DataSourceの設定 -->
	<bean id="myDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="${jdbc.driverClassName}" />
		<property name="url" value="${jdbc.url}" />
		<property name="username" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
	</bean>

	<!-- sessionFactory -->
	<bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
		<!-- 上で設定したDataSource -->
		<property name="dataSource" ref="myDataSource" />

		<!-- モデルクラスがある名前空間を指定 -->
		<property name="packagesToScan" value="package.to.your.models" />

		<!-- Hibernateの接続設定 -->
		<property name="hibernateProperties">
			<props>
				<prop key="dialect">org.hibernate.dialect.MySQLDialect</prop>
				<!-- ここは本番でもテストでもtrueにしておかないとデバッグできなさそう -->
				<prop key="show_sql">true</prop>
				<prop key="format_sql">true</prop>
				<prop key="connection.CharSet">utf8</prop>
				<prop key="connection.characterEncoding">utf8</prop>
				<prop key="connection.useUnicode">true</prop>
			</props>
		</property>
	</bean>

	<!-- transactionManagerを使う -->
	<tx:annotation-driven />
	<bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
		<property name="sessionFactory" ref="mySessionFactory" />
	</bean>

	<!-- GenericDaoHibernateImpl に 上で作った sessionFactory を注入する -->
	<bean id="abstractDaoTarget" class="jp.gr.java_conf.hangedman.dao.GenericDaoHibernateImpl" abstract="true">
		<property name="sessionFactory">
			<ref bean="mySessionFactory" />
		</property>
	</bean>

	<!-- Spring AOPを使って実装を注入する準備 -->
	<bean id="abstractDao" class="org.springframework.aop.framework.ProxyFactoryBean"
		abstract="true">
	</bean>

	<!-- Dao, Modelを設定、やっと終わり。ここから下の<bean>タグはDaoごとに必要か -->
	<bean id="employeesDao" parent="abstractDao">
		<!-- You need to configure the interface for Dao -->
		<property name="proxyInterfaces">
			<value>jp.gr.java_conf.hangedman.dao.EmployeesDao</value>
		</property>
		<property name="target">
			<bean parent="abstractDaoTarget">
				<constructor-arg>
					<value>jp.gr.java_conf.hangedman.models.Employees</value>
				</constructor-arg>
			</bean>
		</property>
	</bean>
```

# サンプル

* [Github generic-dao-hibernate](https://github.com/hangingman/generic-dao-hibernate)