Spring Data Jpa笔记

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);
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值