Spring Data查询方法
一. CRUD:只需要简单的继承,分页和CRUD就有了
public interface EmployeeRepository extends JpaRepository<Employee, Long>, JpaSpecificationExecutor {
}
二. 使用 JavaConfig 或 XML configuration配置Spring,让 Spring 为声明的接口创建代理对象
- 使用Xml配置,可以像下面这样使用jpa命名空间进行配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
">
<!--事务注释扫描开启 驱动-->
<tx:annotation-driven/>
<context:component-scan base-package="com.heyang.service"/>
<!-- 把jdbc.properties 配置文件的信息给到Spring -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 连接池配置 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"><!-- destroy-method="close" 指定销毁的方法,也不用管连接 -->
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!--当前datasource 以下 根据实际开发,可以修改参数,提高性能的配置-->
<!--maxActive: 最大连接数量 -->
<property name="maxActive" value="150" />
<!--minIdle: 最小空闲连接 -->
<property name="minIdle" value="5" />
<!--maxIdle: 最大空闲连接 -->
<property name="maxIdle" value="20" />
<!--initialSize: 初始化连接 -->
<property name="initialSize" value="30" />
<!-- 用来配置数据库断开后自动连接的 -->
<!-- 连接被泄露时是否打印 -->
<property name="logAbandoned" value="true" />
<!--removeAbandoned: 是否自动回收超时连接 -->
<property name="removeAbandoned" value="true" />
<!--removeAbandonedTimeout: 超时时间(以秒数为单位) -->
<property name="removeAbandonedTimeout" value="10" />
<!--maxWait: 超时等待时间以毫秒为单位 1000等于60秒 -->
<property name="maxWait" value="1000" />
<!-- 在空闲连接回收器线程运行期间休眠的时间值,以毫秒为单位. -->
<property name="timeBetweenEvictionRunsMillis" value="10000" />
<!-- 在每次空闲连接回收器线程(如果有)运行时检查的连接数量 -->
<property name="numTestsPerEvictionRun" value="10" />
<!-- 1000 * 60 * 30 连接在池中保持空闲而不被空闲连接回收器线程 -->
<property name="minEvictableIdleTimeMillis" value="10000" />
<property name="validationQuery" value="SELECT NOW() FROM DUAL" />
</bean>
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="packagesToScan" value="com.heyang.domain"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<!-- org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter -->
<!-- private boolean showSql = false;是否显示sql语句 -->
<property name="showSql" value="true" />
<!-- private boolean generateDdl = false;是否建表 -->
<property name="generateDdl" value="false" />
<!-- private String databasePlatform;原来方言 -->
<property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" />
</bean>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<!-- Spring Data Jpa配置 ********************************************-->
<!-- base-package:扫描的包 -->
<jpa:repositories base-package="com.heyang.repository" transaction-manager-ref="transactionManager"
entity-manager-factory-ref="entityManagerFactory" />
</beans>
jdbc.properties
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///数据库名字
jdbc.username=root
jdbc.password=12345678
顺带一提,对于不同的Spring Data子项目Spring提供了不同的xml命名空间,如对于Spring Data MongoDB可以将上面的jpa改为mongodb
当然,使用Spring Boot这一步基本可以省略,我们需要做的就是在application.properties或application.xml文件中配置几个属性即可
三. 高级查询
public interface EmployeeRepository extends JpaRepository<Employee, Long>, JpaSpecificationExecutor {
Employee findByUsername(String username);
//根据命名规则查询
Employee findAllByUsernameLikeAndAgeIs(String username, Integer age);
//使用规则直接查询
List<Employee> findAllByUsernameLike(String username);
//用jpql语句查询
@Query("select o from Employee o where username like ?1")
List<Employee> findByUsernameJpql(String username);
//用原生sql查询
@Query(nativeQuery = true, value = "SELECT * from employee")
List<Employee> findallNative();
}
三. 分页查询和排序查询
- 抽取父类BaseQuery
package com.heyang.query;
import org.apache.commons.lang.StringUtils;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
public abstract class BaseQuery {
//当前页
private int currentPage = 1;
//每页显示的数据
private int pageSize = 10;
//排序根据的列
private String orderName;
//排序类型
private String orderType = "ASC";
public Sort creatSort() {
if (StringUtils.isNotBlank(orderName)) {
return new Sort(Sort.Direction.valueOf(orderType), orderName);
}
return null;
}
//设置一个抽象方法,公共的固定名字,统一规范
public abstract Specification createSpecifictiong();
public int getCurrentPage() {
return currentPage;
}
public void setCurrentPage(int currentPage) {
this.currentPage = currentPage;
}
//修改当前页从0开始的情况,如果是已有封装好的代码,建议新建一个方法,进行扩展
public int getPageCurrentPage() {
return currentPage - 1;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public String getOrderName() {
return orderName;
}
public void setOrderName(String orderName) {
this.orderName = orderName;
}
public String getOrderType() {
return orderType;
}
public void setOrderType(String orderType) {
this.orderType = orderType;
}
}
- 具体的类继承
package com.heyang.query;
import com.github.wenhao.jpa.Specifications;
import com.heyang.domain.Employee;
import org.apache.commons.lang.StringUtils;
import org.springframework.data.domain.Range;
import org.springframework.data.jpa.domain.Specification;
//查询类高级查询
public class EmployeeQuery extends BaseQuery {
private String username;
private String email;
private Integer age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
//高级查询
@Override
public Specification createSpecifictiong() {
Range range = new Range(20, 30);
Specification<Employee> build = Specifications.<Employee>and().like(StringUtils.isNotBlank(username), "username", "%" + username + "%")
.like(StringUtils.isNotBlank(email), "email", "%" + email + "%")
.gt(age != null, "age", age).build();
return build;
}
}
四. 测试类
package com.heyang;
import com.heyang.domain.Employee;
import com.heyang.query.EmployeeQuery;
import com.heyang.repository.EmployeeRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.domain.Specifications;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.persistence.criteria.*;
import java.util.List;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class SSSDJTest {
@Autowired
private EmployeeRepository employeeRepository;
/**
* 测试查询所有
* @throws Exception
*/
@Test
public void test() throws Exception {
employeeRepository.findAll().forEach(e-> System.out.println(e));
}
/**
* 查询一条,根据规则查询,根据jpql查询
* @throws Exception
*/
@Test
public void test1() throws Exception {
Employee one = employeeRepository.findOne(23L);
Employee admin = employeeRepository.findByUsername("admin");
Employee allByUsernameLikeAndAgeIs = employeeRepository.findAllByUsernameLikeAndAgeIs("%admin%", 34);
List<Employee> allByUsernameLike = employeeRepository.findAllByUsernameLike("%admin%");
// allByUsernameLike.forEach(e-> System.out.println(e));
List<Employee> byUsername111 = employeeRepository.findByUsernameJpql("%admin%");
byUsername111.forEach(e-> System.out.println(e));
}
/**
* 使用原生语句查询
* @throws Exception
*/
@Test
public void test2() throws Exception{
employeeRepository.findallNative().forEach(e-> System.out.println(e));
}
/**
* 测试排序,分页
* @throws Exception
*/
@Test
public void test03() throws Exception{
Sort agesort = new Sort(Sort.Direction.DESC, "age");
PageRequest page = new PageRequest(0, 10, agesort);
PageRequest pageAndSort = new PageRequest(0, 10, agesort);
employeeRepository.findAll(agesort).forEach(e-> System.out.println(e));
System.out.println("====================================================");
employeeRepository.findAll(page).forEach(e-> System.out.println(e));
System.out.println("====================================================");
employeeRepository.findAll(pageAndSort).forEach(e-> System.out.println(e));
}
/**
* 测试高级查询
* @throws Exception
*/
@Test
public void test04() throws Exception{
/*
Root 获取字段名字
criteriaQuery
criteriaBuilder
*/
employeeRepository.findAll(((root, cq, cb) -> {
Path username = root.get("username");
Path emal = root.get("email");
Path age = root.get("age");
Predicate p1 = cb.like(username, "%admin%");
Predicate p2 = cb.like(emal, "%qq.com%");
Predicate p3 = cb.gt(age, 20);
Predicate pAll = cb.and(p1, p2, p3);
return pAll;
})).forEach(e-> System.out.println(e));
}
/**
* 查询 加入sort和分页
* @throws Exception
*/
@Test
public void test05() throws Exception{
/*
Root 获取字段名字
criteriaQuery
criteriaBuilder
*/
Pageable age1 = new PageRequest(0, 10, new Sort(Sort.Direction.DESC, "age"));
Page all = employeeRepository.findAll(((root, cq, cb) -> {
Path username = root.get("username");
Path emal = root.get("email");
Path age = root.get("age");
Predicate p1 = cb.like(username, "%admin%");
Predicate p2 = cb.like(emal, "%2%");
Predicate p3 = cb.gt(age, 20);
Predicate pAll = cb.and(p1, p2, p3);
return pAll;
}), age1);
all.forEach(e-> System.out.println(e));
}
@Test
public void test06() throws Exception{
//模拟前台数据传输
EmployeeQuery employeeQuery = new EmployeeQuery();
employeeQuery.setEmail("2");
employeeQuery.setUsername("admin");
employeeQuery.setAge(20);
employeeQuery.setOrderName("age");
employeeQuery.setOrderType("DESC");
//实际代码
Specification specifictiong = employeeQuery.createSpecifictiong();
Sort orders = employeeQuery.creatSort();
Pageable pageRequest = new PageRequest(employeeQuery.getPageCurrentPage(), employeeQuery.getPageSize(),orders);
employeeRepository.findAll(specifictiong,pageRequest).forEach(e-> System.out.println(e));
}
}
五. 查询的其他扩充
具体Spring Data Jpa对方法名的解析规则可参看官方文档4.4.3. Property Expressions
- 限制查询结果
Spring Data Jpa支持使用first、top以及Distinct 关键字来限制查询结果,如:
User findFirstByUsernameOrderByUsernameAsc(String username);
List<User> findTop10ByUsername(String username, Sort sort);
List<User> findTop10ByUsername(String username, Pageable pageable);
- 自定义查询Using @Query
//@Query 注解的使用非常简单,只需在声明的方法上面标注该注解,同时提供一个 JPQL 查询语句即可
@Query("select u from User u where u.email = ?1")
User getByEmail(String eamil);
@Query("select u from User u where u.username = ?1 and u.password = ?2")
User getByUsernameAndPassword(String username, String password);
@Query("select u from User u where u.username like %?1%")
List<User> getByUsernameLike(String username);
- 使用命名参数Using Named Parameters
默认情况下,Spring Data JPA使用基于位置的参数绑定,如前面所有示例中所述。 这使得查询方法在重构参数位置时容易出错。 要解决此问题,可以使用@Param注解为方法参数指定具体名称并在查询中绑定名称,如以下示例所示:
@Query("select u from User u where u.id = :id")
User getById(@Param("id") String userId);
@Query("select u from User u where u.username = :username or u.email = :email")
User getByUsernameOrEmail(@Param("username") String username, @Param("email") String email);
- Using SpEL Expressions
从Spring Data JPA release 1.4开始,Spring Data JPA支持名为entityName的变量。 它的用法是select x from #{#entityName} x。 entityName的解析方式如下:如果实体类在@Entity注解上设置了name属性,则使用它。 否则,使用实体类的简单类名。为避免在@Query注解使用实际的实体类名,就可以使用#{#entityName}进行代替。如以上示例中,@Query注解的查询字符串里的User都可替换为#{#entityName}
@Query("select u from #{#entityName} u where u.email = ?1")
User getByEmail(String eamil);
- 原生查询Native Queries
@Query注解还支持通过将nativeQuery标志设置为true来执行原生查询,同样支持基于位置的参数绑定及命名参数,如:
@Query(value = "select * from tb_user u where u.email = ?1", nativeQuery = true)
User queryByEmail(String email);
@Query(value = "select * from tb_user u where u.email = :email", nativeQuery = true)
User queryByEmail(@Param("email") String email);
- 注意:Spring Data Jpa目前不支持对原生查询进行动态排序,但可以通过自己指定计数查询countQuery来使用原生查询进行分页、排序,如:
@Query(nativeQuery = true,value = "select * from tb_user u where u.username like %?1%",
countQuery = "select count(1) from tb_user u where u.username = %?1%")
Page<User> queryByUsernameLike(String username, Pageable pageable);
六. 分页查询及排序补充
1.Spring Data Jpa可以在方法参数中直接传入Pageable或Sort来完成动态分页或排序,通常Pageable或Sort会是方法的最后一个参数,如:
@Query("select u from User u where u.username like %?1%")
Page<User> findByUsernameLike(String username, Pageable pageable);
@Query("select u from User u where u.username like %?1%")
List<User> findByUsernameAndSort(String username, Sort sort);
那调用repository方法时传入什么参数呢?
2.对于Pageable参数,在Spring Data 2.0之前我们可以new一个org.springframework.data.domain.PageRequest对象,现在这些构造方法已经废弃,取而代之Spring推荐我们使用PageRequest的of方法
new PageRequest(0, 5);
new PageRequest(0, 5, Sort.Direction.ASC, "username");
new PageRequest(0, 5, new Sort(Sort.Direction.ASC, "username"));
PageRequest.of(0, 5);
PageRequest.of(0, 5, Sort.Direction.ASC, "username");
PageRequest.of(0, 5, Sort.by(Sort.Direction.ASC, "username"));
- 注意:Spring Data PageRequest的page参数是从0开始的 zero-based page index
3.对于Sort参数,同样可以new一个org.springframework.data.domain.Sort,但推荐使用Sort.by方法自定义修改、删除 Modifying Queries单独使用@Query注解只是查询,如涉及到修改、删除则需要再加上@Modifying注解,如
@Transactional()
@Modifying
@Query("update User u set u.password = ?2 where u.username = ?1")
int updatePasswordByUsername(String username, String password);
@Transactional()
@Modifying
@Query("delete from User where username = ?1")
void deleteByUsername(String username);
- 注意:Modifying queries can only use void or int/Integer as return type!
4. 多表查询
这里使用级联查询进行多表的关联查询
多对多
package com.example.springbootjpa.entity;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.Date;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "tb_user")
@Data
public class User {
@Id
@GenericGenerator(name = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "username", unique = true, nullable = false, length = 64)
private String username;
@Column(name = "password", nullable = false, length = 64)
private String password;
@Column(name = "email", unique = true, length = 64)
private String email;
@ManyToMany(targetEntity = Role.class, cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
@JoinTable(name = "tb_user_role", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
private Set<Role> roles;
}
package com.example.springbootjpa.entity;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
@Entity
@Table(name = "tb_role")
@Data
public class Role {
@Id
@GenericGenerator(name = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "role_name", unique = true, nullable = false, length = 64)
private String roleName;
}
测试
@Test
public void findByIdTest() {
Optional<User> optional = userRepository.findById("40289f0c65674a930165674d54940000");
Set<Role> roles = optional.get().getRoles();
System.out.println(optional.get());
}
不出意外会报Hibernate懒加载异常,无法初始化代理类,No Session:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.springbootjpa.entity.User.roles, could not initialize proxy - no Session
原因:Spring Boot整合JPA后Hibernate的Session就交付给Spring去管理。每次数据库操作后,会关闭Session,当我们想要用懒加载方式去获得数据的时候,原来的Session已经关闭,不能获取数据,所以会抛出这样的异常。
解决方法:
在application.yml中做如下配置:
spring:
jpa:
open-in-view: true
properties:
hibernate:
enable_lazy_load_no_trans: true
- 一对多(多对一)
package com.example.springbootjpa.entity;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.Set;
@Entity
@Table(name = "tb_dept")
@Data
public class Department {
@Id
@GenericGenerator(name = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "dept_name", unique = true, nullable = false, length = 64)
private String deptName;
@OneToMany(mappedBy = "department", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Employee> employees;
}
package com.example.springbootjpa.entity;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import javax.persistence.*;
import java.util.UUID;
@Entity
@Table(name = "tb_emp")
@Data
public class Employee {
@Id
@GenericGenerator(name = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "emp_name", nullable = false, length = 64)
private String empName;
@Column(name = "emp_job", length = 64)
private String empJob;
@Column(name = "dept_id", insertable = false, updatable = false)
private String deptId;
@ManyToOne(targetEntity = Department.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "dept_id")
private Department department;
}
测试
@Test
public void findByIdTest() {
Optional<Employee> optional = employeeRepository.findById("93fce66c1ef340fa866d5bd389de3d79");
System.out.println(optional.get());
}
结果报错了...
java.lang.StackOverflowError
通过日志看sql的输出,发现了sql重复执行了好多次。以下我截取了前10条sql记录。
Hibernate: select employee0_.id as id1_1_0_, employee0_.dept_id as dept_id2_1_0_, employee0_.emp_job as emp_job3_1_0_, employee0_.emp_name as emp_name4_1_0_ from tb_emp employee0_ where employee0_.id=?
Hibernate: select department0_.id as id1_0_0_, department0_.dept_name as dept_nam2_0_0_ from tb_dept department0_ where department0_.id=?
Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=?
Hibernate: select department0_.id as id1_0_0_, department0_.dept_name as dept_nam2_0_0_ from tb_dept department0_ where department0_.id=?
Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=?
Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=?
Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=?
Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=?
Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=?
Hibernate: select employees0_.dept_id as dept_id2_1_0_, employees0_.id as id1_1_0_, employees0_.id as id1_1_1_, employees0_.dept_id as dept_id2_1_1_, employees0_.emp_job as emp_job3_1_1_, employees0_.emp_name as emp_name4_1_1_ from tb_emp employees0_ where employees0_.dept_id=?
通过观察发现,第一条sql是执行查询Employee的sql,第二条sql是执行查询Department的sql,第三条sql是执行Department里面所有员工的sql,第四条sql是执行查询Department的sql,后面所有的sql都是执行查询Department里面所有员工的sql。
很明显发生了循环依赖的情况。这是Lombok的@Data注解的锅。Lombok的@Data注解相当于@Getter、@Setter、@RequiredArgsConstructor、@ToString、@EqualsAndHashCode这几个注解。
我们可以通过反编译看一下Lombok生成的toString()方法
// Employee
public String toString() {
return "Employee(id=" + getId() + ", empName=" + getEmpName() + ", empJob=" + getEmpJob() + ", deptId=" + getDeptId() + ", department=" + getDepartment() + ")";
}
// Department
public String toString() {
return "Department(id=" + getId() + ", deptName=" + getDeptName() + ", employees=" + getEmployees() + ")";
}
可以发现Lombok为我们生成的toString()方法覆盖了整个类的所有属性
现在将@Data注解去掉,替换为@Setter、@Getter、@EqualsAndHashCode,重写toString()方法
// Department
@Override
public String toString() {
return "Department{" +
"id='" + id + '\'' +
", deptName='" + deptName + '\'' +
'}';
}
// Employee
@Override
public String toString() {
return "Employee{" +
"id='" + id + '\'' +
", empName='" + empName + '\'' +
", empJob='" + empJob + '\'' +
", deptId='" + deptId + '\'' +
", department=" + department +
'}';
}
再次运行测试用例,测试通过,以上Employee toString()方法打印的department会触发懒加载,最终日志输出的sql如下:
Hibernate: select employee0_.id as id1_1_0_, employee0_.dept_id as dept_id2_1_0_, employee0_.emp_job as emp_job3_1_0_, employee0_.emp_name as emp_name4_1_0_ from tb_emp employee0_ where employee0_.id=?
Hibernate: select department0_.id as id1_0_0_, department0_.dept_name as dept_nam2_0_0_ from tb_dept department0_ where department0_.id=?
Employee{id='93fce66c1ef340fa866d5bd389de3d79', empName='jack', empJob='hr', deptId='0a4fe7234fff42afad34f6a06a8e1821', department=Department{id='0a4fe7234fff42afad34f6a06a8e1821', deptName='人事部'}}
再来测试查询Department
@Test
public void findByIdTest() {
Optional<Department> optional = departmentRepository.findById("0a4fe7234fff42afad34f6a06a8e1821");
Set<Employee> employees = optional.get().getEmployees();
Assert.assertNotEquals(0, employees.size());
}
同样还是报了堆栈溢出,错误定位在Department和Employee的hashCode()方法上
java.lang.StackOverflowError
依旧是Lombok的锅,@EqualsAndHashCode为我们生成的equals()和hashCode()方法会使用所有属性,注意,Department中employees是Set集合,当我们调用department.getEmployees()时,Employee的hashCode()方法会被调用,Employee中的hashCode()又依赖于Department的HashCode()方法,这样又形成了循环引用…
// Department
public int hashCode() {
int i = 43;
String $id = getId();
int result = ($id == null ? 43 : $id.hashCode()) + 59;
String $deptName = getDeptName();
result = (result * 59) + ($deptName == null ? 43 : $deptName.hashCode());
Set $employees = getEmployees();
int i2 = result * 59;
if ($employees != null) {
i = $employees.hashCode();
}
return i2 + i;
}
// Employee
public int hashCode() {
int i = 43;
String $id = getId();
int result = ($id == null ? 43 : $id.hashCode()) + 59;
String $empName = getEmpName();
result = (result * 59) + ($empName == null ? 43 : $empName.hashCode());
String $empJob = getEmpJob();
result = (result * 59) + ($empJob == null ? 43 : $empJob.hashCode());
String $deptId = getDeptId();
result = (result * 59) + ($deptId == null ? 43 : $deptId.hashCode());
Department $department = getDepartment();
int i2 = result * 59;
if ($department != null) {
i = $department.hashCode();
}
return i2 + i;
}
自己动手重写equals()和hashCode()方法,去掉@EqualsAndHashCode注解
// Department
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Department that = (Department) o;
return Objects.equals(id, that.id) &&
Objects.equals(deptName, that.deptName);
}
@Override
public int hashCode() {
return Objects.hash(id, deptName);
}
// Employee
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return Objects.equals(id, employee.id) &&
Objects.equals(empName, employee.empName) &&
Objects.equals(empJob, employee.empJob) &&
Objects.equals(deptId, employee.deptId);
}
@Override
public int hashCode() {
return Objects.hash(id, empName, empJob, deptId);
}
再次运行测试用例,测试通过
总结:慎用@Data注解,使用@Getter、@Setter注解,需要时自己重写toString()、equals()以及hashCode()方法
审计Auditing
参考自官方文档5.9Auditing
一般数据库表在设计时都会添加4个审计字段,Spring Data Jpa同样支持审计功能。Spring Data提供了@CreatedBy,@LastModifiedBy,@CreatedDate,@LastModifiedDate4个注解来记录表中记录的创建及修改信息。
实体类
package com.example.springbootjpa.entity;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.util.Date;
import java.util.Set;
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "tb_user")
@Data
public class User {
@Id
@GenericGenerator(name = "idGenerator", strategy = "uuid")
@GeneratedValue(generator = "idGenerator")
private String id;
@Column(name = "username", unique = true, nullable = false, length = 64)
private String username;
@Column(name = "password", nullable = false, length = 64)
private String password;
@Column(name = "email", unique = true, length = 64)
private String email;
@ManyToMany(targetEntity = Role.class, cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(name = "tb_user_role", joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
private Set<Role> roles;
@CreatedDate
@Column(name = "created_date", updatable = false)
private Date createdDate;
@CreatedBy
@Column(name = "created_by", updatable = false, length = 64)
private String createdBy;
@LastModifiedDate
@Column(name = "updated_date")
private Date updatedDate;
@LastModifiedBy
@Column(name = "updated_by", length = 64)
private String updatedBy;
}
实体类上还添加了@EntityListeners(AuditingEntityListener.class),而AuditingEntityListener是由Spring Data Jpa提供的
实现AuditorAware接口
光添加了4个审计注解还不够,得告诉程序到底是谁在创建和修改表记录
package com.example.springbootjpa.auditing;
import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.of("admin");
}
}
这里简单的返回了一个"admin"字符串来代表当前用户名
启用Jpa审计功能
在Spring Boot启动类上添加@EnableJpaAuditing注解用于启用Jpa的审计功能
package com.example.springbootjpa;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@SpringBootApplication
@EnableJpaAuditing
public class SpringBootJpaApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootJpaApplication.class, args);
}
}