集成JPA

上一节我们讲了在Spring中集成Hibernate。Hibernate是第一个被广泛使用的ORM框架,但是很多小伙伴还听说过JPA:Java Persistence API,这又是啥?

在讨论JPA之前,我们要注意到JavaEE早在1999年就发布了,并且有Servlet、JMS等诸多标准。和其他平台不同,Java世界早期非常热衷于标准先行,各家跟进:大家先坐下来把接口定了,然后,各自回家干活去实现接口,这样,用户就可以在不同的厂家提供的产品进行选择,还可以随意切换,因为用户编写代码的时候只需要引用接口,并不需要引用具体的底层实现(想想JDBC)。

JPA就是JavaEE的一个ORM标准,它的实现其实和Hibernate没啥本质区别,但是用户如果使用JPA,那么引用的就是javax.persistence这个“标准”包,而不是org.hibernate这样的第三方包。因为JPA只是接口,所以,还需要选择一个实现产品,跟JDBC接口和MySQL驱动一个道理。

我们使用JPA时也完全可以选择Hibernate作为底层实现,但也可以选择其它的JPA提供方,比如EclipseLink。Spring内置了JPA的集成,并支持选择Hibernate或EclipseLink作为实现。这里我们仍然以主流的Hibernate作为JPA实现为例子,演示JPA的基本用法。

和使用Hibernate一样,我们只需要引入如下依赖:

  • org.springframework:spring-context:5.2.0.RELEASE
  • org.springframework:spring-orm:5.2.0.RELEASE
  • javax.annotation:javax.annotation-api:1.3.2
  • org.hibernate:hibernate-core:5.4.2.Final
  • com.zaxxer:HikariCP:3.4.2
  • org.hsqldb:hsqldb:2.5.0

然后,在AppConfig中启用声明式事务管理,创建DataSource

  1. @Configuration
  2. @ComponentScan
  3. @EnableTransactionManagement
  4. @PropertySource("jdbc.properties")
  5. public class AppConfig {
  6. @Bean
  7. DataSource createDataSource() { ... }
  8. }

