SpringBoot-SpringData JPA框架

1. SpringData JPA简介

Spring Data JPA是Spring Framework家族的一部分,它是Spring Data模块中的一个子项目,主要用于简化基于JPA的开发。Spring Data JPA可以帮助我们快速实现CRUD操作,并提供额外的功能,如分页、查询DSL、动态查询等。简单来说,使用JPA,几乎可以不用编写任何sq语句。

在之前,我们使用JDBC或是Mybatis来操作数据,通过直接编写对应的SQL语句来实现数据访问,但是我们发现实际上我们在Java中大部分操作数据库的情况都是读取数据并封装为一个实体类,因此,为什么不直接将实体类直接对应到一个数据库表呢?也就是说,一张表里面有什么属性,那么我们的对象就有什么属性,所有属性跟数据库里面的字段一一对应,而读取数据时,只需要读取一行的数据并封装为我们定义好的实体类既可以,而具体的SQL语句执行,完全可以交给框架根据我们定义的映射关系去生成,不再由我们去编写,因为这些SQL实际上都是千篇一律的。
而实现JPA规范的框架一般最常用的就是Hibernate,它是一个重量级框架,学习难度相比Mybatis也更高一些,而SpringDataJPA也是采用Hibernate框架作为底层实现,并对其加以封装。

官网:https://spring.io/projects/spring-data-jpa

2. SpringData JPA的使用

2.1 引入依赖

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

2.2 配置

使用application.properties配置

spring.datasource.url=jdbc:mysql://localhost:3306/book?useSSL=false
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

<!-- spring.jpa.hibernate.ddl-auto=update:在应用程序启动时,Hibernate 会根据实体类自动更新数据库表结构,但不会删除现有数据。 -->
<!-- spring.jpa.show-sql=true:启用 SQL 查询的显示,方便调试。 -->
<!-- spring.jpa.properties.hibernate.format_sql=true:格式化 SQL 查询输出,使日志更易读。 -->
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

使用application.yml配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/book?useSSL=false
    username: root
    password:
    driver-class-name: com.mysql.jdbc.Driver
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true

ddl-auto属性用于设置自动表定义,可以实现自动在数据库中为我们创建一个表,表的结构会根据我们定义的实体类决定,它有以下几种:

  • none: 不执行任何操作,数据库表结构需要手动创建。
  • create: 框架在每次运行时都会删除所有表,并重新创建。
  • create-drop: 框架在每次运行时都会删除所有表,然后再创建,但在程序结束时会再次删除所有表。
  • update: 框架会检查数据库表结构,如果与实体类定义不匹配,则会做相应的修改,以保持它们的一致性。
  • validate: 框架会检查数据库表结构与实体类定义是否匹配,如果不匹配,则会抛出异常。

2.3 实体类配置

不需要去创建数据库,这里配置好实体类,运行后,JPA框架 会根据配置自动创建数据库表。

package com.demo.springboot_base.pojo;

import jakarta.persistence.*;
import lombok.Data;
import lombok.experimental.Accessors;

/**
 * 酒店实体类,用于映射数据库中的酒店表
 */
@Data
@Entity
@Table(name = "hotel") // 指定数据库名称为 "hotel"
@Accessors(chain = true) // 启用链式调用
public class Hotel {

    @Id // 标识主键字段
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 主键生成策略为自增
    @Column(name = "id") // 映射到数据库表的 "id" 列
    private Integer id;

    @Column(name = "name") // 映射到数据库表的 "name" 列
    private String name;

    @Column(name = "address") // 映射到数据库表的 "address" 列
    private String address;

    @Column(name = "rent") // 映射到数据库表的 "rent" 列
    private Double rent;
}

2.4 编写Repository接口

编写Repository接口(类似于Mybatis中的mapper接口),继承JpaRepository,并指定实体类类型和主键类型。

package com.demo.springboot_base.repo;

import com.demo.springboot_base.pojo.Hotel;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
 * 使用@Repository注解,将该类注册为Spring Bean
 */
@Repository
public interface HotelRepository extends JpaRepository<Hotel, Integer> {
    Hotel findByNameLike(String name);
}

2.5 测试运行

