領先一步
VMware 提供培訓和認證,以加速您的進展。
了解更多撰寫這篇部落格文章的動機,是為了提供一個簡單的逐步指南,以便在 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)。
最後,也將這些 jar 新增到您的 classpath
此範例將基於僅包含 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 遷移指南