使用Hibernate时,我们需要创建一个LocalSessionFactoryBean,并让它再自动创建一个SessionFactory。使用JPA也是类似的,我们需要创建一个LocalContainerEntityManagerFactoryBean,并让它再自动创建一个EntityManagerFactory

  1. @Bean
  2. LocalContainerEntityManagerFactoryBean createEntityManagerFactory(@Autowired DataSource dataSource) {
  3. var entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
  4. // 设置DataSource:
  5. entityManagerFactoryBean.setDataSource(dataSource);
  6. // 扫描指定的package获取所有entity class:
  7. entityManagerFactoryBean.setPackagesToScan("com.itranswarp.learnjava.entity");
  8. // 指定JPA的提供商是Hibernate:
  9. JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
  10. entityManagerFactoryBean.setJpaVendorAdapter(vendorAdapter);
  11. // 设定特定提供商自己的配置:
  12. var props = new Properties();
  13. props.setProperty("hibernate.hbm2ddl.auto", "update");
  14. props.setProperty("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
  15. props.setProperty("hibernate.show_sql", "true");
  16. entityManagerFactoryBean.setJpaProperties(props);
  17. return entityManagerFactoryBean;
  18. }

观察上述代码,除了需要注入DataSource和设定自动扫描的package外,还需要指定JPA的提供商,这里使用Spring提供的一个HibernateJpaVendorAdapter,最后,针对Hibernate自己需要的配置,以Properties的形式注入。

最后,我们还需要实例化一个JpaTransactionManager,以实现声明式事务:

  1. @Bean
  2. PlatformTransactionManager createTxManager(@Autowired EntityManagerFactory entityManagerFactory) {
  3. return new JpaTransactionManager(entityManagerFactory);
  4. }

这样,我们就完成了JPA的全部初始化工作。有些童鞋可能从网上搜索得知JPA需要persistence.xml配置文件,以及复杂的orm.xml文件。这里我们负责地告诉大家,使用Spring+Hibernate作为JPA实现,无需任何配置文件。

所有Entity Bean的配置和上一节完全相同,全部采用Annotation标注。我们现在只需关心具体的业务类如何通过JPA接口操作数据库。

还是以UserService为例,除了标注@Component@Transactional外,我们需要注入一个EntityManager,但是不要使用Autowired,而是@PersistenceContext

  1. @Component
  2. @Transactional
  3. public class UserService {
  4. @PersistenceContext
  5. EntityManager em;
  6. }

我们回顾一下JDBC、Hibernate和JPA提供的接口,实际上,它们的关系如下:

JDBCHibernateJPA
DataSourceSessionFactoryEntityManagerFactory
ConnectionSessionEntityManager

SessionFactoryEntityManagerFactory相当于DataSourceSessionEntityManager相当于Connection。每次需要访问数据库的时候,需要获取新的SessionEntityManager,用完后再关闭。

但是,注意到UserService注入的不是EntityManagerFactory,而是EntityManager,并且标注了@PersistenceContext。难道使用JPA可以允许多线程操作同一个EntityManager

实际上这里注入的并不是真正的EntityManager,而是一个EntityManager的代理类,相当于:

  1. public class EntityManagerProxy implements EntityManager {
  2. private EntityManagerFactory emf;
  3. }

Spring遇到标注了@PersistenceContextEntityManager会自动注入代理,该代理会在必要的时候自动打开EntityManager。换句话说,多线程引用的EntityManager虽然是同一个代理类,但该代理类内部针对不同线程会创建不同的EntityManager实例。

简单总结一下,标注了@PersistenceContextEntityManager可以被多线程安全地共享。

因此,在UserService的每个业务方法里,直接使用EntityManager就很方便。以主键查询为例:

  1. public User getUserById(long id) {
  2. User user = this.em.find(User.class, id);
  3. if (user == null) {
  4. throw new RuntimeException("User not found by id: " + id);
  5. }
  6. return user;
  7. }

JPA同样支持Criteria查询,比如我们需要的查询如下:

  1. SELECT * FROM user WHERE email = ?

使用Criteria查询的代码如下:

  1. public User fetchUserByEmail(String email) {
  2. // CriteriaBuilder:
  3. var cb = em.getCriteriaBuilder();
  4. CriteriaQuery<User> q = cb.createQuery(User.class);
  5. Root<User> r = q.from(User.class);
  6. q.where(cb.equal(r.get("email"), cb.parameter(String.class, "e")));
  7. TypedQuery<User> query = em.createQuery(q);
  8. // 绑定参数:
  9. query.setParameter("e", email);
  10. // 执行查询:
  11. List<User> list = query.getResultList();
  12. return list.isEmpty() ? null : list.get(0);
  13. }

一个简单的查询用Criteria写出来就像上面那样复杂,太恐怖了,如果条件多加几个,这种写法谁读得懂?

所以,正常人还是建议写JPQL查询,它的语法和HQL基本差不多:

  1. public User getUserByEmail(String email) {
  2. // JPQL查询:
  3. TypedQuery<User> query = em.createQuery("SELECT u FROM User u WHERE u.email = :e", User.class);
  4. query.setParameter("e", email);
  5. List<User> list = query.getResultList();
  6. if (list.isEmpty()) {
  7. throw new RuntimeException("User not found by email.");
  8. }
  9. return list.get(0);
  10. }

同样的,JPA也支持NamedQuery,即先给查询起个名字,再按名字创建查询:

  1. public User login(String email, String password) {
  2. TypedQuery<User> query = em.createNamedQuery("login", User.class);
  3. query.setParameter("e", email);
  4. query.setParameter("p", password);
  5. List<User> list = query.getResultList();
  6. return list.isEmpty() ? null : list.get(0);
  7. }

NamedQuery通过注解标注在User类上,它的定义和上一节的User类一样:

  1. @NamedQueries(
  2. @NamedQuery(
  3. name = "login",
  4. query = "SELECT u FROM User u WHERE u.email=:e AND u.password=:p"
  5. )
  6. )
  7. @Entity
  8. public class User {
  9. ...
  10. }

对数据库进行增删改的操作,可以分别使用persist()refresh()remove()方法,参数均为Entity Bean本身,使用非常简单,这里不再多述。

练习

集成JPA - 图1下载练习:使用JPA (推荐使用IDE练习插件快速下载)

小结

在Spring中集成JPA要选择一个实现,可以选择Hibernate或EclipseLink;

使用JPA与Hibernate类似,但注入的核心资源是带有@PersistenceContext注解的EntityManager代理类。

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

集成JPA - 图2 集成JPA - 图3