在 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://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”需要知道一个“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 社区所有即将举行的活动。

查看全部