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,在 JDBCDataSource 的 bean 設定上增加 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>
...在這邊,透過
LocalSessionFactoryBean 的 setDataSource 注入 DataSource 實例,packageToScan 設定了自動掃描實體(Entity)物件的套件來源,這樣就會自動尋找設定了 @Entity 的類別取得 ORM 資訊。
      練習 13 中的
Dvd、Director、DvdDao、Director 以及對應的 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 架構中,控制器應該是擔任請求轉發,而上頭的流程似乎混入了商務邏輯,也就是包括了
Director、Dvd 物件之建立、設定相依關係,以及分別利用 DirectorDao、DvdDao 分別儲存的邏輯,這並不建議,如果日後這些邏輯有了更複雜的變化,控制器就會開始面臨不斷的修改而增胖;另一方面,上面這種寫法,會讓控制器依賴在 DvdDao、DirectorDao 等多個介面之上。
      因此,建議建立一個新的商務物件來處理相關流程,例如若有個
DvdLibraryService 提供了 addDvd 與 allDvds 方法,就可以將 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 中注入 DirectorDao 與 DvdDao 實例,你要在 DirectorDaoHibernateImpl 與 DvdDaoHibernateImpl 中加註 @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
