第七节 通过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配置,创建JPAspring-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(注意是接口而不是实现类),同时扩展了PricingsJpaRepository

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

说明如下:

  • 这个接口扩展了PricingsJpaRepository。后者提供了很多预定义的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,除了扩展OrdersJpaRepository之外,还要扩展OrderOperations

@Named
public interface OrderRepository extends Orders, JpaRepository<Order, Integer>, OrderOperations {

    Stream<Order> findByBuyer(Buyer buyer);
}

在运行时,spring-data-jpa会自动为标准方法findByBuyer()生成实现,并且将对OrderRepositoryfindByOrgBuyers()方法调用转发给OrderOperationsImplfindByOrgBuyers()方法去实现。

总之,使用spring-data-jpa可以:

  • 对于基于属性的简单查询和排序,可以在运行时自动生成实现;
  • 对于复杂查询,可以自定义实现方法,由spring-data-jpa调用。

results matching ""

    No results matching ""