Springboot实战10 ORM 集成:如何使用 Spring Data JPA 访问关系型数据库?

在前面的课程中,我们详细介绍了如何使用 Spring 所提供的 JdbcTemplate 模板工具类实现数据访问的实现方法。相较 JDBC 所提供的原生 API,JdbcTemplate 做了一层封装,大大简化了数据的操作过程。而在 09 讲中,我们又进一步引入了 Spring Data 框架,可以说 Spring Data 框架是基于 JdbcTemplate 上另一层更高级的封装。

今天,我们将基于 Spring Data 中的 Spring Data JPA 组件介绍如何集成 ORM 框架实现关系型数据库访问。

引入 Spring Data JPA

如果你想在应用程序中使用 Spring Data JPA,首先需要在 pom 文件中引入 spring-boot-starter-data-jpa 依赖,如下代码所示:

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

在介绍这一组件的使用方法之前,我们有必要对 JPA 规范进行一定的了解。

JPA 全称是 JPA Persistence API,即 Java 持久化 API,它是一个 Java 应用程序接口规范,用于充当面向对象的领域模型和关系数据库系统之间的桥梁,属于一种 ORM(Object Relational Mapping,对象关系映射)技术。

JPA 规范中定义了一些既定的概念和约定,集中包含在 javax.persistence 包中,常见的如对实体(Entity)定义、实体标识定义、实体与实体之间的关联关系定义,以及 09 讲中介绍的 JPQL 定义等,关于这些定义及其使用方法,一会儿我们会详细展开说明。

与 JDBC 规范一样,JPA 规范也有一大批实现工具和框架,极具代表性的如老牌的 Hibernate 及今天我们将介绍的 Spring Data JPA。

为了演示基于 Spring Data JPA 的整个开发过程,我们将在 SpringCSS 案例中专门设计和实现一套独立的领域对象和 Repository,接下来我们一起来看下。

实体类注解

我们知道 order-service 中存在两个主要领域对象,即 Order 和 Goods。为了与前面课时介绍的领域对象有所区分,本节课我们重新创建两个领域对象,分别命名为 JpaOrder 和 JpaGoods,它们就是 JPA 规范中的实体类。

我们先来看下相对简单的 JpaGoods,这里我们把 JPA 规范的相关类的引用罗列在了一起,JpaGoods 定义如下代码所示:

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
 
@Entity
@Table(name="goods")
public class JpaGoods {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;    
    private String goodsCode;
    private String goodsName;
    private Float price;    
    //省略 getter/setter
}

JpaGoods 中使用了 JPA 规范中用于定义实体的几个注解:最重要的 @Entity 注解、用于指定表名的 @Table 注解、用于标识主键的 @Id 注解,以及用于标识自增数据的 @GeneratedValue 注解,这些注解都比较直白,在实体类上直接使用即可。

接下来,我们看下比较复杂的 JpaOrder,定义如下代码所示:

