2013-11-30

從 Spring + Hibernate 到 Spring Data JPA

抱著 Spring + Hibernate 的架構好長一段時間了,最近發現一個有趣的東西,Spring Data JPA,目前穩定版本是 1.4.2。

Duplicated code!!!

我的 Spring + Hibernate 的標準作法。

UserDao.java
public interface UserDao {

  User get(int uid);

  User getByAccount(String account);

  void add(User cmd);

  void update(User cmd);

  void delete(User cmd);

  List<User> list(UserQueryVO queryVO, int start, int pageSize);

  int count(UserQueryVO queryVO);
}

UserDaoImpl.java
@Repository("userDao")
@SuppressWarnings("serial")
public class UserDaoImpl implements UserDao, Serializable {

  private Logger log = LoggerFactory.getLogger(this.getClass());
  private SessionFactory sessionFactory;

  @Inject
  public UserDaoImpl(SessionFactory sessionFactory) {
    this.sessionFactory = sessionFactory;
  }

  private Session getSession() {
    return this.sessionFactory.getCurrentSession();
  }

  @Override
  public User get(int uid) {
    return (User) this.getSession().get(User.class, uid);
  }

  @Override
  public User getByAccount(String account) {
    Criteria c = this.getSession().createCriteria(User.class);
    c.add(Property.forName("account").eq(account));
    @SuppressWarnings("unchecked")
    List<User> list = c.list();
    if (list.isEmpty()) {
      return null;
    }
    return list.get(0);
  }

  @Override
  public void add(User cmd) {
    this.getSession().save(cmd);
  }

  @Override
  public void update(User cmd) {
    this.getSession().update(cmd);
  }

  @Override
  public void delete(User cmd) {
    this.getSession().delete(cmd);
  }

  @Override
  public List<User> list(UserQueryVO queryVO, int start, int pageSize) {
    Criteria c = this.createCriteria(queryVO);
    c.setFirstResult(start);
    c.setMaxResults(pageSize);
    @SuppressWarnings("unchecked")
    return c.list();
  }

  @Override
  public int count(UserQueryVO queryVO) {
    Criteria c = this.createCriteria(queryVO);
    c.setProjection(Projections.rowCount());
    Number cnt = (Number) c.uniqueResult();
    return cnt.intValue();
  }

  private Criteria createCriteria(UserQueryVO queryVO) {
    Criteria c = this.getSession().createCriteria(User.class);
    if (queryVO != null) {
      // ...
    }
    return c;
  }
}
很常見的一段程式,幾乎每個專案每個功能都會需要來上這麼一段。
User 來一次,Role 來一次,Category 再來一次,每加一個功能就多兩隻 java(僅就資料存取這部份),一個 interface 和一個 impl,程式大同小異,這是我很難忍受的事情。

以前不是沒有想過抽出抽象層,也有這麼做過。

UserDao.java
public interface UserDao extends AbstractDao {

  User getByAccount(String account);

}
事情看起來簡單多了,但每個功能還是要多兩隻程式,除此之後,還多了兩隻 abstract 的程式。

Spring Data JPA 是什麼?

以最白話的方式解釋。
this.userDao.save(obj);

List list = this.userDao.findAll();

User user = this.userDao.findByAccount(account);
我想從 method 名稱就可以知道這些程式在做什麼吧,既然做的這麼明顯,那電腦沒理由看不懂,這就是 Spring Data JPA 在做的事情!
  • 不需要 impl,只要一隻 interface 就夠了,嚇人吧。
  • 基本的 CRUD 當然也不需要定義了。

丟掉 impl

既然從 method 名稱就可以知道要做什麼事,電腦當然可以代為處理,那還要 impl 做什麼呢?

只要定義好 method 名稱,Spring Data JPA 會自動產出 impl。
public interface UserDao extends JpaRepository<User, Integer> {

  User findByAccount(String account);

}

內建 CRUD

幾乎每個功能都需要 CRUD,作法也一樣,當然可以內建並由電腦操刀。

org.springframework.data.repository.CrudRepository.java
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

 <S extends T> S save(S entity);

 <S extends T> Iterable<S> save(Iterable<S> entities);

 T findOne(ID id);

 boolean exists(ID id);

 Iterable<T> findAll();

 Iterable<T> findAll(Iterable<ID> ids);

 long count();

 void delete(ID id);

 void delete(T entity);

 void delete(Iterable<? extends T> entities);

 void deleteAll();
}

