Spring 2.0 中 JPA 入门

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

这篇博客文章的目的是提供一个简单的分步指南,指导您如何在 Spring 框架的独立环境中开始使用 JPA。虽然 JPA 规范最初是EJB 3.0 的持久化机制,但幸运的是,人们认识到任何此类机制实际上都应该能够持久化简单的 POJO。因此,只需在您的类路径中添加几个 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添加到您的类路径

添加包含您的数据库驱动程序的 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 也添加到您的类路径中

  • 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类中,我没有对字符串属性“name”或布尔属性“vegetarian”使用注释。然而,在Address类中,我使用了注释,因为我希望数据库中的列具有非默认名称(例如,我选择了“STREET_NAME”,而默认值将是“STREETNAME”)。

当然,任何 ORM 机制最重要的特性之一是如何指定对象之间关系到其数据库对应物的映射。在Restaurant类中,有一个@OneToOne注释用于描述与Address和一个@ManyToMany注释用于描述与Entree类成员的关系。由于这些其他类的实例也由EntityManager管理,因此可以指定“级联”规则。例如,当Restaurant被删除时,关联的Address也将被删除。稍后,您将看到针对此场景的测试用例。

最后,看看 @Id 注释和为 ID 的 @GeneratedValue 指定的“策略”。此元数据用于描述主键生成策略,该策略反过来控制数据库中的标识。

要了解更多关于这些和额外的 JPA 注释,请查看 JPA 规范——它实际上是 JSR-220 的子集。

代码 - 数据访问层

为了访问领域模型的实例,最好创建一个通用接口,隐藏底层持久化机制的所有细节。这样,如果以后切换到除 JPA 之外的其他东西,将不会对架构产生任何影响。这也使得测试服务层更容易,因为它允许创建此数据访问接口的存根实现——甚至动态模拟实现。

这是接口。请注意,它不依赖于任何 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是可选的。可以通过简单地将JpaTemplateEntityManagerFactory提供给其构造函数来直接构建。实际上,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确保所有 DAO 共享相同的EntityManager。因此,事务传播自动发生——由服务层决定。换句话说,它的行为实际上与 Spring 框架中配置的其他持久化机制完全相同。没有特定于 JPA 的内容——因此将其排除在专注于 JPA 的本文之外是有道理的。

配置

由于我选择了基于注解的映射,因此在呈现领域类时您实际上已经看到了大部分特定于 JPA 的配置。如上所述,也可以通过 XML(在“orm.xml”文件中)配置这些映射。唯一其他必需的配置是“META-INF/persistence.xml”文件。在这种情况下,它非常简单,因为数据库相关的属性将通过 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”(我特意将“jpa”从 bean 名称中省略,因为任何依赖于 DAO 的服务层 bean 都应该只关注通用接口)。此 DAO 的 JPA 实现唯一必需的属性是“entityManagerFactory”,它用于创建JpaTemplate。“entityManagerFactory”依赖于“dataSource”,并且对此没有任何 JPA 特定的内容。在此配置中,您将看到一个DriverManagerDataSource,但在生产代码中,这将替换为连接池——通常是通过JndiObjectFactoryBean(或 Spring 2.0 新的便捷 jndi:lookup 标签)获得的。最后一个 bean 是测试类所需的“transactionManager”。这与服务层中用于事务划分的“transactionManager”相同。实现类是 Spring 的JpaTransactionManager。对于熟悉配置 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:///"/>
    <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 的信息,请修改代码和/或配置并观察对这些测试的影响。例如,尝试修改级联设置——或关联的基数。请注意,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(BEA 基于 Kodo 的 JPA 实现)Hibernate JPA 迁移指南

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有