Java
Hibernate

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

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

動機

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

GenericDaoについての資料

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

GenericDaoを 非使用/使用 の比較

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

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

サンプルとコーディング

GenericDao(interface)

お手本通りinterfaceを定義する、アプリからデータベースに対して基本的な CRUD処理 を行うようにする。

  • 簡単な説明(ツッコミどころは明らかにあるのですが、面倒なので詳しくは書かない)。

    • CREATE: モデルのインスタンスを受け取ってデータベースにINSERT
    • READ: 主キーありならそれをキーにしてSELECT、無いなら全てSELECT
    • UPDATE: モデルのインスタンスを受け取ってデータベースにUPDATE
    • DELETE: 主キーを受け取ってデータベースからDELETE
  • T ← これはデータベースのテーブルにマップされたオブジェクトのクラスで、宣言時にわかります

  • PK ← これは主キーとなるもののクラスで、だいたいの場合 String, Integer, Long で宣言されるのではないでしょうか

    • 複数キー使いたいときはどうするんですかね
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; を宣言しておいて、コンストラクタでそのクラスの実体を受け取ります
    • 後で実際のクラスを使用するためのトリックです
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

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で生成しましょう。

ポイント

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で注入する(←すごい)
EmployeesDao.java
public interface EmployeesDao extends GenericDao<Employees, Integer> {
}

DaoのSpring設定

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

サンプル