# Spring Data JPA上手
**Repository Path**: ninja650/study_jpa
## Basic Information
- **Project Name**: Spring Data JPA上手
- **Description**: Spring Data JPA 入门到上手的经典案列
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 2
- **Created**: 2020-09-09
- **Last Updated**: 2023-03-29
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
## Spring Data JPA谁说你不会,你就抡键盘!
JPA和Hibernate和Spring Data JPA可不是一样的,这里先声明,但他们都作用于一个方向,那就是ORM(对象关系映射),JPA只是一套规范,Hibernate和Spring Data JPA是它规范下的两种实现
> ORM 就是建立实体类和数据库表之间的关系,自动生成 SQL语句,自动执行 从而达到操作实体类就相当于操作数据库的
> 表
另一个方向就是非关系映射,比如我们常用的MyBatis,他是通过写sql语句直接与数据库产生关系,当然MyBatis Plus虽然是他的哥哥,但未了弥补MyBatis的缺陷,它也将手伸向了ORM
在我们的项目中,不是用了JPA就不能用MyBatis的,都是可以一起使用的,这看老大允不允许吧,两者是不冲突的
## Spring Data JPA入门
> Spring Data JPA 是Spring 基于JPA规范做出的自己的实现,Spring Data是一个系列,里面是关于Spring 对各种持久化数据库做出的一揽子封装,我们可以其来操作各种数据库,比如MySQL、MongoDB、Redis、ElasticSearch等:[更多详细信息见官网](https://spring.io/projects/spring-data)
> 现在不会Java程序员说不会SpringBoot,这有点说不过去吧,我们就直接使用SpringBoot集成Spring Data JPA来学习一下JPA的常用手法
- 创建一个SpringBoot项目,依赖如下
```xml
4.0.0
com.example
demo
0.0.1-SNAPSHOT
demo
Demo project for Spring Boot
1.8
UTF-8
UTF-8
2.3.0.RELEASE
org.springframework.boot
spring-boot-starter-data-jpa
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.junit.vintage
junit-vintage-engine
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.springframework.boot
spring-boot-maven-plugin
```
- 定义Dao接口,一般我们把它叫做Repository接口,如下所示
- 在定义接口之前,我们先要确定我们操作的对象,因为是面向对象的
```java
package com.example.demo.entity.basic;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/**
* @Description
* @Author Ninja
* @Date 2020/9/13
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) //主键自增
private Long id;
private String name;
private Long age;
public User(String sname, long sage) {
this.name = sname;
this.age = sage;
}
}
```
```java
package com.example.demo.repository;
import com.example.demo.entity.basic.User;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends JpaRepository {}
```
> 就上面这段代码,使用Spring Data JPA已经入门了,下面我详细解释一下
>
> 在实体类User中,有以下几个注解需要注意一下
>
> - @Entity :表示该实体为一个ORM实体,与数据库映射
> - @Table(name = "user") ;:表示该实体在数据库中的表名
> - @Id :标注该字段为主键,每个ORM实体比如有该注解标注的属性
> - @GeneratedValue(strategy = GenerationType.IDENTITY) :主键生成策略有以下几种
> - GenerationType.TABLE, //特定表生成 [一般不使用]
> - GenerationType.SEQUENCE, //数据库底层 [一般不使用]
> - GenerationType.IDENTITY, //自增序列生成 [一般]
> - GenerationType.AUTO //默认,自动选择 [一般]
> - 不写 GeneratedValue //由程序控制 [一般]
>
> 1. 至于更多的注解,下面我会归纳到一起,这里我们还是开始使用吧
- SpringBoot配置Spring Data JPA
- application.yml
```yml
server:
port: 3333
spring:
datasource:
url: jdbc:mysql://localhost:3306/test_jpa2?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
username: root
password: root
jpa:
database: MySQL
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
show-sql: true
# hibernate.ddl-auto: create
hibernate:
ddl-auto: update
# naming:
# implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
# : create "每次运行程序时,都会重新创建表,故而数据会丢失"
# : create-drop "每次运行程序时会先创建表结构,然后待程序结束时清空表"
# : upadte "每次运行程序,没有表时会创建表,如果对象发生改变会更新表结构,原有数据不会清空,只会更新(推荐使用)"
# : validate "运行程序会校验数据与数据库的字段类型是否相同,字段不同会报错"
# : none "禁用DDL处理"
# properties.hibernate.dialect: org.hibernate.dialect.MySQL5InnoDBialect
```
- 启动类
- 很常规的一个启动类,无需多说
```java
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
```
### JpaRepository API
```java
package com.example.demo.repository;
import com.example.demo.entity.basic.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Optional;
@SpringBootTest
public class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
}
```
```java
//最普通的JPA提供的API调用-----------
//保存一条数据
@Test
public void test1() {
User user = new User(1L, "玛卡巴卡", 23L);
User save = userRepository.save(user);
if (save != null) {
System.out.println(save);
}
}
//查询、修改数据
//JPA默认以id为依据,当ID已经存在时,save()为修改操作,当ID不存在时为插入操作
@Test
public void test2() {
Optional optional = userRepository.findById(1L);
if (optional.isPresent()) {
User user = optional.get();
System.out.println("得到User数据" + user);
user.setName("胡汉三");
User save = userRepository.save(user);
if (save != null) {
System.out.println(save);
}
}
}
//删除一条数据
@Test
public void test3() {
userRepository.deleteById(1L);
}
//分页、排序查询
@Test
public void test4() {
//构建排序对象 Sort
Sort sort = Sort.by(Sort.Direction.DESC, "age");
//通过page、size、排序对象:构建分页对象 PageRequest
PageRequest pageRequest = PageRequest.of(0, 3,sort);
Page page = userRepository.findAll(pageRequest);
System.out.println("数据总条数为:" + page.getTotalElements());
System.out.println("当前page:" + page.getNumber());
System.out.println("当前size:" + page.getSize());
System.out.println("数据为:" + page.getContent());
System.out.println("总页数为: " + page.getTotalPages());
}
```
### 方法命名规则查询
上面我们使用了JpaRepository提供的一些基本的CRUD的API,下面我们来使用JPA的一个有意思的用法
> 方法命名规则查询 :
>
> - 通过定制符合规则的接口命名来实现CRUD
>
> ```java
> @Repository
> public interface UserRepository extends JpaRepository {
>
> //查询位于某个年龄段的人员信息
> List findByAgeBetween(Long start, Long end);
>
> //对name进行模糊查询
> List findByNameLike(String name);
>
> }
> ```
```java
//中级难度: 接口命名查询-----------
@Test
public void test5() {
List list = userRepository.findByAgeBetween(18L, 30L);
list.forEach(user -> {
System.out.println(user);
});
}
//模糊查询,记得自己给定是前置模糊还是后置模糊,JPA将这个权利交给用户指定
@Test
public void test6() {
List list = userRepository.findByNameLike("%" + "三" + "%");
list.forEach(user -> {
System.out.println(user);
});
}
```
> 至于更多命名规则关键字
| Keyword | Sample | JPQL |
| ----------------- | ---------------------------------------- | :----------------------------------------------------------- |
| NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
| StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
| EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
| Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
| OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
| Not | findByLastnameNot | … where x.lastname <> ?1 |
| In | findByAgeIn(Collection ages) | … where x.age in ?1 |
| NotIn | findByAgeNotIn(Collection age) | … where x.age not in ?1 |
| TRUE | findByActiveTrue() | … where x.active = true |
| FALSE | findByActiveFalse() | … where x.active = false |
| IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
| | | |
| Like | findByFirstnameLike | … where x.firstname like ?1 |
| IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
| IsNull | findByAgeIsNull | … where x.age is null |
| Before | findByStartDateBefore | … where x.startDate < ?1 |
| After | findByStartDateAfter | … where x.startDate > ?1 |
| GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
| GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
| LessThanEqual | findByAgeLessThanEqual | … where x.age ⇐ ?1 |
| LessThan | findByAgeLessThan | … where x.age < ?1 |
| Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
| Is,Equals | findByFirstnameIs, findByFirstnameEquals | … where x.firstname = ?1 |
| Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
| And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
### JPQL操作
- 使用 @Query注解配合JPQL的语句即可实现
- 这里值得注意的是:如果是要对数据库产生数据变更操作,需要额外添加一个注解 : @Modifying
```java
@Repository
public interface UserRepository extends JpaRepository {
//JPQL的用法演示
//对数据库有变化的操作,应该额外添加一个注解: @Modifying
@Query(value = "from User")
List jpqlFindAll();
@Query(value = "from User where name = ?1")
User jpqlFindByName(String name);
}
```
### 原生SQL操作
- 也是使用 @Query注解,但得开启本地SQL
- nativeQuery=true :开启本地sql,这个就是原生的SQL,使用原生SQL,必须给该属性
- value=" " :值为原生SQL,其中的值使用占位符的方式给定
- ?1 :函数的第一个入参
- ?2:函数的第二个入参
- ?3:依次类推
- 这里值得注意的是:如果是要对数据库产生数据变更操作,需要额外添加一个注解 : @Modifying
```java
@Repository
public interface UserRepository extends JpaRepository {
//原生本地sql演示
//对数据库有变化的操作,应该额外添加一个注解: @Modifying
@Query(nativeQuery=true, value = "select * FROM user where name like ?1 and age BETWEEN ?2 and ?3 ORDER BY id ASC")
List localSql(String name, Long age1, Long age2);
}
```
```java
//使用原生sql
@Test
public void test7() {
List users = userRepository.localSql("%三", 18L, 50L);
users.forEach(user -> {
System.out.println(user);
});
}
```
## 高级查询
### Example条件构造器
Example条件构造器的使用相比上面简单的api的使用,它实现的就是动态SQL,使用JPQL的思想,只需要一个装有条件值的Entity就可以完成动态SQL查询,免去了繁琐的if判断,但是缺点也很明显,就是对象Entity中只支持String类型的字段做一些条件查询,比如(starts/contains/ends/regex...),对于其他的类型目前只支持精确匹配,比如Long,Double类型是不支持大于、小于、Between条件给定的,只能精确判断,想要更精确的查询,我们还是得去看看下面的Specification条件构造器进行复杂查询,但是Example使用上相对简单的多,也让人很好理解,对于一些比较简单的动态查询还是可以满足的,下面我们就来简单认识一下
```java
//高级难度: 条件构造器查询-----------
//Example查询 ------ 基本的默认全部精确匹配
@Test
public void test10(){
User user = new User();
user.setName(" 法外狂徒");
user.setAge(34L);
Example example = Example.of(user);
List list = userRepository.findAll(example);
System.out.println(list);
}
//Example查询 ------ 对String类型的属性进行规则匹配
@Test
public void test11(){
User user = new User();
user.setName("三");
user.setAge(34L);
ExampleMatcher matching = ExampleMatcher.matching()
//下面api相当于 : like '%三%'
//因为name字段为String类型,所以我们可以对其做很多规则匹配,详细见下面解释
.withMatcher("name", match -> match.contains())
//下面api相当于: sage = 34
.withMatcher("age", ExampleMatcher.GenericPropertyMatchers.exact())
// 忽略该值
.withIgnorePaths("id");
Example example = Example.of(user,matching);
//加一个排序和分页
Sort sort = Sort.by(Sort.Direction.ASC, "id");
PageRequest pageRequest = PageRequest.of(0, 1);
Page page = userRepository.findAll(example, pageRequest);
System.out.println("数据总条数为:" + page.getTotalElements());
System.out.println("当前page:" + page.getNumber());
System.out.println("当前size:" + page.getSize());
System.out.println("数据为:" + page.getContent());
System.out.println("总页数为: " + page.getTotalPages());
}
```
- 关于对String类型的一些规则匹配,以前我写过的一篇博客中有说明到,详细不再赘述
- [点我!Example详解](https://www.cnblogs.com/msi-chen/p/11318600.html)
### Specification 条件构造器
> 上面我们说道了,Example只能对String类型的字段进行多规则匹配,下面来了解一下其他字段的多规则匹配,Example的增强版:Specification
- 创建实体
```java
@Entity
@Table(name = "orders")
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Orders {
@Id
private String orderNumber;
private double initialPrice;
private double price;
private LocalDate startTime;
private LocalDate endTime;
private String status;
private String userId;
private String details;
}
```
- 创建Repository
```java
@Repository
public interface OrdersRepository extends JpaRepository, JpaSpecificationExecutor {
}
```
> Specification里面我们常用就是就只有一个方法,大家可以去看看,那就是:toPredicate()
>
> ```java
> //root :代表查询的根对象,可以通过root获取实体中的属性
> //query :代表一个顶层查询对象,用来自定义查询
> //build :用来构建查询,此对象里有很多条件方法
> Predicate toPredicate(Root root,CriteriaQuery> query, CriteriaBuilder builder);
> ```
- 编写测试类进行测试
```java
@SpringBootTest
public class OrdersRepositoryTest {
@Autowired
private OrdersRepository ordersRepository;
//简单尝试一下
@Test
public void test1() {
Specification specification = new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder builder) {
//对details模糊查询,模糊字段为 '详情%'
// return builder.like(root.get("details").as(String.class), "详情%");
//对price字段进行大于等于 300的条件查询
//这里price 实体类型为Double,数据库Float,指定为Double报错
return builder.ge(root.get("price").as(Float.class), 300F);
}
};
List list = ordersRepository.findAll(specification);
System.out.println(list);
}
//分页、排序查询了解一下
@Test
public void test2() {
Specification specification = new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder builder) {
return builder.like(root.get("details").as(String.class), "详情%");
}
};
//单单只是分页
//构造分页page & size, page是从0开始的
//PageRequest pageRequest = PageRequest.of(0, 2);
//分页 + 排序
Sort sort = Sort.by(Sort.Direction.DESC, "price");
PageRequest pageRequest = PageRequest.of(0, 2, sort);
Page page = ordersRepository.findAll(specification, pageRequest);
System.out.println("数据总条数为:" + page.getTotalElements());
System.out.println("当前page:" + page.getNumber());
System.out.println("当前size:" + page.getSize());
System.out.println("数据为:" + page.getContent());
System.out.println("总页数为: " + page.getTotalPages());
}
//多条件多规则匹配
@Test
public void test3() {
Specification specification = new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> query, CriteriaBuilder builder) {
//多字段的查询条件集合
List list = new ArrayList<>();
//构建对details字段的模糊查询
Predicate detailsLike = builder.like(root.get("details").as(String.class), "详情%");
//构建对price的大于查询
Predicate priceBetween = builder.between(root.get("price").as(Float.class), 200F, 400F);
//对状态字段做一个精确匹配
Predicate statusEquals = builder.equal(root.get("status").as(String.class), "1");
list.add(detailsLike);
list.add(priceBetween);
list.add(statusEquals);
//这里既然有 builder.and(),那就肯定有or()
return builder.and(list.toArray(new Predicate[list.size()]));
}
};
List all = ordersRepository.findAll(specification);
System.out.println(all);
}
}
```
### 多表查询
这个其实有点偏话外题了,对于对象关系映射的JPA的框架来说,一般操作都是针对于单表操作的,大不了就是冗余字段嘛,如果设计到多表查询,JPA显得就很被动了,这就是MyBatis在国内很火的原因,因为国内业务复杂 ,表的设计能不冗余就不冗余,涉及多表操作那就是基操,一个接口对应的sql语句可能xml里面也就几十行吧,面对这种情况,JPA也不不能不能实现,但是相对就笨拙了一下,也失去了JPA的灵魂,一般企业里面也不会这样使用,纯属增加一下见识
> 我们就以国家和城市来做一个测试吧,将国家和城市关联起来查询得到一个虚拟结果表,将虚拟结果表数据封装起来拿到打印一下即可,一种方式就是使用原生sql操作,但是结果不好封装,我们只能封装到Map中,第二种是使用EntityManager去实现
#### 第一种方式:原生sql + Map封装
```java
@Query(nativeQuery = true,value = "select * from country c1 LEFT JOIN city c2 ON c2.country_country_id = c1.country_id where c1.country_id = ?1")
List