Spring 2.0 中配置 JPA

翻译:SpringSide团队 转载请注明出处。

本文提供了一个简单的 Spring 框架 standalone 环境下,如何 step-by-step 去开发 JPA 的向导。 JPA 的规范最开始时是由 EJB 3.0 的持久性机制产生的, 它被公认为是把简单的 POJOs 持久化的机制。你只需少量 JAR 在 classpath 中,配置一点 Spring 的 bean, 就能在你喜爱的IDE中去开始感受 JPA 的强大威力了。我们在这里使用的是 Glassfish JPA - 一个基于 Oracle’s TopLink ORM framework 的开源项目

初始化设置
  1. 保证你使用的是Java 5 (EJB 3.0 中 JPA 的先决条件).
  2. 从 https://glassfish.dev.java.net/downloads/persistence/JavaPersistence.html 下载 glassfish JPA jar (注意: 我使用的是 “V2_build_02″ jar, 但该版本后的版本也应回往前兼容的.)
  3. 从“installer” jar 中解压,并运行: java -jar glassfish-persistence-installer-v2-b02.jar
  4. 把 toplink-essentials.jar 加入你的 classpath
  5. 把数据库的驱动 JAR 也加入( 我用的是 version 1.8.0.1 的 hsqldb.jar 作为例子,但实际上你只需很少的改变就能适配到另外的数据库 )
  6. 加入2.0 M5 以上版本的Spring JAR( http://sourceforge.net/project/showfiles.php?group_id=73357) - spring.jar - spring-jpa.jar - spring-mock.jar
  7. 最后,把这些 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);
}
}


进一步阅读

JPA是一个很大的话题,这篇日志只是讲述了如何在 Spring 框架中配置 JPA. 

只要你弄懂了配置如何运作,你就能很好的运用 JPA提供的ORM功能。我非常建议你去看看 JPA的官方文档还有 Spring reference 文档。在Spring 2.0 RC1 文档中就有加入了 JPA 的 ORM部分内容。以下是比较有用的一些连接:

JSR-220 (包含了 JPA 说明文档)
http://www.jcp.org/en/jsr/detail?id=220
Glassfish JPA (相关的实现说明)
https://glassfish.dev.java.net/javaee5/persistence/
Kodo 4.0 (BEA的基于Kodo的JPA实现)
http://commerce.bea.com/showproduct.jsp?family=KODO&major=4.0&minor=0
Hibernate JPA Migration Guide
http://www.hibernate.org/371.html
(自 http://blog.interface21.com/main/2006/05/30/getting-started-with-jpa-in-spring-20/ , cac 翻译)

注:因无法拷贝原的连接地址。因此,此处略有改动。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 内容概要 《计算机试卷1》是一份综合性的计算机基础和应用测试卷,涵盖了计算机硬件、软件、操作系统、网络、多媒体技术等多个领域的知识点。试卷包括单选题和操作应用两大类,单选题部分测试学生对计算机基础知识的掌握,操作应用部分则评估学生对计算机应用软件的实际操作能力。 ### 适用人群 本试卷适用于: - 计算机专业或信息技术相关专业的学生,用于课程学习或考试复习。 - 准备计算机等级考试或职业资格认证的人士,作为实战演练材料。 - 对计算机操作有兴趣的自学者,用于提升个人计算机应用技能。 - 计算机基础教育工作者,作为教学资源或出题参考。 ### 使用场景及目标 1. **学习评估**:作为学校或教育机构对学生计算机基础知识和应用技能的评估工具。 2. **自学测试**:供个人自学者检验自己对计算机知识的掌握程度和操作熟练度。 3. **职业发展**:帮助职场人士通过实际操作练习,提升计算机应用能力,增强工作竞争力。 4. **教学资源**:教师可以用于课堂教学,作为教学内容的补充或学生的课后练习。 5. **竞赛准备**:适合准备计算机相关竞赛的学生,作为强化训练和技能检测的材料。 试卷的目标是通过系统性的题目设计,帮助学生全面复习和巩固计算机基础知识,同时通过实际操作题目,提高学生解决实际问题的能力。通过本试卷的学习与练习,学生将能够更加深入地理解计算机的工作原理,掌握常用软件的使用方法,为未来的学术或职业生涯打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值