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管理,因此可以指定 “cascade” 規則。 例如,當一個Restaurant被刪除時,相關聯的Address也將被刪除。 稍後,您將看到此情境的測試案例。

最後,看看 @Id 註釋和為 ID 的 @GeneratedValue 指定的 “strategy”。 此中繼資料用於描述主鍵產生策略,該策略反過來控制資料庫中的識別碼。

要了解更多關於這些以及其他 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” 相同。 主要的一點是,資料存取層中沒有與事務相關的程式碼。 使用 SpringJpaTemplate可確保相同的EntityManager在所有 DAO 之間共享。 因此,事務傳播會自動發生 - 由服務層指示。 換句話說,它的行為實際上與在 Spring 框架中配置的其他持久化機制完全相同。 沒有任何特定於 JPA 的內容 - 因此將其從專注於 JPA 的條目中排除在外的理由。

配置

由於我選擇了基於註釋的映射,因此當呈現領域類別時,您實際上已經看到了大部分 JPA 特定的配置。 如上所述,也可以透過 XML (在 “orm.xml” 檔案中) 配置這些映射。 唯一其他需要的配置是在 “META-INF/persistence.xml” 中。 在這種情況下,這非常簡單,因為資料庫相關屬性將可供EntityManagerFactory透過在 Spring 配置中提供的依賴注入的 “dataSource” 使用 (接下來會介紹)。 此 “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” (我特意從 bean 名稱中省略了 “jpa”,因為依賴 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://127.0.0.1/"/>
    <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,它本身有一些屬性。 有一個布林屬性用於指定是否應顯示 SQL,另一個布林屬性用於產生 DDL。 這兩者都已設定為 “true”,因此每次執行測試時都會自動產生資料庫結構描述。 這在早期開發階段非常方便,因為它為映射、欄位名稱等中的實驗提供了即時回饋。“databasePlatformClass” 提供了有關正在使用的特定資料庫的必要資訊。 最後,“entityManagerFactory” 有一個用於 “loadTimeWeaver” 的屬性,它在 JPA 持久化提供者轉換類別檔案以適應某些功能 (例如延遲載入) 中起作用。

整合測試

學習新 API 的最佳方式可能是編寫一堆測試案例。JpaRestaurantDaoTests類別提供了一些基本測試。 為了了解更多關於 JPA 的資訊,請修改程式碼和/或配置,並觀察對這些測試的影響。 例如,嘗試修改 cascade 設定 - 或關聯的基數。 請注意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 是一個廣泛的主題,而這篇部落格文章僅觸及了表面 - 主要目標是示範如何使用 Spring 進行基於 JPA 的持久層實作的基本配置。 顯然,就物件關係映射而言,這個領域模型是微不足道的。 然而,一旦您有了這個有效運作的配置,您就可以擴展此處的範例,同時探索 JPA 提供的 ORM 功能。 我強烈建議您透過 JavaDoc 和 Spring 參考文件更深入地了解 Spring JPA 的支援。 2.0 RC1 版本在參考文件的 ORM 區段中新增了關於 JPA 的子區段。

以下是一些有用的連結

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

取得 Spring 電子報

隨時掌握 Spring 電子報的最新資訊

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進展。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將舉辦的活動

查看 Spring 社群中所有即將舉辦的活動。

查看全部