springboot整合Spring Data JPA
一、概念
JPA是The Java Persistence API标准,Java持久层API,是一种能让对象能够快速映射到关系型数据库的技术规范。
Spring Data JPA 是采用基于JPA规范的Hibernate框架基础下提供了Repository层的实现。Spring Data Repository极大地简化了实现各种持久层的数据库访问而写的样板代码量,同时CrudRepository提供了丰富的CRUD功能去管理实体类。SpringBoot框架为Spring Data JPA提供了整合,spring-boot-starter-data-jpa能够让你快速使用这门技术,它提供了以下依赖。
二、简单整合
以下将整理出SpringData简单的增删查改。
1.新建项目
在新建项目中选择springboot项目,选择JPA依赖与MySQL驱动等导入项目中。
2.编写配置文件
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/jpatest?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
# 默认false,在日志里显示执行的sql语句
show-sql: true
hibernate:
#指定为update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,如果指定create,则每次启动项目都会清空数据并删除表,再新建
ddl-auto: update
database: mysql
#在建表的时候,将默认的存储引擎切换为 InnoDB
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto第一建表的时候可以create,指明在程序启动的时候要删除并且创建实体类对应的表。后续使用就需要改为update。
3.编写实体类
需要写好类与表的映射关系。
@Entity // @Entity: 实体类, 必须
@Table(name = "t_user")
@AllArgsConstructor//lombok相关注解
@NoArgsConstructor
@Data
public class User {
/**
* TABLE: 使用一个特定的数据库表格来保存主键
* SEQUENCE: 根据底层数据库的序列来生成主键,条件是数据库支持序列。这个值要与generator一起使用,generator 指定生成主键使用的生成器(可能是orcale中自己编写的序列)。
* IDENTITY: 主键由数据库自动生成(主要是支持自动增长的数据库,如mysql)
* AUTO: 主键由程序控制,也是GenerationType的默认值。
*/
@Id //主键id
@GeneratedValue(strategy=GenerationType.IDENTITY)//主键生成策略
@Column(name="id")//数据库字段名
private Integer id;
@Column(name="name",nullable = false) // @Column: 对应数据库列名,可选, nullable 是否可以为空, 默认true
private String name;
@Column(name="age")
private Integer age;
@Column(name="address")
private String address;
}
4.运行项目
运行项目后,可以看到控制台结果:
控制台显示使用hibernate创建了该表。
数据库中的结果
由此,表创建好了。
5.编写dao层
需要继承JpaRepository,关于可以继承的四个类如下:
代码如下:
/**
* 参数一 T :当前需要映射的实体
* 参数二 ID :当前映射的实体中的OID的类型
*/
public interface UsersRepository extends JpaRepository<User,Integer> {
//可以根据自己的需要以及驼峰命名来写自己所需要的的方法!
//方法名称必须要遵循驼峰式命名规则,findBy(关键字)+属性名称(首字母大写)+查询条件(首字母大写)
List<User> findByName(String name);
}
只要实现了JpaRepository这个类就可以直接用来测试了,不需要再加原来的@Repository注解。
6.测试
6.1 增加操作 + 查询操作
@SpringBootTest
class SpringbootJpaApplicationTests {
@Autowired
private UsersRepository repository;
@Test
void contextLoads() {
User user = new User();
user.setName("ChaEunWoo");
user.setAge(23);
user.setAddress("Korea?");
repository.save(user);
List<User> userList = repository.findByName("ChaEunWoo");
userList.forEach(System.out::println);
}
}
输出结果:
如图,数据插入成功了。
当使用findById查询时,返回的是一个optional实例对象。
Optional<User> users = repository.findById(1);
System.out.println(users);
//或者使用下面的
User user = usersDao.getById(3);
System.out.println(user);
结果是使用find查询的结果
6.2 修改操作
代码如下:(如果测试爆红no session不通过,一定要加入@Transactional注解)
但是,如果加入了事务的注解,在测试类中使用事务注解会导致回滚!!因此还需要一个注解使事务不回滚: @Rollback(value = false) 。
@Transactional
@Rollback(value = false)
@Test
void contextLoads() {
//如果传入的对象有ID,而且hibernate在数据库能够查到,则save用作修改,否则为新加数据
System.out.println(usersDao.getById(1));
User user = new User();
user.setId(1);
user.setName("ChaEunWoo");
user.setAge(24);
user.setAddress("Korea");
usersDao.save(user);
System.out.println(usersDao.getById(1));
}
6.3 删除操作
@Test
public void toDelete(){
List<User> all = usersDao.findAll();
all.forEach(System.out::println);
usersDao.deleteById(4);
all = usersDao.findAll();
all.forEach(System.out::println);
}
6.4 查询方法
findOne:立即查询
getOne:延迟查询,内部先返回一个客户的动态代理对象,当使用到它的时候才会去数据库查询
三、SpringData运行原理
- 通过JdkDynamicAopProxy的invoke方法创建了一个动态代理对象。
- SimpleJpaRepository当中封装了JPA的操作(借助JPA的api完成CRUD)
- 通过hibernate完成了对数据库的操作(封装了JDBC)
四、其他及多条件查询
1. count及exist
@Test
public void test01(){
long count = usersDao.count();//得出有多少条数据
System.out.println(count);
boolean result = usersDao.existsById(1);//查询id为1的数据是否存在
System.out.println(result);
}
由图可以看出,ID查找用户是否存在使用的是count来判断!
2. jpql查询
2.1 简介
jpql:jpa query language(jpa查询语句)
查询的语句(表替换成类)必须是类和类中的属性!
2.2 使用
方法:
- 将jpql配置在接口方法上面
- 使用注解 @query
/**
* 使用jpql语言进行查询
* jpql : from User where name = ?
* @param name
* @return
*/
@Query(value = "from User where name = ?1")
List<User> findJpql(String name);
//测试方法
@Test
public void test02(){
List<User> users = usersDao.findJpql("HuangXin");
users.forEach(System.out::println);
}
结果:
查询可以自己根据SpringData jpa官方名称查找命名直接查询或者直接自己写query语句查新。
//方法名称必须要遵循驼峰式命名规则,findBy(关键字)+属性名称(首字母大写)+查询条件(首字母大写)
User findByNameAndId(String name,Integer id);
@Query(value = "from User where name = ?1 and id = ?2")
User findByJpql(String userName, Integer UserId);
更新操作时,需要再加上一条 @Modifying 注解!
@Query(value = "update User set name = ?1 where id = ?2")
@Modifying
int updateJpql(String name,Integer id);
#测试
@Test
@Transactional
@Rollback(value = false)
public void test05(){
int result = usersDao.updateJpql("WWWWWW", 5);
System.out.println(result);
}
结果:
@Query注解中还有一个nativeQuery字段
该字段为true时,表示value中的语句使用SQL语句查询
该字段为false时,表示value中的语句使用内部查询(即使用类名以及属性)
@Query(value = "select * from t_user ",nativeQuery = true)
List<User> findAllJpql();
3. SpringData JPA官方规则定义查询
//方法名称必须要遵循驼峰式命名规则,findBy(关键字)+属性名称(首字母大写)+查询条件(首字母大写)
//精准匹配
User findByNameAndId(String name,Integer id);
//模糊查询,传递的参数为("%a%")等
List<User> findByNameLike(String name);
//模糊查询+年龄阶段+精准匹配 AgeAfter表示年龄在传入的参数之后的那部分
List<User> findByNameLikeAndAgeAfterAndAddress(String name,Integer age,String address);
五、Specification动态查询
1. JpaSpecificationExecutor中的方法
public interface JpaSpecificationExecutor<T> {
//查询对象
Optional<T> findOne(@Nullable Specification<T> var1);
//查询列表
List<T> findAll(@Nullable Specification<T> var1);
//查询并分页
//Pageable:分页参数
//返回值:pageBean,由SpringDataJPA提供
Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);
//查询列表,根据Sort字段排序
List<T> findAll(@Nullable Specification<T> var1, Sort var2);
//统计查询
long count(@Nullable Specification<T> var1);
}
2. 查询条件
Specification:查询条件,我们需要定义自己的Specification从而使用JpaSpecificationExecutor的方法。
我们需要实现的方法如下:
//参数解析
//Root:查询的根对象(查询的任何属性都能从根对象中获取)
//CriteriaQuery:顶层查询对象,自定义查询方式(了解,一般用不上)
//CriteriaBuilder:查询的构造器,包含了许多的查询条件
//
Predicate toPredicate(Root<T> var1, CriteriaQuery<?> var2, CriteriaBuilder var3);
3. 查询测试
3.1 测试一 精确查询
精确查询
@Test
public void test08(){
Specification<User> specification = new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
//root 获取要查询的对象属性
//使用CriteriaBuilder构造查询条件,内含模糊匹配、精准匹配,不等于等条件
//步骤
//1.获取比较的属性(是User对象中的属性!)
Path<Object> name = root.get("name");
//2.构造查询条件,equal为精准查询 select * from t_user where name = ?
Predicate predicate = criteriaBuilder.equal(name, "ChaEunWoo");
//3.返回查询条件
return predicate;
}
};
Optional<User> user = usersDao.findOne(specification);
System.out.println(user);
}
3.2 测试二 拼接等
==模糊查询+年龄比较+拼接==
@Test
public void test09(){
Specification<User> specification = new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
Predicate p1 = cb.like(root.get("name"), "%a%");
Predicate p2 = cb.ge(root.get("age"),15);
Predicate predicate = cb.and(p1,p2);//and or 即相当于拼接
return predicate;
}
};
List<User> userList = usersDao.findAll(specification);
userList.forEach(System.out::println);
}
3.3 测试三 排序
添加排序
在测试二基础上,新建排序对象,加到findAll方法里即可。
//排序
@Test
public void test10(){
Specification<User> specification = new Specification<User>() {
@Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder cb) {
Predicate p1 = cb.like(root.get("name"), "%a%");
Predicate p2 = cb.ge(root.get("age"),15);
Predicate predicate = cb.and(p1,p2);//and or 即相当于拼接
return predicate;
}
};
//添加排序
//创建排序对象,新版本只能通过Sort.by来创建对象(springboot2.1版本以上 Sort已经不能用 new 实例化了)
//参数1:正序Sort.Direction.ASC/倒序Sort.Direction.DESC
//参数二:排序的 属性! 名称,对象的属性名称!
List<String> list = new ArrayList<>();
list.add("age");
Sort sort = Sort.by(Sort.Direction.ASC,"age");
List<User> userList = usersDao.findAll(specification,sort);
userList.forEach(System.out::println);
}
测试结果:
3.4 测试四 分页查询
分页查询
/**
* 分页查询
* 参数一:Specification--查询条件
* 参数二:PageAble--分页参数
* 分页参数:查询的页码,每页查询的条数
* findAll(Specification,Pageable):带有条件的分页查询
* findAll(Pageable):不带条件的分页查询
* 返回值:Page(SpringDataJpa为我们封装好的pageBean对象,数据列表,总条数)
*/
@Test
public void test11(){
/**
* 使用PageRequest.of来生成Pageable实现类
* 参数1:当前查询页
* 参数2:页大小(即一页多少条数据)
* 参数3:定义排序 --参数可省
* 参数4:依据什么属性排序 --可省可省
*/
Pageable pageable = PageRequest.of(0,5);
Specification<User> specification = null;
Page<User> page = usersDao.findAll(specification, pageable);
// page.getTotalElements();//得到所有数据总数
// page.getTotalPages();//得到总页数
// page.getContent();//得到需要查询的页面的数据集合
System.out.println("The data total number is "+page.getTotalElements());
System.out.println("The total page number is "+page.getTotalPages());
page.getContent().forEach(System.out::println);
}
测试结果:
六、多表关系与多表操作
1. 多表关系
(1)一对一
(2)一对多
一的一方:主表
多的一方:从表
外键:需要在从表上新建一列作为外键,它来源于主表的主键
(3)多对多
中间表:中间表最少应该有两个字段组成,这两个字字段作为外键指向两张表的主键,组成了联合主键
2. 多表操作
2.1 一对多关系操作
假设一家公司有多个员工,一个员工从属于一家公司。
实体类编写如下:
Employee.java
//一对多的从体
@Entity
@Table(name = "t_employee")
public class Employee {
@Id()
@GeneratedValue(strategy= GenerationType.IDENTITY)//主键生成策略
@Column(name="emp_id")
private int empId;
@Column(name="emp_name")
private String empName;
@Column(name="emp_age")
private int empAge;
//配置多对一关系,与一对多差不多
@ManyToOne(targetEntity = Company.class)
@JoinColumn(name = "emp_com_id",referencedColumnName = "com_id")
private Company company;
public int getEmpId() {
return empId;
}
public void setEmpId(int empId) {
this.empId = empId;
}
public String getEmpName() {
return empName;
}
public void setEmpName(String empName) {
this.empName = empName;
}
public int getEmpAge() {
return empAge;
}
public void setEmpAge(int empAge) {
this.empAge = empAge;
}
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
public Employee(int empId, String empName, int empAge, Company company) {
this.empId = empId;
this.empName = empName;
this.empAge = empAge;
this.company = company;
}
public Employee() {
}
@Override
public String toString() {
return "Employee{" +
"empId=" + empId +
", empName='" + empName + '\'' +
", empAge=" + empAge +
", company=" + company +
'}';
}
}
Company.java
//一对多的主体
@Entity
@Table(name = "t_company")
public class Company {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)//主键生成策略
@Column(name="com_id")
private int comId;
@Column(name="com_name")
private String comName;
@Column(name="com_mail")
private String comMail;
@Column(name="com_address")
private String comAddress;
//声明关系,配置一对多的关系
//配置外键(中间键) name:外键字段名称 referencedColumnName:参照主表的主键名称
@OneToMany(targetEntity = Employee.class,cascade = CascadeType.ALL)
//targetEntity指对方的字节码文件,cascade级联操作,要改一起改,要删一起删,在主表上配置!!
@JoinColumn(name = "emp_com_id",referencedColumnName = "com_id")
private Set<Employee> employeesSet = new HashSet<>();
public int getComId() {
return comId;
}
public void setComId(int comId) {
this.comId = comId;
}
public String getComName() {
return comName;
}
public void setComName(String comName) {
this.comName = comName;
}
public String getComMail() {
return comMail;
}
public void setComMail(String comMail) {
this.comMail = comMail;
}
public String getComAddress() {
return comAddress;
}
public void setComAddress(String comAddress) {
this.comAddress = comAddress;
}
public Set<Employee> getEmployeesSet() {
return employeesSet;
}
public void setEmployeesSet(Set<Employee> employeesSet) {
this.employeesSet = employeesSet;
}
public Company() {
}
public Company(int comId, String comName, String comMail, String comAddress, Set<Employee> employeesSet) {
this.comId = comId;
this.comName = comName;
this.comMail = comMail;
this.comAddress = comAddress;
this.employeesSet = employeesSet;
}
@Override
public String toString() {
return "Company{" +
"comId=" + comId +
", comName='" + comName + '\'' +
", comMail='" + comMail + '\'' +
", comAddress='" + comAddress + '\'' ;
}
}
执行前:
在外键可以为null的情况下,删除某公司后,数据表中公司id为空:
在hibernate一对多的注解中,由于没有添加mappedBy=“多端的关联属性名”,导致在更新数据时,外键被置空(null),意思是由多的一端来维护关系。(一般由有外键的一方来维护)
2.2 多对多关系操作
暂略…