因为这里仅作演示,不涉及复杂的业务场景,所以不再写Service层,直接在单元测试中进行测试。

	@Resource
	HotelRepository hotelRepository;
	
    @Test
	void contextLoads() {
        //jpa提供了很多内置方法,不需要我们进行任何操作就可以使用,例如:findAll()、findById()、save()、delete()等。

        //这里作为演示,从插入数据开始
        Hotel hotel = hotelRepository.save(new Hotel().setName("吉果酒店").setAddress("太原市迎泽区万邦国际1705").setRent(78.4));
		System.out.println("我刚刚插入的数据:" + hotel);

        //修改数据,因为插入和修改都使用的save,所以需要改对象来实现修改
		hotel.setAddress("太原市迎泽区万邦国际1205");
        //传入修改后的对象,实现修改操作
		Hotel hotel1 = hotelRepository.save(hotel);
		System.out.println("我刚刚修改的数据:" + hotel1);

        //根据id查询数据
		System.out.println("根据id查询数据 = " + hotelRepository.findById(hotel1.getId()));

        // 再新增一个数据
		Hotel hotel2 = hotelRepository.save(new Hotel().setName("红苹果酒店").setAddress("太原市迎泽区万邦国际2102").setRent(88.5));
		System.out.println("我刚刚又插入了一条数据:" + hotel2);

        //查询数据库全部数据
		System.out.println("直接查询全部 = " + hotelRepository.findAll());

        //根据传入的对象删除数据
		hotelRepository.delete(hotel2);

        //删除后的数据库中当前的数据
		System.out.println("使用对象删除数据后现在的数据 = " + hotelRepository.findAll());

        //根据id删除数据
		hotelRepository.deleteById(hotel1.getId());

        //删除后的数据库中当前的数据
		System.out.println("根据id删除数据后现在的数据 = " + hotelRepository.findAll());

	}

输出日志:

2024-11-11T15:57:11.665+08:00  INFO 10300 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
Hibernate: 
    create table hotel (
        id integer not null auto_increment,
        address varchar(255),
        name varchar(255),
        rent float(53),
        primary key (id)
    ) engine=InnoDB