從 Spring + Hibernate 到 Spring Data JPA

  • 將 Hibernate 換成 JPA
  • 修改設定檔
  • 翻修 Dao
  • 丟掉 Impl

將 Hibernate 換成 JPA

目前 Spring Data 僅支援 JPA,不支援 Hibernate(?),所以得將 Hibernate 換成 JPA,幸好 Hibernate 早就擁抱 JPA,除非是用到特異功能,否則一般不難切換。

丟掉純 Hibernate 與 Spring ORM。
<dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-c3p0</artifactId>
 <version>4.2.5.Final</version>
</dependency>
<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-orm</artifactId>
 <version>3.2.5.RELEASE</version>
</dependency>
改用 JPA 的 Hibernate 實作版本,聽起來有點昏,簡單講,JPA 只是官方規格,並不是實作,而 Hibernate 有提供 JPA 的實作版本。
<dependency>
 <groupId>org.hibernate</groupId>
 <artifactId>hibernate-entitymanager</artifactId>
 <version>4.2.7.SP1</version>
</dependency>
以及 Spring Data JPA 與 Spring AOP。
<dependency>
 <groupId>org.springframework.data</groupId>
 <artifactId>spring-data-jpa</artifactId>
 <version>1.4.2.RELEASE</version>
</dependency> 
<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-aop</artifactId>
 <version>3.2.5.RELEASE</version>
</dependency>
以上是變動的部份,我本來就有用到的 spring-webmvc、spring-tx 與 javax.inject 則繼續留用。

修改設定檔

以下是修改 Spring 的 XML 設定檔。

首先是增加 JPA 的宣告。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:jpa="http://www.springframework.org/schema/data/jpa"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans 
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context 
    http://www.springframework.org/schema/context/spring-context.xsd
    http://www.springframework.org/schema/tx
    http://www.springframework.org/schema/tx/spring-tx.xsd
    http://www.springframework.org/schema/data/jpa
    http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
再來是修改搜尋 annoatation package 的位置,原本需要搜尋 model、service 與 dao 三個 package。
<context:component-scan base-package="idv.neil.model,idv.neil.service,idv.neil.dao" />
改成只要搜尋 service 一個 package 就可以了,model 交給 EntityManager 負責,dao 交給 Spring Data JPA 負責。
<context:component-scan base-package="idv.neil.service" />
再來是拿掉 Hibernate 專用的 sessionFactory 宣告。
<bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
 <property name="configLocation" value="classpath:hibernate.cfg.xml" />
</bean>
換上 EntityManager 的宣告,這是折磨我最久的地方,因為跟 Hibernate 設定真的差很大。
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
 <property name="driverClassName" value="com.mysql.jdbc.Driver" />
 <property name="url">
  <value><![CDATA[jdbc:mysql://localhost/SDJ?useUnicode=true&characterEncoding=utf-8]]></value>
 </property>
 <property name="username" value="..." />
 <property name="password" value="..." />
</bean>
<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
 <property name="dataSource" ref="dataSource" />
 <property name="packagesToScan" value="idv.neil.model" />
 <property name="jpaVendorAdapter">
  <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
 </property>
 <property name="jpaProperties">
  <props>
   <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</prop>
  </props>
 </property>
</bean>
再來是修改 TransactionManager 的設定,拿掉 Hibernate 版本的 TransactionManager 。
<bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
 <property name="sessionFactory" ref="sessionFactory" />
</bean>
換上 JPA 版本的 TransactionManager。
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
 <property name="entityManagerFactory" ref="emf" />
</bean>
最後加上 Spring Data JPA 的設定就好了。
<jpa:repositories base-package="idv.neil.dao" />
翻修 Dao
public interface UserDao extends JpaRepository<User, Integer> {

  User findByAccount(String account);

}
丟掉  Impl
就勇敢的殺掉吧!

最後只調整一下所有呼叫 UserDao 的地方,API 參考前面的 org.springframework.data.repository.CrudRepository.java 就可以了。

想要多了解一點,請看 More in Spring Data JPA 1.5.0 M1
---
---
---

1 則留言: