在 Spring 2.0 中開始使用 JPA

工程 | Mark Fisher | 2006年5月30日 | ...

這篇部落格文章的目的是提供一個簡單的分步指南,介紹如何在 Spring Framework 的 獨立 環境中開始使用 JPA。雖然 JPA 規範最初是作為 EJB 3.0 的 持久化機制提出的,但幸運的是,人們認識到任何這樣的機制實際上都應該能夠持久化簡單的 POJO。因此,只需在 classpath 中包含少量 JAR 包和幾個 Spring 配置的 bean,你就可以在你喜歡的 IDE 中開始嘗試使用 JPA 程式碼了。我將使用 Glassfish JPA - 它是參考實現,基於 Oracle 的 TopLink ORM 框架。

初步設定

確保你正在使用 Java 5(JPA 和 EJB 3.0 的先決條件)。

從以下地址下載 glassfish JPA jar 包:https://glassfish.dev.java.net/downloads/persistence/JavaPersistence.html(注意:我使用了“V2_build_02” jar 包,但任何後續版本也應該可以工作。)

要從“installer” jar 包中解壓出 jar 包,執行java -jar glassfish-persistence-installer-v2-b02.jar(這是接受許可協議所必需的)

新增toplink-essentials.jar到你的 classpath

新增包含資料庫驅動程式的 JAR 包(我在示例中使用了 hsqldb.jar 1.8.0.1 版,但要適應其他資料庫只需要進行少量更改)。

使用 2.0 M5 版本新增以下 Spring JAR 包(可在此處獲取:http://sourceforge.net/project/showfiles.php?group_id=73357)。

  • spring.jar
  • spring-jpa.jar
  • spring-mock.jar

最後,也將這些 jar 包新增到你的 classpath

  • commons-logging.jar
  • log4j.jar
  • junit.jar

程式碼 - 領域模型

本例將基於一個故意簡化、僅包含 3 個類的領域模型。請注意註解的使用。使用 JPA 時,可以選擇使用註解或 XML 檔案來指定物件關係對映元資料,甚至可以結合使用這兩種方法。在這裡,我選擇只使用註解,在領域模型程式碼列表之後會立即提供簡要說明。

首先,Restaurant


package blog.jpa.domain;

import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OneToOne;

@Entity
public class Restaurant {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  private String name;

  @OneToOne(cascade = CascadeType.ALL)
  private Address address;

  @ManyToMany
  @JoinTable(inverseJoinColumns = @JoinColumn(name = "ENTREE_ID"))
  private Set<Entree> entrees;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Address getAddress() {
    return address;
  }

  public void setAddress(Address address) {
    this.address = address;
  }

  public Set<Entree> getEntrees() {
    return entrees;
  }

  public void setEntrees(Set<Entree> entrees) {
    this.entrees = entrees;
  }

}

其次,Address


package blog.jpa.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Address {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  @Column(name = "STREET_NUMBER")
  private int streetNumber;

  @Column(name = "STREET_NAME")
  private String streetName;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public int getStreetNumber() {
    return streetNumber;
  }

  public void setStreetNumber(int streetNumber) {
    this.streetNumber = streetNumber;
  }

  public String getStreetName() {
    return streetName;
  }

  public void setStreetName(String streetName) {
    this.streetName = streetName;
  }

}

第三,Entree


package blog.jpa.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Entree {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;

  private String name;

  private boolean vegetarian;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public boolean isVegetarian() {
    return vegetarian;
  }

  public void setVegetarian(boolean vegetarian) {
    this.vegetarian = vegetarian;
  }

}

如你所見,並非所有持久化欄位都添加了註解。JPA 使用預設值(例如使用與屬性名完全匹配的列名),因此在許多情況下你無需明確指定元資料。然而,你仍然可以選擇這樣做,以提供更充分的自文件化程式碼。注意,在Entree類中,我沒有對 String 屬性“name”或 boolean 屬性“vegetarian”使用註解。然而,在Address類中,我使用了註解,因為我想為資料庫中的列設定一個非預設名稱(例如,我選擇了“STREET_NAME”,而預設值是“STREETNAME”)。

當然,任何 ORM 機制最重要的特性之一是如何指定物件之間的關係到其資料庫對應項的對映。在Restaurant類中,有一個@OneToOne註解來描述與一個Address的關係,以及一個@ManyToMany註解來描述與Entree類的成員之間的關係。由於這些其他類的例項也由EntityManager管理,可以指定“級聯”規則。例如,當刪除一個Restaurant被刪除時,相關的Address也會被刪除。稍後,你將看到針對此場景的測試用例。

最後,看看 @Id 註解以及為 ID 的 @GeneratedValue 指定的“策略”。這些元資料用於描述 主鍵 生成策略,主鍵生成策略反過來控制資料庫中的標識。

要了解更多關於這些以及其他 JPA 註解的資訊,請查閱 JPA 規範 - 實際上它是 JSR-220 的一個子集。

程式碼 - 資料訪問層

為了訪問領域模型的例項,最好建立一個通用介面,隱藏底層持久化機制的所有細節。這樣,如果以後切換到 JPA 以外的其他機制,將不會影響架構。這也使得測試服務層更加容易,因為它支援建立該資料訪問介面的 stub 實現,甚至動態 mock 實現。

這是介面。請注意,它不依賴於任何 JPA 或 Spring 類。事實上,這裡除了核心 Java 類之外的唯一依賴是我的領域模型類(在這種簡單情況下,只有一個 -Restaurant):


package blog.jpa.dao;

import java.util.List;
import blog.jpa.domain.Restaurant;

public interface RestaurantDao {

  public Restaurant findById(long id);

  public List<Restaurant> findByName(String name);

  public List<Restaurant> findByStreetName(String streetName);

  public List<Restaurant> findByEntreeNameLike(String entreeName);

  public List<Restaurant> findRestaurantsWithVegetarianEntrees();

  public void save(Restaurant restaurant);

  public Restaurant update(Restaurant restaurant);

  public void delete(Restaurant restaurant);

}

對於這個介面的實現,我將擴充套件 Spring 的JpaDaoSupport類。它提供了一個便捷方法來獲取JpaTemplate。如果你曾經將 Spring 與 JDBC 或其他 ORM 技術一起使用,那麼你可能對這種方法非常熟悉。

需要注意的是,使用JpaDaoSupport是可選的。可以直接構造一個JpaTemplate,只需提供EntityManagerFactory給其建構函式即可。實際上,JpaTemplate本身也是可選的。如果你不想將 JPA 異常自動轉換為 Spring 的執行時異常層次結構,那麼可以完全避免使用JpaTemplate。在這種情況下,你可能仍然對 Spring 的EntityManagerFactoryUtils類感興趣,它提供了一個方便的靜態方法來獲取共享的(因此也是事務性的)EntityManager.

這是實現程式碼


package blog.jpa.dao;

import java.util.List;
import org.springframework.orm.jpa.support.JpaDaoSupport;
import blog.jpa.domain.Restaurant;

public class JpaRestaurantDao extends JpaDaoSupport implements RestaurantDao {

  public Restaurant findById(long id) {
    return getJpaTemplate().find(Restaurant.class, id);
  }

  public List<Restaurant> findByName(String name) {
    return getJpaTemplate().find("select r from Restaurant r where r.name = ?1", name);
  }

  public List<Restaurant> findByStreetName(String streetName) {
    return getJpaTemplate().find("select r from Restaurant r where r.address.streetName = ?1", streetName);
  }

  public List<Restaurant> findByEntreeNameLike(String entreeName) {
    return getJpaTemplate().find("select r from Restaurant r where r.entrees.name like ?1", entreeName);
  }

  public List<Restaurant> findRestaurantsWithVegetarianEntrees() {
    return getJpaTemplate().find("select r from Restaurant r where r.entrees.vegetarian = 'true'");
  }

  public void save(Restaurant restaurant) {
    getJpaTemplate().persist(restaurant);
  }

  public Restaurant update(Restaurant restaurant) {
    return getJpaTemplate().merge(restaurant);
  }

  public void delete(Restaurant restaurant) {
    getJpaTemplate().remove(restaurant);
  }

}

服務層

由於這裡的目的是專注於資料訪問層的 JPA 實現,服務層被省略了。顯然,在實際場景中,服務層將在系統架構中發揮關鍵作用。它將是劃分事務的地方——通常,事務會在 Spring 配置中以宣告方式劃分。在下一步檢視配置時,你會注意到我提供了一個“transactionManager” bean。基礎測試類使用它來自動將每個測試方法包裝在事務中,並且它與用於將服務層方法包裝在事務中的“transactionManager”是同一個。關鍵點在於,資料訪問層中沒有事務相關的程式碼。使用 Spring 的JpaTemplate確保所有 DAO 共享同一個EntityManager。因此,事務傳播會自動發生——由服務層決定。換句話說,它的行為將與 Spring framework 中配置的其他持久化機制完全相同。沒有特定於 JPA 的內容——這也是將它排除在這篇專注於 JPA 的文章之外的原因。

配置

由於我選擇了基於註解的對映,所以在展示領域類時,你實際上已經看到了大多數特定於 JPA 的配置。如上所述,也可以透過 XML(在 'orm.xml' 檔案中)配置這些對映。唯一需要的其他配置在 'META-INF/persistence.xml' 中。在這種情況下,這非常簡單,因為資料庫相關的屬性可以透過 Spring 配置中提供的依賴注入的“dataSource”來提供給EntityManagerFactory(接下來會介紹)。這個 'persistence.xml' 檔案中唯一的其他資訊是使用本地事務還是全域性(JTA)事務。以下是 'persistence.xml' 檔案的內容


<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">

  <persistence-unit name="SpringJpaGettingStarted" transaction-type="RESOURCE_LOCAL"/>

</persistence>

Spring 配置中只有 4 個 bean(好吧,還有幾個內部 bean)。首先是“restaurantDao”(我故意將“jpa”從 bean 名稱中刪除,因為任何依賴於 DAO 的服務層 bean 都應該只關心通用介面)。這個 DAO 的 JPA 實現唯一需要的屬性是“entityManagerFactory”,它用於建立JpaTemplate。“entityManagerFactory”依賴於“dataSource”,這與 JPA 沒有特定關係。在此配置中,你將看到一個DriverManagerDataSource,但在生產程式碼中,它會被連線池取代——通常是透過一個JndiObjectFactoryBean(或 Spring 2.0 新的便捷 jndi:lookup 標籤)獲取的。最後一個 bean 是測試類所需的“transactionManager”。它與用於在服務層劃分事務的“transactionManager”是同一個。實現類是 Spring 的JpaTransactionManager。對於熟悉為 JDBC、Hibernate、JDO、TopLink 或 iBATIS 配置 Spring 的人來說,這些 bean 大多數看起來會非常熟悉。唯一的例外是EntityManagerFactory。我將簡要討論它,但首先看看完整的 'applicationContext.xml' 檔案。


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">

  <bean id="restaurantDao" class="blog.jpa.dao.JpaRestaurantDao">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
  </bean>

  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.ContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaVendorAdapter">
      <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
        <property name="showSql" value="true"/>
        <property name="generateDdl" value="true"/>
        <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.HSQLPlatform"/>
      </bean>
    </property>
    <property name="loadTimeWeaver">
      <bean class="org.springframework.instrument.classloading.SimpleLoadTimeWeaver"/>
    </property>
  </bean>

  <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
    <property name="url" value="jdbc:hsqldb:hsql:///"/>
    <property name="username" value="sa"/>
    <property name="password" value=""/>
  </bean>

  <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
    <property name="dataSource" ref="dataSource"/>
  </bean>

</beans>

首先你看到“entityManagerFactory”需要知道一個“dataSource”。接下來是“jpaVendorAdapter”,因為有各種 JPA 實現。在這種情況下,我將TopLinkJpaVendorAdapter配置為一個內部 bean,它有一些自己的屬性。有一個 boolean 屬性用於指定是否顯示 SQL,另一個 boolean 屬性用於生成 DDL。這兩個屬性都設定為“true”,因此每次執行測試時都會自動生成資料庫 schema。這在早期開發階段非常方便,因為它為對映、列名等方面的實驗提供了即時反饋。“databasePlatformClass”提供了正在使用的特定資料庫的必要資訊。最後,“entityManagerFactory”有一個“loadTimeWeaver”屬性,它在 JPA 持久化提供程式轉換類檔案以適應某些特性(例如懶載入)方面發揮作用。

整合測試

學習新 API 的最好方法也許是編寫一系列測試用例。JpaRestaurantDaoTests類提供了一些基本測試。為了瞭解更多關於 JPA 的資訊,可以修改程式碼和/或配置並觀察對這些測試的影響。例如,嘗試修改 級聯 設定 - 或關聯的多重性。注意JpaRestaurantDaoTests擴充套件了 Spring 的AbstractJpaTests。你可能已經熟悉 Spring 的AbstractTransactionalDataSourceSpringContextTests。此類行為方式相同,測試方法引起的任何資料庫更改預設會回滾。AbstractJpaTests實際上做的更多,但深入研究這些細節超出了本文的範圍。如果感興趣,可以檢視原始碼AbstractJpaTests.

這是JpaRestaurantDaoTests程式碼


package blog.jpa.dao;

import java.util.List;
import org.springframework.test.jpa.AbstractJpaTests;
import blog.jpa.dao.RestaurantDao;
import blog.jpa.domain.Restaurant;

public class JpaRestaurantDaoTests extends AbstractJpaTests {

  private RestaurantDao restaurantDao;

  public void setRestaurantDao(RestaurantDao restaurantDao) {
    this.restaurantDao = restaurantDao;
  }

  protected String[] getConfigLocations() {
    return new String[] {"classpath:/blog/jpa/dao/applicationContext.xml"};
  }

  protected void onSetUpInTransaction() throws Exception {
    jdbcTemplate.execute("insert into address (id, street_number, street_name) values (1, 10, 'Main Street')");
    jdbcTemplate.execute("insert into address (id, street_number, street_name) values (2, 20, 'Main Street')");
    jdbcTemplate.execute("insert into address (id, street_number, street_name) values (3, 123, 'Dover Street')");

    jdbcTemplate.execute("insert into restaurant (id, name, address_id) values (1, 'Burger Barn', 1)");
    jdbcTemplate.execute("insert into restaurant (id, name, address_id) values (2, 'Veggie Village', 2)");
    jdbcTemplate.execute("insert into restaurant (id, name, address_id) values (3, 'Dover Diner', 3)");

    jdbcTemplate.execute("insert into entree (id, name, vegetarian) values (1, 'Hamburger', 0)");
    jdbcTemplate.execute("insert into entree (id, name, vegetarian) values (2, 'Cheeseburger', 0)");
    jdbcTemplate.execute("insert into entree (id, name, vegetarian) values (3, 'Tofu Stir Fry', 1)");
    jdbcTemplate.execute("insert into entree (id, name, vegetarian) values (4, 'Vegetable Soup', 1)");

    jdbcTemplate.execute("insert into restaurant_entree (restaurant_id, entree_id) values (1, 1)");
    jdbcTemplate.execute("insert into restaurant_entree (restaurant_id, entree_id) values (1, 2)");
    jdbcTemplate.execute("insert into restaurant_entree (restaurant_id, entree_id) values (2, 3)");
    jdbcTemplate.execute("insert into restaurant_entree (restaurant_id, entree_id) values (2, 4)");
    jdbcTemplate.execute("insert into restaurant_entree (restaurant_id, entree_id) values (3, 1)");
    jdbcTemplate.execute("insert into restaurant_entree (restaurant_id, entree_id) values (3, 2)");
    jdbcTemplate.execute("insert into restaurant_entree (restaurant_id, entree_id) values (3, 4)");
  }

  public void testFindByIdWhereRestaurantExists() {
    Restaurant restaurant = restaurantDao.findById(1);
    assertNotNull(restaurant);
    assertEquals("Burger Barn", restaurant.getName());
  }

  public void testFindByIdWhereRestaurantDoesNotExist() {
    Restaurant restaurant = restaurantDao.findById(99);
    assertNull(restaurant);
  }

  public void testFindByNameWhereRestaurantExists() {
    List<Restaurant> restaurants = restaurantDao.findByName("Veggie Village");
    assertEquals(1, restaurants.size());
    Restaurant restaurant = restaurants.get(0);
    assertEquals("Veggie Village", restaurant.getName());
    assertEquals("Main Street", restaurant.getAddress().getStreetName());
    assertEquals(2, restaurant.getEntrees().size());
  }

  public void testFindByNameWhereRestaurantDoesNotExist() {
    List<Restaurant> restaurants = restaurantDao.findByName("No Such Restaurant");
    assertEquals(0, restaurants.size());
  }

  public void testFindByStreetName() {
    List<Restaurant> restaurants = restaurantDao.findByStreetName("Main Street");
    assertEquals(2, restaurants.size());
    Restaurant r1 = restaurantDao.findByName("Burger Barn").get(0);
    Restaurant r2 = restaurantDao.findByName("Veggie Village").get(0);
    assertTrue(restaurants.contains(r1));
    assertTrue(restaurants.contains(r2));
  }

  public void testFindByEntreeNameLike() {
    List<Restaurant> restaurants = restaurantDao.findByEntreeNameLike("%burger");
    assertEquals(2, restaurants.size());
  }

  public void testFindRestaurantsWithVegetarianOptions() {
    List<Restaurant> restaurants = restaurantDao.findRestaurantsWithVegetarianEntrees();
    assertEquals(2, restaurants.size());
  }

  public void testModifyRestaurant() {
    String oldName = "Burger Barn";
    String newName = "Hamburger Hut";
    Restaurant restaurant = restaurantDao.findByName(oldName).get(0);
    restaurant.setName(newName);
    restaurantDao.update(restaurant);
    List<Restaurant> results = restaurantDao.findByName(oldName);
    assertEquals(0, results.size());
    results = restaurantDao.findByName(newName);
    assertEquals(1, results.size());
  }

  public void testDeleteRestaurantAlsoDeletesAddress() {
    String restaurantName = "Dover Diner";
    int preRestaurantCount = jdbcTemplate.queryForInt("select count(*) from restaurant");
    int preAddressCount = jdbcTemplate.queryForInt("select count(*) from address where street_name = 'Dover Street'");
    Restaurant restaurant = restaurantDao.findByName(restaurantName).get(0);
    restaurantDao.delete(restaurant);
    List<Restaurant> results = restaurantDao.findByName(restaurantName);
    assertEquals(0, results.size());
    int postRestaurantCount = jdbcTemplate.queryForInt("select count(*) from restaurant");
    assertEquals(preRestaurantCount - 1, postRestaurantCount);
    int postAddressCount = jdbcTemplate.queryForInt("select count(*) from address where street_name = 'Dover Street'");
    assertEquals(preAddressCount - 1, postAddressCount);
  }

  public void testDeleteRestaurantDoesNotDeleteEntrees() {
    String restaurantName = "Dover Diner";
    int preRestaurantCount = jdbcTemplate.queryForInt("select count(*) from restaurant");
    int preEntreeCount = jdbcTemplate.queryForInt("select count(*) from entree");
    Restaurant restaurant = restaurantDao.findByName(restaurantName).get(0);
    restaurantDao.delete(restaurant);
    List<Restaurant> results = restaurantDao.findByName(restaurantName);
    assertEquals(0, results.size());
    int postRestaurantCount = jdbcTemplate.queryForInt("select count(*) from restaurant");
    assertEquals(preRestaurantCount - 1, postRestaurantCount);
    int postEntreeCount = jdbcTemplate.queryForInt("select count(*) from entree");
    assertEquals(preEntreeCount, postEntreeCount);
  }
}

延伸閱讀

JPA 是一個龐大的主題,本部落格僅觸及其皮毛——主要目標是演示基於 JPA 的持久化實現在 Spring 中的基本配置。顯然,這個領域模型在物件關係對映方面微不足道。但是,一旦有了這個可工作的配置,你就可以在此示例的基礎上進行擴充套件,同時探索 JPA 提供的 ORM 能力。我強烈建議你透過 JavaDoc 和 Spring 參考文件更仔細地研究 Spring 對 JPA 的支援。2.0 RC1 版本在參考文件的 ORM 部分中增加了一個關於 JPA 的子部分。

這裡有一些有用的連結

JSR-220(包含 JPA 規範)Glassfish JPA(參考實現)Kodo 4.0(基於 Kodo 的 BEA JPA 實現)Hibernate JPA 遷移指南

訂閱 Spring 新聞通訊

訂閱 Spring 新聞通訊,保持聯絡

訂閱

保持領先

VMware 提供培訓和認證,助你加速前進。

瞭解更多

獲取支援

Tanzu Spring 透過一份簡單的訂閱提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案。

瞭解更多

即將舉行的活動

檢視 Spring 社群所有即將舉行的活動。

檢視全部