2024-11-11T15:57:11.744+08:00  INFO 10300 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-11-11T15:57:12.033+08:00  WARN 10300 --- [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-11-11T15:57:12.066+08:00  INFO 10300 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page template: index
2024-11-11T15:57:13.014+08:00  INFO 10300 --- [           main] c.d.s.SpringbootBaseApplicationTests     : Started SpringbootBaseApplicationTests in 4.783 seconds (process running for 5.857)
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
Hibernate: 
    insert 
    into
        hotel
        (address, name, rent) 
    values
        (?, ?, ?)
我刚刚插入的数据:Hotel(id=1, name=吉果酒店, address=太原市迎泽区万邦国际1705, rent=78.4)
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.id=?
Hibernate: 
    update
        hotel 
    set
        address=?,
        name=?,
        rent=? 
    where
        id=?
我刚刚修改的数据:Hotel(id=1, name=吉果酒店, address=太原市迎泽区万邦国际1205, rent=78.4)
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.id=?
根据id查询数据 = Optional[Hotel(id=1, name=吉果酒店, address=太原市迎泽区万邦国际1205, rent=78.4)]
Hibernate: 
    insert 
    into
        hotel
        (address, name, rent) 
    values
        (?, ?, ?)
我刚刚又插入了一条数据:Hotel(id=2, name=红苹果酒店, address=太原市迎泽区万邦国际2102, rent=88.5)
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0
直接查询全部 = [Hotel(id=1, name=吉果酒店, address=太原市迎泽区万邦国际1205, rent=78.4), Hotel(id=2, name=红苹果酒店, address=太原市迎泽区万邦国际2102, rent=88.5)]
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.id=?
Hibernate: 
    delete 
    from
        hotel 
    where
        id=?
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0
使用对象删除数据后现在的数据 = [Hotel(id=1, name=吉果酒店, address=太原市迎泽区万邦国际1205, rent=78.4)]
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.id=?
Hibernate: 
    delete 
    from
        hotel 
    where
        id=?
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0
根据id删除数据后现在的数据 = []

2.6 分析输出日志

  1. 配置完了实体类,因为当前数据库中没有对应的表,所以会根据配置进行自动创建
Hibernate: 
    create table hotel (
        id integer not null auto_increment,
        address varchar(255),
        name varchar(255),
        rent float(53),
        primary key (id)
    ) engine=InnoDB
  1. 插入了一条数据,并打印出来
Hibernate: 
    insert 
    into
        hotel
        (address, name, rent) 
    values
        (?, ?, ?)
我刚刚插入的数据:Hotel(id=1, name=吉果酒店, address=太原市迎泽区万邦国际1705, rent=78.4)
  1. 修改了一条数据,并打印出来,因为我使用的是刚刚新增返回的数据,所以再传递进去后,会先查询数据库中有没有这条记录,如果有,则会进行修改操作。
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.id=?
Hibernate: 
    update
        hotel 
    set
        address=?,
        name=?,
        rent=? 
    where
        id=?
我刚刚修改的数据:Hotel(id=1, name=吉果酒店, address=太原市迎泽区万邦国际1205, rent=78.4)
  1. 根据id查询数据
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.id=?
根据id查询数据 = Optional[Hotel(id=1, name=吉果酒店, address=太原市迎泽区万邦国际1205, rent=78.4)]
  1. 新增了一条数据
Hibernate: 
    insert 
    into
        hotel
        (address, name, rent) 
    values
        (?, ?, ?)
我刚刚又插入了一条数据:Hotel(id=2, name=红苹果酒店, address=太原市迎泽区万邦国际2102, rent=88.5)
  1. 查询全部数据
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0
直接查询全部 = [Hotel(id=1, name=吉果酒店, address=太原市迎泽区万邦国际1205, rent=78.4), Hotel(id=2, name=红苹果酒店, address=太原市迎泽区万邦国际2102, rent=88.5)]
  1. 根据传入的对象删除数据,同修改一样,传入的是对象,所以会先进行查询,存在则会进行删除,然后再查询一次,查看删除是否成功。
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.id=?
Hibernate: 
    delete 
    from
        hotel 
    where
        id=?
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0
使用对象删除数据后现在的数据 = [Hotel(id=1, name=吉果酒店, address=太原市迎泽区万邦国际1205, rent=78.4)]
  1. 根据id删除数据,同上面传入对象一样,先进行查询,存在则会进行删除,然后再查询一次,查看删除是否成功。
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.id=?
Hibernate: 
    delete 
    from
        hotel 
    where
        id=?
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0
根据id删除数据后现在的数据 = []

3. 使用JPA编写方法名实现拼接自定义SQL语句

看上去比较绕口,简单来说,就是JPA提供了一些关联sql语句的关键词,我们在repository中可以直接使用这些关键词,这样jpa就会根据关键词自动拼接sql语句,来实现一些复杂的查询功能。

属性对应SQL语句拼接方法名示例解释
Distinctselect distinctfindDistinctBy...查询结果去重
Andwhere ... and ...findByNameAndAddress多条件查询,并且
Orwhere ... or ...findByNameOrAddress多条件查询,或者
Betweenwhere ... between ...findByNameBetween范围查询
Is、Equalswhere name= ?1findByNameIsNull判断是否为空
Notwhere ... not ...findByNameNot排除查询
Likewhere ... like ...findByNameLike模糊查询
Order Byorder by ...findByNameOrderByRentDesc排序
Group Bygroup by ...findByNameGroupByAddress分组
Havinghaving ...findByNameHavingMaxRent分组过滤
LessThanwhere ... < ...findByNameLessThan小于
LessThanEqualwhere ... <= ...findByNameLessThanEqual小于等于
GreaterThanwhere ... > ...findByNameGreaterThan大于
GreaterThanEqualwhere ... >= ...findByNameGreaterThanEqual大于等于
IsNull,NULLwhere name is nullfindByNameIsNull判断是否为空
IsNotNull,NOT NULLwhere name is not nullfindByNameIsNotNull判断是否不为空
StartsWithwhere name like ?1findByNameStartsWith以...开头
EndsWithwhere name like ?1findByNameEndsWith以...结尾
Containswhere name like ?1findByNameContains包含...

这里举个例子,例如我们要根据名字和地址模糊查询,并且按照租金降序排序,可以使用如下方法:

在Repository接口中定义方法

    List<Hotel> findByNameLikeAndAddressLikeOrderByRentDesc(String name, String address);

测试方法

	@Test
	void test01() {
		System.out.println(hotelRepository.findByNameLikeAndAddressLikeOrderByRentDesc("%酒店%", "%太原%"));
	}

输出

Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.name like ? escape '\\' 
        and h1_0.address like ? escape '\\' 
    order by
        h1_0.rent desc
[Hotel(id=4, name=红苹果酒店, address=太原市迎泽区万邦国际2102, rent=88.5), Hotel(id=3, name=吉果酒店, address=太原市迎泽区万邦国际1205, rent=78.4)]

可以看到,我们没有编写任何sql语句,仅仅只是使用了JPA提供的关键词来编写方法,jpa自动拼接了sql语句,并执行了查询。但是缺点也很明显,就是只能使用这些关键词,如果我们需要更复杂的查询,就需要自己编写sql语句了。

4. JPQL自定义SQL语句

JPQL(Java Persistence Query Language)是Java持久化查询语言,是一种对象查询语言,用于在Java应用程序中查询持久化对象。JPQL是基于SQL的,但是它提供了更丰富的查询功能,例如:

  • 关系表达式:可以查询对象之间的关系,例如:查询所有员工的部门信息。
  • 聚合函数:可以对查询结果进行聚合操作,例如:查询所有员工的工资总和。
  • 排序:可以对查询结果进行排序,例如:查询所有员工按工资降序排序。
  • 子查询:可以嵌套查询,例如:查询所有员工的部门信息,并且部门信息中有员工的数量大于2。
  • 命名参数:可以给查询参数命名,例如:查询所有员工的名字为“张三”的员工信息。

JPQL的语法和SQL类似,但是有一些差别,例如:

  • 关键字:JPQL使用关键字where、select、from、and、or等,而SQL使用关键字select、from、where等。
  • 大小写敏感:JPQL是大小写敏感的,而SQL是大小写不敏感的。
  • 注释:JPQL不支持注释,而SQL支持注释。
  • 占位符:JPQL使用问号作为占位符,而SQL使用冒号。

下面我们来看看如何使用JPQL编写自定义SQL语句。

4.1 编写JPQL语句

编写JPQL语句,需要先定义实体类,然后在repository接口中定义方法,方法中编写JPQL语句。

在repository接口中定义方法

@Repository
public interface HotelRepository extends JpaRepository<Hotel, Long> {
    @Transactional    //DML操作需要事务环境,可以不在这里声明,但是调用时一定要处于事务环境下
    @Modifying     //表示这是一个DML操作
    // 1. 第一种  使用 ?1  ?2 来代表第一个参数,第二个参数
    //这里操作的是一个实体类对应的表,参数使用?代表,后面接第n个参数
    @Query("select h from Hotel h where h.name like ?1 and h.address like ?2 order by h.rent desc")
    List<Hotel> findByNameAndAddress(String name,String address);
    // 2. 第二种  使用 :name  :address 来代表参数,方法里使用@param注解来指定参数名
//    @Query("select h from Hotel h where h.name like :name and h.address like :address order by h.rent desc")
//    List<Hotel> findByNameAndAddress(@Param("name") String name, @Param("address") String address);
}

4.2 运行测试

	@Test
	void test02() {
		System.out.println(hotelRepository.findByNameAndAddress("%酒店%", "%太原%"));
	}

输出

Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.name like ? escape '\\' 
        and h1_0.address like ? escape '\\' 
    order by
        h1_0.rent desc

可以看到,JPQL自动拼接了sql语句,并执行了查询。

4.3 分析输出日志

  1. 定义了实体类,并创建了表
Hibernate: 
    create table hotel (
        id bigint not null,
        address varchar(255),
        name varchar(255),
        rent float(53),
        primary key (id)
    )
  1. 定义了repository接口,并定义了方法,方法中编写了JPQL语句
Hibernate: 
    select
        h1_0 
    from
        Hotel h1_0 
    where
        h1_0.name like ? escape '\\' 
        and h1_0.address like ? escape '\\' 
    order by
        h1_0.rent desc
  1. 执行了查询,并返回结果
Hibernate: 
    select
        h1_0.id,
        h1_0.address,
        h1_0.name,
        h1_0.rent 
    from
        hotel h1_0 
    where
        h1_0.name like ? escape '\\' 
        and h1_0.address like ? escape '\\' 
    order by
        h1_0.rent desc

可以看到,JPQL自动拼接了sql语句,并执行了查询。