Java Tutorial 第五堂(1)使用 spring-orm 整合 Hibernate


Java Tutorial 第四堂(3)Hibernate 與 JPA << 前情

在先前的課程中,我們 使用 spring-webmvc 框架 建立簡單的 Web 應用程式,使用 spring 相依注入 中進行依賴物件之注入,而在 Hibernate 與 JPA 中,既然認識了 ORM,那麼就也來使用 spring-orm 將之整合起來至 使用 spring 相依注入 撰寫的 DVDLibrary 專案之中吧!

練習 14:使用 spring-orm

這個練習要將練習 12 與練習 13 整合在一起。在 Lab 檔案的 exercises/exercise14 中有個 DVDLibrary 目錄,已經事先將練習 12 與練習 13 中一些可重用的程式碼(像是 Dvd.java、DvdDao.java 等)與設定檔(像是 build.gradle 等)準備好。

spring-orm 提到了 LocalSessionFactoryBean,用以簡化 Hibernate 的 SessionFactoy 之設定與建立,請開啟 dispatcher-servlet.xml,在 JDBCDataSourcebean 設定上增加 id 屬性為 dataSource,同時也增加 LocalSessionFactoryBean 之設定:
...
 <bean id="dataSource" class="org.hsqldb.jdbc.JDBCDataSource"
         p:url="jdbc:hsqldb:file:src/main/resources/db/dvd_library"
         p:user="codedata"
         p:password="123456"/>

 <bean class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
     <property name="dataSource" ref="dataSource" />
     <property name="packagesToScan" value="tw.codedata" />
     <property name="hibernateProperties">
         <props>
             <prop key="hibernate.hbm2ddl.auto">create</prop>
             <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
             <prop key="show_sql">true</prop>
             <prop key="format_sql">true</prop>
         </props>
     </property>
 </bean>
...

在這邊,透過 LocalSessionFactoryBeansetDataSource 注入 DataSource 實例,packageToScan 設定了自動掃描實體(Entity)物件的套件來源,這樣就會自動尋找設定了 @Entity 的類別取得 ORM 資訊。

練習 13 中的 DvdDirectorDvdDaoDirector 以及對應的 DAO 實作類別,都已經複製至練習 14 準備的專案之中,在動手修改 DvdController 之前,請先看一下原本的程式碼,例如 add 方法原先是這麼撰寫的:
...
Dvd dvd = new Dvd(title, year, duration, director);
getDvdDao().saveDvd(dvd);
m.addAttribute("dvd", dvd);
...

接下來你可能會打算將之改為:
...
DirectorDao directorDao = new DirectorDaoHibernateImpl(factory);
DvdDao dvdDao = new DvdDaoHibernateImpl(factory);

Director director = new Director("XD");
directorDao.saveDirector(director);

dvdDao.saveDvd(new Dvd("XD", 112, 1, director));
...

在 MVC 架構中,控制器應該是擔任請求轉發,而上頭的流程似乎混入了商務邏輯,也就是包括了 DirectorDvd 物件之建立、設定相依關係,以及分別利用 DirectorDaoDvdDao 分別儲存的邏輯,這並不建議,如果日後這些邏輯有了更複雜的變化,控制器就會開始面臨不斷的修改而增胖;另一方面,上面這種寫法,會讓控制器依賴在 DvdDaoDirectorDao 等多個介面之上。

因此,建議建立一個新的商務物件來處理相關流程,例如若有個 DvdLibraryService 提供了 addDvdallDvds 方法,就可以將 DvdController如下修改:
package tw.codedata;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
public class DvdController {
    private DvdLibraryService dvdLibraryService;

    @Autowired
    public void setDvdLibraryService(DvdLibraryService dvdLibraryService) {
        this.dvdLibraryService = dvdLibraryService;
    }

    public DvdLibraryService getDvdLibraryService() {
        return dvdLibraryService;
    }

    @RequestMapping("list")
    public String list(Model m) {
        m.addAttribute("dvds", getDvdLibraryService().allDvds());
        return "list";
    }

    @RequestMapping("add")
    public String add(
            @RequestParam("title") String title, 
            @RequestParam("year") Integer year,
            @RequestParam("duration") Integer duration,
            @RequestParam("director") String director,
            Model m) {
        Dvd dvd = getDvdLibraryService().addDvd(title, year, duration, director);
        m.addAttribute("dvd", dvd);
        return "success";
    }
}

以上也利用了 Spring 自動注入 DvdLibraryService,如上修改之後,DvdController 仍維持基本的請求轉發職責,也僅依賴在 DvdLibraryService 之上,而 DvdLibraryService 只是包括了原先打算寫在 DvdController 的邏輯:
package tw.codedata;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DvdLibraryService {
    private DirectorDao directorDao;
    private DvdDao dvdDao;

    @Autowired
    public void setDirectorDao(DirectorDao directorDao) {
        this.directorDao = directorDao;
    }

    @Autowired
    public void setDvdDao(DvdDao dvdDao) {
        this.dvdDao = dvdDao;
    }

    public DirectorDao getDirectorDao() {
        return directorDao;
    }

    public DvdDao getDvdDao() {
        return dvdDao;
    }

    public List<Dvd> allDvds() {
        return getDvdDao().allDvds();
    }

    public Dvd addDvd(String title, Integer year, Integer duration, String directorName) {

        Director director = new Director(directorName);
        getDirectorDao().saveDirector(director);
        Dvd dvd = new Dvd(title, year, duration, director);
        getDvdDao().saveDvd(dvd);
        return dvd;
    }
}

為了讓 Spring 可以自動在 DvdLibraryService 中注入 DirectorDaoDvdDao 實例,你要在 DirectorDaoHibernateImplDvdDaoHibernateImpl 中加註 @Service
package tw.codedata;

import com.google.common.base.Optional;
import java.util.List;
import org.hibernate.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DirectorDaoHibernateImpl implements DirectorDao {
    private SessionFactory sessionFactory;

    @Autowired
    public DirectorDaoHibernateImpl(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    ...

其中 SessionFactory 的建構,也是透過 @Autowired 標註,讓 Spring 自動將 dispatcher-servlet.xml 中設定的 LocalSessionFactoryBean 注入。DvdDaoHibernateImpl 也是增加相同的標註:
package tw.codedata;

import java.util.List;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DvdDaoHibernateImpl implements DvdDao {
    private SessionFactory sessionFactory;

    @Autowired
    public DvdDaoHibernateImpl(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

...

接下來就可以使用 gradle tomcatRunWar 來啟動程式,如果啟動時發生了 OutOfMemoryError: PermGen space 的錯誤,這是因為 JVM 的記憶體配置中,用來存放 .class 資訊的 PermGen 記憶體區段空間不足,可以在專案根目錄中建立一個 gradle.properties,撰寫以下資訊,增加 JVM 的 PermGen 區段空間大小:
org.gradle.jvmargs=-XX:MaxPermSize=256m