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,但任何更高版本也应该可以工作。)

要从“安装程序”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类中,我没有为 String 属性“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是可选的。可以通过简单地向JpaTemplate提供EntityManagerFactory来直接构造一个JpaTemplate。事实上,JpaTemplate本身是可选的。如果您不希望将 JPA 异常自动转换为 Spring 的运行时异常层次结构,则可以完全避免。在这种情况下,您可能仍然对 Spring 的EntityManagerFactoryUtilsEntityManager.

类感兴趣,该类提供了一种方便的静态方法来获取共享的(因此是事务性的)


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);
  }

}

以下是实现

服务层JpaTemplate由于这里的目的是关注数据访问层中 JPA 的实现,因此省略了服务层。显然,在现实情况下,服务层将在系统架构中扮演关键角色。它将是事务分界线的地方——通常,它们将在 Spring 配置中声明性地分界。在下一步中,当您查看配置时,您会注意到我提供了一个“transactionManager”bean。它被基类测试用来自动将每个测试方法包装在一个事务中,它与将服务层方法与事务一起包装的“transactionManager”相同。主要要点是数据访问层中没有与事务相关的代码。使用 SpringEntityManager确保相同的

在所有 DAO 中共享。因此,事务传播会自动发生——由服务层决定。换句话说,它的行为实际上与在 Spring 框架内配置的其他持久化机制完全相同。没有 JPA 特定的内容——因此,将其排除在本条目之外的理由是专注于 JPA。

配置EntityManagerFactory由于我选择了基于注释的映射,因此在呈现域类时,您实际上已经看到了大多数 JPA 特定的配置。如上所述,也可以通过 XML(在“orm.xml”文件中)配置这些映射。唯一其他必需的配置是在“META-INF/persistence.xml”中。在这种情况下,这非常简单,因为数据库相关的属性将通过 Spring 配置中提供的依赖注入的“dataSource”提供给


<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">

  <persistence-unit name="SpringJpaGettingStarted" transaction-type="RESOURCE_LOCAL"/>

</persistence>

。 “persistence.xml”中的唯一其他信息是使用本地事务还是全局(JTA)事务。以下是“persistence.xml”文件的内容JpaTemplateSpring 配置中只有 4 个 bean(好吧,还有几个内部 bean)。首先是“restaurantDao”(我故意将“jpa”从 bean 名称中省略,因为依赖于 DAO 的任何服务层 bean 只应关注泛型接口)。JPA 实现此 DAO 的唯一必需属性是“entityManagerFactory”,它用于创建。“entityManagerFactory”依赖于“dataSource”,而这与 JPA 无关。在此配置中,您将看到一个DriverManagerDataSource,但在生产代码中,这将被连接池替换——通常通过JndiObjectFactoryBean(或 Spring 2.0 的新便利 jndi:lookup 标签) 获取。最后一个 bean 是测试类所需的“transactionManager”。这与用于在服务层中划定事务的“transactionManager”相同。实现类是 Spring 的JpaTransactionManagerEntityManagerFactory。对于熟悉为 JDBC、Hibernate、JDO、TopLink 或 iBATIS 配置 Spring 的任何人来说,大多数这些 bean 看起来都非常熟悉。唯一的例外是


<?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>

。我将简要讨论它,但首先请查看完整的“applicationContext.xml”文件首先,您看到“entityManagerFactory”需要知道一个“dataSource”。接下来是“jpaVendorAdapter”,因为存在各种 JPA 实现。在这种情况下,我已经将TopLinkJpaVendorAdapter

配置为内部 bean,并且它本身也有一些属性。有一个布尔属性用于指定是否应显示 SQL,以及另一个用于生成 DDL 的布尔属性。这两个都已设置为“true”,因此每次执行测试时都会自动生成数据库模式。这在早期开发阶段非常方便,因为它为映射、列名等的实验提供了即时反馈。“databasePlatformClass”提供了正在使用哪个特定数据库的必要信息。最后,“entityManagerFactory”有一个“loadTimeWeaver”属性,它在 JPA 持久性提供程序转换类文件方面发挥作用,以适应某些功能,例如延迟加载。

集成测试学习新 API 的最佳方法可能是编写大量测试用例。该JpaRestaurantDaoTests学习新 API 的最佳方法可能是编写大量测试用例。该类提供了一些基本测试。为了了解更多关于 JPA 的信息,请修改代码和/或配置并观察对这些测试的影响。例如,尝试修改级联设置,或关联的基数。请注意,扩展了 Spring 的AbstractJpaTests。您可能已经熟悉 Spring 的AbstractTransactionalDataSourceSpringContextTests扩展了 Spring 的。此类将以相同的方式运行,因为测试方法引起的任何数据库更改都将默认回滚。扩展了 Spring 的.

实际上做得更多,但这篇文章的范围不包括这些细节。如有兴趣,请查看学习新 API 的最佳方法可能是编写大量测试用例。该的源代码


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社区中所有即将举行的活动。

查看全部