初始化設置
- 保證你使用的是Java 5 (EJB 3.0 中 JPA 的先決條件).
- 從 https://glassfish.dev.java.net/downloads/persistence/JavaPersistence.html 下載 glassfish JPA jar (注意: 我使用的是 “V2_build_02″ jar, 但該版本后的版本也應回往前兼容的.)
- 從“installer” jar 中解壓,并運行: java -jar glassfish-persistence-installer-v2-b02.jar
- 把 toplink-essentials.jar 加入你的 classpath
- 把數據庫的驅動 JAR 也加入( 我用的是 version 1.8.0.1 的 hsqldb.jar 作為例子,但實際上你只需很少的改變就能適配到另外的數據庫 )
- 加入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
領域模型 (domain model)
這個例子中我們只是有目的地列舉了3個簡單的domain model. 要注意的是這例子中我們使用了annotation。 使用 JPA 時,一般會選擇用annotation 或 XML 文件,又或者兩者一起配合用,去指定ORM(object-relational mapping)元數據。在這里,我們只是選擇了單獨用annotation, 因為只需要在domain model 的代碼中加入簡短的描述就能馬上辦到。 首先, 看看餐廳 Restaurant class:
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;
}
}
Adderss class:
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 class:
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;
}
}
如你看到的那樣,并不是所有的 field 都需要annotation。JAP 會使用默認值(例如用數據表中的列名來精確匹配屬性名),所以在很多的情況下,你并不需要很明確的去指定元數據。需要注意的是,在 Entree 類中,我并沒有為String 屬性 “name” 或 boolean 屬性 “vegetarian” 加入 annotation。
然而,在 Address 類中,我使用了annotation, 因為我不想用數據庫表中的列名作為默認名(例如,我用 “STREET_NAME”而默認的是 “STREETNAME”)。 在ORM機制中最重要的當然是指定Objects和database間的對應關系。在 Restaurant 類中,我們用 @OneToOne 去描述它與 Address 的關系,同時我們用 @ManyToMany 去描述它與 Entree 類中的成員關系。因為其它類的實例也是由 EntityManager 所管理的, 所以可以指定“cascade”規則,如當一個 Restaurant 被刪除,所有相關聯的 Address 也會同時被刪除。在下面,你將會看到這個場景的測試用例。 最后,看看 @Id 和指定“strategy”給ID的 @GeneratedValue 。 這元數據是用來描述數據庫中唯一鍵 primary key 的生成方式的。想知道更多關于JPA annotation 的資料,查看 JPA 的說明文檔,JSR-220. 數據訪問層 (Data Access Layer) 最好的方式是建立通用的接口去隱藏持久層所有的實現細節,這樣就算由JPA換到其它的實現方式也不會影響到系統架構。
同時,這也為業務邏輯層提供了方便,可以更容易地實現 stub 或 mock 測試。 RestaurantDao 是一個接口,注意它沒有對任何 JPA 或 Spring 的類有依賴。實際上,它只對自身的domain model 有依賴(在這個簡單的例子中,只有一個,那就是 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 是可選的,它只是提供了通過 EntityManagerFactory 更直接使用 JpaTemplate 的方法。JpaTemplate 也是可選的,如果你不希望 Spring 的自動處理 JPA exception 的事務方式,你完全可以避免使用 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);
}
}
業務邏輯層 (Service Layer)
由于我們的主要目的是集中在數據訪問層 JPA 的實現,所以業務邏輯層基本上忽略不講。在實際項目中,業務邏輯層對于整個系統的架構至關重要。它是分離事務(transaction)的重點。一般情況下,我們都會通過 Spring 來配置事務。在下面的步驟中,當你看配置時,你會注意到我已提供了一個“transactionManager” 的 bean。 它可以為測試用例中的每個測試方法提供事務回滾,同時它也讓使用同一個 “transactionManager”的業務邏輯層的方法提供事務處理。數據庫訪問層的代碼與則不負責事務處理,事務傳播的發生是自動的,最終由業務邏輯層來處理。Spring 框架中的所有持久層類的配置都是相同的,使用 Spring JpaTemplate 時要注意保證所有 DAO 都共享同一個EntityManager 。
配置
因為我選擇了使用基于 annotation 的映射關系,你或許已經看過許多 JPA 的配置說明,如我上面說提到的,它同樣可以通過 XML( 在‘orm.xml’文件里 )來配置映射關系。另一種則只要求配置‘META-INF/persistence.xml’。這樣的話,就能更為容易,因為database相關的屬性可以通過EntityManagerFactory 來獲得。在‘persistence.xml’中需要的信息只是需要 local 的還是global(JTA) 的事務。下面是 ‘persistence.xml’ 的具體內容:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
<persistence-unit name="SpringJpaGettingStarted" transaction-type="RESOURCE_LOCAL"/>
</persistence>
有4個bean在這 Spring 配置中是要注意的。
首先,看看“restaurantDao” (我沒有在bean名前的"jpa"也加上去的原因是所有的業務邏輯層都必須只與接口相關),其唯一需要的 property 就是 “entityManagerFactory” ,用于產生JpaTemplate。 “entityManagerFactory” 需要“dataSource”, 這個在 JPA 說明文檔里并沒有提到。在這里,我們使用了DriverManagerDataSource,但是在實際操作中,你需要用你自己數據庫的連接池,或者是用JndiObjectFactoryBean來得到Jndi。最后的 “transactionManager” bean是測試和邏輯層處理事務需要到的。如果你已經熟悉 Spring 下 JDBC, Hibernate, JDO, TopLink, 或 iBATIS 的配置,這幾個bean對于你來說是再簡單不過了。然我們來看看完整的‘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://localhost/"/>
<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” bean需要 “jpaVendorAdapter” ,對于 “jpaVendorAdapter”有多種多樣的JPA實現方式。在這個例子里,我用了 TopLinkJpaVendorAdapter 作為 inner bean,它也需要自己的一些property ,它有兩個屬性分別指明是否現實SQL和是否生成DDL.在這里我們都設為 “true” ,所以當測試的時候數據庫表會每次都自動生成,這對于早期的開發帶來不少方便。還有就是“databasePlatformClass” 提供了必要的數據庫庫使用的詳細信息。“entityManagerFactory”還有一個“loadTimeWeaver” 屬性,以配合某些特性,如延遲加載(lazy-loading)。
集成測試(Integration Testing)
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);
}
}