第七节 通过Spring Data JPA访问数据
上节内容介绍了如何通过JPA
原生API
访问数据库。从范例代码中可以看到,这个过程还是比较繁琐的,例如:
- 需要创建和调用
EntityManager
; - 需要使用
JPQL
编写查询语句; - 需要自己管理事务。
实际上大多数时候我们的查询都只是根据属性值查询实体对象而已。我们不希望为这样简单的目的编写一堆样板代码。
Spring Data JPA
的出现大大简化了JPA
查询的实现。它使得我们只需要通过创建一个接口,并且在其中根据我们的意图构造一个方法名称,Spring
在运行时会自动使用JPA
实现这个接口,查询数据库并返回正确的数据。
本项目的tmall-persistence-spring-data-jpa
模块,就是使用spring-data-jpa
实现tmall-domain
模块中定义的仓储接口。
引入依赖
为了使用spring-data-jpa
,我们只需要在maven
项目的pom.xml
中引入spring-data-jpa
依赖。当然还需要一个JPA
的实现框架,以及相关数据库的JDBC
驱动:
<dependencies>
<dependency>
<groupId>yang.yu.tmall</groupId>
<artifactId>tmall-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>javax.transaction-api</artifactId>
<version>1.3</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<scope>runtime</scope>
<exclusions>
<exclusion>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.5</version>
</dependency>
Spring配置
需要类或xml
形式定义Spring
配置,创建JPA
和spring-data-jpa
相关的基础设施。此处采用配置类形式的Spring
配置:
@Configuration
@ComponentScan("yang.yu.tmall")
@EnableJpaRepositories(basePackages = {"yang.yu.tmall.repository"})
@EnableTransactionManagement
@PropertySource("/jdbc.properties")
public class JpaSpringConfig {
private Environment env;
public JpaSpringConfig(Environment env) {
this.env = env;
}
@Bean(destroyMethod = "close")
public ComboPooledDataSource dataSource() throws Exception {
ComboPooledDataSource result = new ComboPooledDataSource();
result.setDriverClass(env.getProperty("jdbc.driverClassName"));
result.setJdbcUrl(env.getProperty("jdbc.url"));
result.setUser(env.getProperty("jdbc.username"));
result.setPassword(env.getProperty("jdbc.password", ""));
return result;
}
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter result = new HibernateJpaVendorAdapter();
result.setDatabase(Database.H2);
result.setDatabasePlatform(env.getProperty("hibernate.dialect"));
result.setGenerateDdl(true);
result.setShowSql(true);
return result;
}
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource, JpaVendorAdapter adapter) {
LocalContainerEntityManagerFactoryBean result = new LocalContainerEntityManagerFactoryBean();
result.setDataSource(dataSource);
result.setJpaVendorAdapter(adapter);
result.setPackagesToScan("yang.yu.tmall.domain");
result.setJpaPropertyMap(hibernateProperties());
return result;
}
private Map<String, String> hibernateProperties() {
Map<String, String> props = new HashMap<>();
props.put("hibernate.implicit_naming_strategy", "jpa");
return props;
}
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
定义JpaRepository接口的子类,同时实现实体仓储接口
在tmall-domain
模块中,我们定义了一个定价仓储接口:
/**
* 定价仓储接口
*/
public interface Pricings {
/**
* 保存定价信息
* @param pricing 要保存的定价信息
* @return 持久化后的定价信息
*/
Pricing save(Pricing pricing);
/**
* 获得指定产品在指定时间的定价信息
* @param product 产品
* @param time 时间
* @return 当时的产品价格
*/
Optional<Pricing> getPricingAt(Product product, LocalDateTime time);
}
定价实体Pricing
记录产品每次调价的信息。其定义如下:
@Entity
@Table(name = "pricings")
public class Pricing extends BaseEntity {
@ManyToOne
private Product product; //产品
private Money unitPrice; //单价
@Column(name = "effective_time")
private LocalDateTime effectiveTime; //生效时间
public Pricing() {
}
public Pricing(Product product, Money unitPrice, LocalDateTime effectiveTime) {
this.product = product;
this.unitPrice = unitPrice;
this.effectiveTime = effectiveTime;
}
public Pricing(Product product, Money unitPrice) {
this(product, unitPrice, LocalDateTime.now());
}
public Product getProduct() {
return product;
}
public Money getUnitPrice() {
return unitPrice;
}
public LocalDateTime getEffectiveTime() {
return effectiveTime;
}
}
我们定义一个接口PricingRepository
(注意是接口而不是实现类),同时扩展了Pricings
和JpaRepository
:
@Named
public interface PricingRepository extends Pricings, JpaRepository<Pricing, Integer> {
@Override
default Optional<Pricing> getPricingAt(Product product, LocalDateTime time) {
return findFirstByProductAndEffectiveTimeIsLessThanEqualOrderByEffectiveTimeDesc(product, time);
}
Optional<Pricing> findFirstByProductAndEffectiveTimeIsLessThanEqualOrderByEffectiveTimeDesc(Product product, LocalDateTime time);
}
说明如下:
- 这个接口扩展了
Pricings
和JpaRepository
。后者提供了很多预定义的CRUD
方法。 spring-data-jpa
提供的JpaRepository
已经提供了Pricings
接口中定义的save()
方法。因此PricingRepository
不再需要提供这个方法。- 由于
Pricings
接口的方法getPricingAt()
不符合spring-data-jpa
的方法命名规范,不能由spring-data-jpa
在运行时自动提供实现,因此我做了一个转换。我在PricingRepository
中定义了一个符合spring-data-jpa
规范的方法名findFirstByProduct AndEffectiveTimeIsLessThanEqualOrderByEffectiveTimeDesc()
,同时利用Java 8
引进的默认方法特性定义了Pricings
接口的同名默认方法getPricingAt()
,它直接调用findFirstByProductAndEffectiveTimeIsLessThanEqualOrderByEffectiveTime Desc()
方法。 findFirstByProductAndEffectiveTimeIsLessThanEqualOrderByEffectiveTimeDesc()
方法由spring-data-jpa
在运行时自动生成实现类。它查询数据库,返回关联指定产品的、生效时间不晚于指定时间的、按生效时间降序排列的定价实体的列表中的第一个定价实体(如果有的话)。- 通过给
PricingRepository
类标注@Named
注解,将它定义为一个由Spring
容器管理的bean
。可以把它注入到它的客户类中,供后者使用。@Named
注解由依赖注入规范JSR-330
定义。为什么不使用Spring
中等价的@Bean
注解?首先是因为JSR-330
是依赖注入的规范,规范优先于实现;其次是因为@Bean
只能注解类而不能注解接口。
集成测试
我们用集成测试作为仓储接口的客户类,来测试spring-data-jpa
是否正确实现:
@SpringJUnitConfig(classes = JpaSpringConfig.class)
@Transactional
public class PricingRepositoryTest implements WithAssertions {
@Inject
private Pricings pricings;
@Inject
private EntityManager entityManager;
private Product product1, product2;
private Pricing pricing1, pricing2, pricing3, pricing4;
@BeforeEach
void beforeEach() {
product1 = entityManager.merge(new Product("电冰箱", null));
product2 = entityManager.merge(new Product("电视机", null));
pricing1 = entityManager.merge(new Pricing(product1, Money.valueOf(500), LocalDate.of(2020,10, 1).atStartOfDay()));
pricing2 = entityManager.merge(new Pricing(product1, Money.valueOf(600), LocalDate.of(2020,2, 15).atStartOfDay()));
pricing3 = entityManager.merge(new Pricing(product2, Money.valueOf(7000), LocalDate.of(2020,7, 14).atStartOfDay()));
pricing4 = entityManager.merge(new Pricing(product2, Money.valueOf(7100), LocalDate.of(2020,2, 15).atStartOfDay()));
}
@AfterEach
void afterEach() {
Arrays.asList(product1, product2, pricing1, pricing2, pricing3, pricing4)
.forEach(entityManager::remove);
}
@Test
void getPriceAt() {
LocalDateTime time2002_02_15 = LocalDate.of(2020, 2, 15).atStartOfDay();
LocalDateTime time2002_02_16 = LocalDate.of(2020, 2, 16).atStartOfDay();
LocalDateTime time2002_10_01 = LocalDate.of(2020, 10, 1).atStartOfDay();
assertThat(pricings.getPricingAt(product1, time2002_02_15))
.map(Pricing::getUnitPrice)
.contains(Money.valueOf(600));
assertThat(pricings.getPricingAt(product1, time2002_02_16))
.map(Pricing::getUnitPrice)
.contains(Money.valueOf(600));
assertThat(pricings.getPricingAt(product1, time2002_10_01))
.map(Pricing::getUnitPrice)
.contains(Money.valueOf(500));
}
}
这个集成测试类使用了spring-test
,因此可以实现依赖注入(通过JSR-330
注解@Inject
)和事务管理(通过@Transactional
注解)。
无法自动实现的方法
并非所有的查询方法都能通过spring-data-jpa
来自动生成实现类。这个时候也有解决方法。下面是一个例子:
订单仓储接口Orders
中有一个findByOrgBuyers()
方法,用于查找买家类型为机构买家的所有订单:
public interface Orders {
...
Stream<Order> findByBuyer(Buyer buyer);
Stream<Order> findByOrgBuyers();
...
}
Orders
接口中的findByBuyer()
方法符合spring-data-jpa
规范,可由spring-data-jpa
自动生成实现;而findByOrgBuyers()
方法无法自动生成实现。
这时可以定义一个接口,此处命名为OrderOperations
,它定义了同样的findByOrgBuyers()
方法:
public interface OrderOperations {
Stream<Order> findByOrgBuyers();
}
同时提供它的实现类,命名为接口名加Impl
后缀:
public class OrderOperationsImpl implements OrderOperations {
private EntityManager entityManager;
public OrderOperationsImpl(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public Stream<Order> findByOrgBuyers() {
String jpql = "select o from Order o join o.buyer b where TYPE(b) = OrgBuyer";
return entityManager.createQuery(jpql, Order.class)
.getResultStream();
}
}
它可以依赖注入Spring
管理的任何bean
。此处通过构造函数注入一个EntityManager
,由EntityManager
实现findByOrgBuyers()
方法。
我们的spring-data-jpa
实现的仓储OrderRepository
,除了扩展Orders
和JpaRepository
之外,还要扩展OrderOperations
:
@Named
public interface OrderRepository extends Orders, JpaRepository<Order, Integer>, OrderOperations {
Stream<Order> findByBuyer(Buyer buyer);
}
在运行时,spring-data-jpa
会自动为标准方法findByBuyer()
生成实现,并且将对OrderRepository
的findByOrgBuyers()
方法调用转发给OrderOperationsImpl
的findByOrgBuyers()
方法去实现。
总之,使用spring-data-jpa
可以:
- 对于基于属性的简单查询和排序,可以在运行时自动生成实现;
- 对于复杂查询,可以自定义实现方法,由
spring-data-jpa
调用。