Java-SpringBoot之使用SpringData JPA框架
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框架作为底层实现,并对其加以封装。
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 分析输出日志
- 配置完了实体类,因为当前数据库中没有对应的表,所以会根据配置进行自动创建
Hibernate:
create table hotel (
id integer not null auto_increment,
address varchar(255),
name varchar(255),
rent float(53),
primary key (id)
) engine=InnoDB
- 插入了一条数据,并打印出来
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)
- 根据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)]
- 新增了一条数据
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)]
- 根据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语句 | 拼接方法名示例 | 解释 |
|---|---|---|---|
| Distinct | select distinct | findDistinctBy... | 查询结果去重 |
| And | where ... and ... | findByNameAndAddress | 多条件查询,并且 |
| Or | where ... or ... | findByNameOrAddress | 多条件查询,或者 |
| Between | where ... between ... | findByNameBetween | 范围查询 |
| Is、Equals | where name= ?1 | findByNameIsNull | 判断是否为空 |
| Not | where ... not ... | findByNameNot | 排除查询 |
| Like | where ... like ... | findByNameLike | 模糊查询 |
| Order By | order by ... | findByNameOrderByRentDesc | 排序 |
| Group By | group by ... | findByNameGroupByAddress | 分组 |
| Having | having ... | findByNameHavingMaxRent | 分组过滤 |
| LessThan | where ... < ... | findByNameLessThan | 小于 |
| LessThanEqual | where ... <= ... | findByNameLessThanEqual | 小于等于 |
| GreaterThan | where ... > ... | findByNameGreaterThan | 大于 |
| GreaterThanEqual | where ... >= ... | findByNameGreaterThanEqual | 大于等于 |
| IsNull,NULL | where name is null | findByNameIsNull | 判断是否为空 |
| IsNotNull,NOT NULL | where name is not null | findByNameIsNotNull | 判断是否不为空 |
| StartsWith | where name like ?1 | findByNameStartsWith | 以...开头 |
| EndsWith | where name like ?1 | findByNameEndsWith | 以...结尾 |
| Contains | where name like ?1 | findByNameContains | 包含... |
这里举个例子,例如我们要根据名字和地址模糊查询,并且按照租金降序排序,可以使用如下方法:
在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 分析输出日志
- 定义了实体类,并创建了表
Hibernate:
create table hotel (
id bigint not null,
address varchar(255),
name varchar(255),
rent float(53),
primary key (id)
)
- 定义了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
- 执行了查询,并返回结果
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语句,并执行了查询。