@Entity
@Table(name="`order`")
public class JpaOrder implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String orderNumber;
    private String deliveryAddress;
 
    @ManyToMany(targetEntity=JpaGoods.class)
    @JoinTable(name = "order_goods", joinColumns = @JoinColumn(name = "order_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "goods_id", referencedColumnName = "id"))
    private List<JpaGoods> goods = new ArrayList<>();
 
    //省略 getter/setter
}

这里除了引入了常见的一些注解,还引入了 @ManyToMany 注解,它表示 order 表与 goods 表中数据的关联关系。

在JPA 规范中,共提供了 one-to-one、one-to-many、many-to-one、many-to-many 这 4 种映射关系,它们分别用来处理一对一、一对多、多对一,以及多对多的关联场景。

针对 order-service 这个业务场景,我们设计了一张 order_goods 中间表存储 order 与 goods 表中的主键关系,且使用了 @ManyToMany 注解定义 many-to-many 这种关联关系,也使用了 @JoinTable 注解指定 order_goods 中间表,并通过 joinColumns 和 inverseJoinColumns 注解分别指定中间表中的字段名称以及引用两张主表中的外键名称。

定义 Repository

定义完实体对象后,我们再来提供 Repository 接口,这一步的操作非常简单,OrderJpaRepository 的定义如下代码所示:

@Repository("orderJpaRepository")
public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>
{
}

从上面代码中我们发现,OrderJpaRepository 是一个继承了 JpaRepository 接口的空接口,基于 09 讲的介绍,我们知道 OrderJpaRepository 实际上已经具备了访问数据库的基本 CRUD 功能。

使用 Spring Data JPA 访问数据库

有了上面定义的 JpaOrder 和 JpaGoods 实体类,以及 OrderJpaRepository 接口,我们已经可以实现很多操作了。

比如我们想通过 Id 获取 Order 对象,首先可以通过构建一个 JpaOrderService 直接注入 OrderJpaRepository 接口,如下代码所示:

@Service
public class JpaOrderService {
 
    @Autowired
    private OrderJpaRepository orderJpaRepository;
 
    public JpaOrder getOrderById(Long orderId) {
 
        return orderJpaRepository.getOne(orderId);
    }
}

然后,我们再通过构建一个 Controller 类嵌入上述方法,并通过 HTTP 请求查询 Id 为 1 的 JpaOrder 对象,获得的结果如下代码所示:

{
    "id"1,
    "orderNumber""Order10001",
    "deliveryAddress""test_address1",
    "goods": [
        {
            "id"1,
            "goodsCode""GoodsCode1",
            "goodsName""GoodsName1",
            "price"100.0
        },
        {
            "id"2,
            "goodsCode""GoodsCode2",
            "goodsName""GoodsName2",
            "price"200.0
        }
    ]
}

请注意,这里我们不仅获取了 order 表中的订单基础数据,还同时获取了 goods 表中的商品数据,这种效果是如何实现的呢?是因为在 JpaOrder 对象中,我们添加了 @ManyToMany 注解,该注解会自动从 order_goods 表中获取商品主键信息,并从 goods 表中获取商品详细信息。

了解了使用 Spring Data JPA 实现关系型数据库访问的过程,并对比《数据访问:如何使用 JdbcTemplate 访问关系型数据库?》中通过 JdbcTemplate 获取这部分数据的实现过程,我们发现使用 Spring Data JPA 更简单。

在多样化查询实现过程中,我们不仅可以使用 JpaRepository 中默认集成的各种 CRUD 方法,还可以使用 09 讲中介绍的 @Query 注解、方法名衍生查询等。今天,我们还将同时引入 QueryByExample 和 Specification 这两种机制来丰富多样化查询方式。

使用 @Query 注解

使用 @Query 注解实现查询的示例如下代码所示:

@Repository("orderJpaRepository")
public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>
{
 
    @Query("select o from JpaOrder o where o.orderNumber = ?1")
    JpaOrder getOrderByOrderNumberWithQuery(String orderNumber);
}

这里,我们使用了 JPQL 根据 OrderNumber 查询订单信息。JPQL 的语法与 SQL 语句非常类似,09 讲中我们对 JPQL 进行了讨论,这里我们不再赘述,你可以前往回顾。

说到 @Query 注解,JPA 中还提供了一个 @NamedQuery 注解对 @Query 注解中的语句进行命名。@NamedQuery 注解的使用方式如下代码所示:

@Entity
@Table(name = "`order`")
@NamedQueries({ @NamedQuery(name = "getOrderByOrderNumberWithQuery", query = "select o from JpaOrder o where o.orderNumber = ?1") })
public class JpaOrder implements Serializable {

在上述示例中,我们在实体类 JpaOrder 上添加了一个 @NamedQueries 注解,该注解可以将一批 @NamedQuery 注解整合在一起使用。同时,我们还使用了 @NamedQuery 注解定义了一个“getOrderByOrderNumberWithQuery”查询,且指定了对应的 JPQL 语句。

如果你想使用这个命名查询,在 OrderJpaRepository 中定义与该命名一致的方法即可。

使用方法名衍生查询

使用方法名衍生查询是最方便的一种自定义查询方式,在这过程中开发人员唯一需要做的就是在 JpaRepository 接口中定义一个符合查询语义的方法。

比如我们希望通过 OrderNumber 查询订单信息,那么可以提供如下代码所示的接口定义:

@Repository("orderJpaRepository")
public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>
{
    JpaOrder getOrderByOrderNumber(String orderNumber);
}

通过 getOrderByOrderNumber 方法后,我们就可以自动根据 OrderNumber 获取订单详细信息了。

使用 QueryByExample 机制

接下来我们将介绍另一种强大的查询机制,即 QueryByExample(QBE)机制。

针对 JpaOrder 对象,假如我们希望根据 OrderNumber 及 DeliveryAddress 中的一个或多个条件进行查询,按照方法名衍生查询的方式构建查询方法后,得到如下代码所示的方法定义:

List<JpaOrder> findByOrderNumberAndDeliveryAddress (String 
	orderNumber, String deliveryAddress);

如果查询条件中使用的字段非常多,上面这个方法名可能非常长,且还需要设置一批参数,这种查询方法定义显然存在缺陷。

因为不管查询条件有多少个,我们都需要把所有参数进行填充,哪怕部分参数并没有被用到。而且,如果将来我们需要再添加一个新的查询条件,该方法必须做调整,从扩展性上讲也存在设计缺陷。为了解决这些问题,我们便可以引入 QueryByExample 机制。

QueryByExample 可以翻译为按示例查询,是一种用户友好的查询技术。它允许我们动态创建查询,且不需要编写包含字段名称的查询方法,也就是说按示例查询不需要使用特定的数据库查询语言来编写查询语句。

从组成结构上讲,QueryByExample 包括 Probe、ExampleMatcher 和 Example 这三个基本组件。其中, Probe 包含对应字段的实例对象,ExampleMatcher 携带有关如何匹配特定字段的详细信息,相当于匹配条件,Example 则由 Probe 和 ExampleMatcher 组成,用于构建具体的查询操作。

现在,我们基于 QueryByExample 机制重构根据 OrderNumber 查询订单的实现过程。

首先,我们需要在 OrderJpaRepository 接口的定义中继承 QueryByExampleExecutor 接口,如下代码所示:

@Repository("orderJpaRepository")
public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>, QueryByExampleExecutor<JpaOrder> {

然后,我们在 JpaOrderService 中实现如下代码所示的 getOrderByOrderNumberByExample 方法:

public JpaOrder getOrderByOrderNumberByExample(String orderNumber) {
        JpaOrder order = new JpaOrder();
        order.setOrderNumber(orderNumber);
 
        ExampleMatcher matcher = ExampleMatcher.matching().withIgnoreCase()
                .withMatcher("orderNumber", GenericPropertyMatchers.exact()).withIncludeNullValues();
 
        Example<JpaOrder> example = Example.of(order, matcher);
 
        return orderJpaRepository.findOne(example).orElse(new JpaOrder());
}

上述代码中,我们首先构建了一个 ExampleMatcher 对象用于初始化匹配规则,然后通过传入一个 JpaOrder 对象实例和 ExampleMatcher 实例构建了一个 Example 对象,最后通过 QueryByExampleExecutor 接口中的 findOne() 方法实现了 QueryByExample 机制。

使用 Specification 机制

本节课中,最后我们想介绍的查询机制是 Specification 机制。

先考虑这样一种场景,比如我们需要查询某个实体,但是给定的查询条件不固定,此时该怎么办?这时我们通过动态构建相应的查询语句即可,而在 Spring Data JPA 中可以通过 JpaSpecificationExecutor 接口实现这类查询。相比使用 JPQL 而言,使用 Specification 机制的优势是类型安全。

继承了 JpaSpecificationExecutor 的 OrderJpaRepository 定义如下代码所示:

@Repository("orderJpaRepository")
public interface OrderJpaRepository extends JpaRepository<JpaOrder, Long>,     JpaSpecificationExecutor<JpaOrder>{

对于 JpaSpecificationExecutor 接口而言,它背后使用的就是 Specification 接口,且 Specification 接口核心方法就一个,我们可以简单地理解该接口的作用就是构建查询条件,如下代码所示:

Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);

其中 Root 对象代表所查询的根对象,我们可以通过 Root 获取实体的属性,CriteriaQuery 代表一个顶层查询对象,用来实现自定义查询,而 CriteriaBuilder 用来构建查询条件。

基于 Specification 机制,我们同样对根据 OrderNumber 查询订单的实现过程进行重构,重构后的 getOrderByOrderNumberBySpecification 方法如下代码所示:

public JpaOrder getOrderByOrderNumberBySpecification(String orderNumber) {
        JpaOrder order = new JpaOrder();
        order.setOrderNumber(orderNumber);
 
        @SuppressWarnings("serial")
        Specification<JpaOrder> spec = new Specification<JpaOrder>() {
            @Override
            public Predicate toPredicate(Root<JpaOrder> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                Path<Object> orderNumberPath = root.get("orderNumber");
              
                Predicate predicate = cb.equal(orderNumberPath, orderNumber);
                return predicate;
            }
        };
 
        return orderJpaRepository.findOne(spec).orElse(new JpaOrder());     
}

从上面示例中可以看到,在 toPredicate 方法中,首先我们从 root 对象中获取了“orderNumber”属性,然后通过 cb.equal 方法将该属性与传入的 orderNumber 参数进行了比对,从而实现了查询条件的构建过程。

小结与预告

10 讲中,我们主要对通过 Spring Data JPA 进行数据操作的方法和技巧做了一一介绍。

在 Spring Boot 中,我极力推荐使用 Spring Data JPA 实现对关系型数据库访问,因为它不仅具有 ORM 框架的通用功能,同时还添加了 QueryByExample 和 Specification 机制等扩展性功能,应用上简单而高效。

这里给你留一道思考题:在使用 Spring Data JPA 时,如何正确使用 QueryByExample 和 Specification 机制实现灵活的自定义查询?

介绍完数据访问层组件之后,我们将继续讨论如何实现服务与服务之间的远程方案,11 讲我们会先给出构建一个 RESTful 风格的 Web 服务的实现方法。


精选评论

*杰:

spring Data JPA 国外用的比较广,国内大多还是mybatis 或者mybatis plus

    讲师回复:

    对的,国内确实是 Mybatis 用的比较多。

**0206:

老师好,想问问@JoinTable注解,关系表一定要用外键吗?可以不用外键吗?

    讲师回复:

    外键更多是数据库层面的约束,@JoinTable注解主要设置表与表之间的关联关系,我理解并一定要用外键。现在很多应用在设计上都不设置数据库层面的外键,而是更多通过应用程序的代码控制表与表之间的关联关系了。

*旺:

好像很少用JPA的公司吧?

    讲师回复:

    国内感觉确实还是用 Mybatis 的多,据说国外 JPA 是主流。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Spring Data JPA 是什么? Spring Data JPASpring 框架中的一个模块,它提供了一种方便的方式来访问和操作数据库,同时也简化了开发人员的工作。它基于 JPA 规范,提供了一些常用的 CRUD 操作方法,同时也支持自定义查询和分页查询等功能。 Spring Data JPA 的优点是什么? Spring Data JPA 的优点包括: 1. 简化了数据访问层的开发,提高了开发效率。 2. 提供了一些常用的 CRUD 操作方法,减少了重复的代码编写。 3. 支持自定义查询和分页查询等功能,提高了查询效率。 4. 可以与 Spring 框架无缝集成,方便使用。 5. 支持多种数据库,包括关系数据库和 NoSQL 数据库等。 6. 提供了一些高级特性,如二级缓存、延迟加载等。 Spring Data JPA 的缺点是什么? Spring Data JPA 的缺点包括: 1. 学习曲线较陡峭,需要掌握 JPA 规范和 Spring 框架的相关知识。 2. 对于复杂的查询,需要编写自定义查询语句,增加了开发难度。 3. 对于大规模数据的查询和操作,可能会出现性能问题。 4. 对于一些特殊的需求,可能需要使用原生 SQL 或其他 ORM 框架来实现。 Spring Data JPA 和 Hibernate 有什么区别? Spring Data JPA 是基于 JPA 规范的,而 Hibernate 是一个 ORM 框架,它实现了 JPA 规范。因此,Spring Data JPA 和 Hibernate 之间的区别主要在以下几个方面: 1. Spring Data JPA 是一个数据访问层框架,而 Hibernate 是一个 ORM 框架。 2. Spring Data JPA 提供了一些常用的 CRUD 操作方法,而 Hibernate 更加灵活,可以编写任意复杂的查询语句。 3. Spring Data JPA 可以与 Spring 框架无缝集成,而 Hibernate 可以与任何 Java 应用程序集成。 4. Spring Data JPA 支持多种数据库,包括关系数据库和 NoSQL 数据库等,而 Hibernate 主要支持关系数据库。 5. Spring Data JPA 提供了一些高级特性,如二级缓存、延迟加载等,而 Hibernate 也提供了类似的特性。 如何使用 Spring Data JPA使用 Spring Data JPA 的步骤如下: 1. 添加依赖:在项目的 pom.xml 文件中添加 Spring Data JPA 的依赖。 2. 配置数据源:在 Spring 的配置文件中配置数据源。 3. 定义实体类:定义与数据库表对应的实体类,并使用 JPA 注解进行映射。 4. 定义 DAO 接口:定义一个继承 JpaRepository 接口的 DAO 接口。 5. 编写业务逻辑:在 Service 层中编写业务逻辑,调用 DAO 接口中的方法进行数据操作。 6. 运行程序:启动应用程序,测试数据访问和操作是否正常。 如何进行分页查询? 使用 Spring Data JPA 进行分页查询的步骤如下: 1. 在 DAO 接口中定义一个继承 PagingAndSortingRepository 接口的方法。 2. 在 Service 层中调用 DAO 接口中的分页查询方法,并指定分页参数。 3. 在控制器中接收分页参数,并将查询结果传递给前端页面。 4. 在前端页面中显示分页信息和查询结果。 如何进行自定义查询? 使用 Spring Data JPA 进行自定义查询的步骤如下: 1. 在 DAO 接口中定义一个自定义查询方法,并使用 @Query 注解指定查询语句。 2. 在 Service 层中调用 DAO 接口中的自定义查询方法。 3. 在控制器中接收查询结果,并将结果传递给前端页面。 4. 在前端页面中显示查询结果。 如何进行事务管理? 使用 Spring Data JPA 进行事务管理的步骤如下: 1. 在 Spring 的配置文件中配置事务管理器。 2. 在 Service 层中使用 @Transactional 注解标记需要进行事务管理的方法。 3. 在控制器中调用 Service 层中的方法。 4. 如果方法执行成功,则事务会自动提交,否则事务会自动回滚。 如何进行多表查询? 使用 Spring Data JPA 进行多表查询的步骤如下: 1. 在 DAO 接口中定义一个自定义查询方法,并使用 @Query 注解指定查询语句。 2. 在查询语句中使用 JOIN 关键字连接多个表。 3. 在 Service 层中调用 DAO 接口中的自定义查询方法。 4. 在控制器中接收查询结果,并将结果传递给前端页面。 5. 在前端页面中显示查询结果。 如何进行级联操作? 使用 Spring Data JPA 进行级联操作的步骤如下: 1. 在实体类中使用 @OneToMany 或 @ManyToOne 注解定义关联关系。 2. 在 Service 层中编写业务逻辑,调用 DAO 接口中的方法进行级联操作。 3. 在控制器中接收操作结果,并将结果传递给前端页面。 4. 在前端页面中显示操作结果。 如何进行缓存管理? 使用 Spring Data JPA 进行缓存管理的步骤如下: 1. 在 Spring 的配置文件中配置缓存管理器。 2. 在实体类中使用 @Cacheable 或 @CacheEvict 注解指定缓存策略。 3. 在 Service 层中编写业务逻辑,调用 DAO 接口中的方法进行数据操作。 4. 在控制器中接收操作结果,并将结果传递给前端页面。 5. 在前端页面中显示操作结果。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

源码头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值