Hibernate 关联时的问题及解决

2019-11-29  本文已影响0人  宇宙小神特别萌
Hibernate 关联时的问题及解决目录.png

项目类型:spring5.x+hibernate5

一、no session 原因及解决

1、原因:load()延迟查询引起noSession问题

问题分析:对于根据id查询时,在dao通过load方式查询对象时,加载页面会报 noSession异常。当使用hibernate框架操作数据库的时候,如果做查询的话会有立即加载(get)和延迟加载(load)的区别,延迟加载表示,当你查询某个数据(假设是对象)的时候,hibernate不会立马发送sql语句,而是当我们调用这个对象的属性的时候,也就是真正使用查询出来的数据的时候才会发送sql语句去一级缓存(即session,这里的session和域对象session没有半毛钱关系)中获取,但是正常这个session的开启核关闭是在service层执行的,但是我们真正使用查询的对象的数据时,是在web层,但是这个时候session已经关闭,就会报no-session异常。

noSession分析图

解决方式1:让session的关闭时间要在web层使用完之后。 但是web层已经是最后一层了,怎么办?还有比web更后的东西哦,就是过滤器, 所以在web.xml中配置开启和关闭session的过滤器即可 ,但是要配在strut或springmvc过滤器器之前,否则无效。

web.xml

    <!--hibernate延迟加载,配置在所有filter之前(springmvc过滤器前) 解决无限循环和no Session问题,在get方法上使用@JsonBackReference-->
    <filter>
        <filter-name>hibernateFilter</filter-name>
        <filter-class>org.springframework.orm.hibernate5.support.OpenSessionInViewFilter</filter-class>
        <init-param>
            <param-name>sessionFactoryBeanName</param-name>
            <param-value>sessionFactory</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>hibernateFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>

添加过滤器解决noSession分析图

使用load方法的解决方案 : 就是把原CustomerService层的绑定的session对象 提取配置到前面的 过滤器中了。

解决方式2: load改用get立即加载方式查询对象。

    /**
     * 根据id获取用户信息-成功
     * 查询数据不存在,返回null,推荐使用, 不存在延迟加载问题,不采用lazy机制的
     *
     * @param id
     */
    @Override
    public UserEntity getEntity(Integer id) {
        return hibernateTemplate.get(UserEntity.class, id);
    }

    /**
     * load查询单条数据,查询数据不存在,报异常(noSession),不推荐使用,存在延迟加载问题-失败
     *
     * @param id
     */
    @Override
    public UserEntity loadEntity(Integer id) {
        return hibernateTemplate.load(UserEntity.class, id);
    }
    

2、原因:没有开启使用注解事务(或者已经开启注解事务但并未使用)

    <!-- 注解方式(配置事物):通过@Transactional启用事物管理,xml方式的配置会覆盖注解配置 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
    

解决方式:开启注解事务并在需要事务环境的类中使用,@Transactional注解可以使用在类上,也可以使用在方法上

3、原因:手动配置事务时,propagation的值并未含有事务环境

<!-- 手动配置事务通知属性 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <!-- 定义事务传播属性 -->
        <tx:attributes>
            <!--方法头-->
            <tx:method name="save*" propagation="REQUIRED"/>
            <tx:method name="merge*" propagation="REQUIRED"/>
            <tx:method name="insert*" propagation="REQUIRED"/>
            <tx:method name="add*" propagation="REQUIRED"/>
            <tx:method name="set*" propagation="REQUIRED"/>
            <tx:method name="new*" propagation="REQUIRED"/>
            <tx:method name="update*" propagation="REQUIRED"/>
            <tx:method name="edit*" propagation="REQUIRED"/>
            <tx:method name="remove*" propagation="REQUIRED"/>
            <tx:method name="delete*" propagation="REQUIRED"/>
            <tx:method name="execute*" propagation="REQUIRED"/>
            <tx:method name="change*" propagation="REQUIRED"/>
            <tx:method name="get*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="find*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="load*" propagation="REQUIRED" read-only="true"/>
            <tx:method name="*" propagation="REQUIRED" read-only="true"/>
        </tx:attributes>
    </tx:advice>

解决方式: 添加方法头,特别注意,将propagation值设置为REQUIRED

二、懒加载出现死循环

懒加载出现死循环场景:多对多关联查询,多对一和一对多查询、自关联查询 等会出现对象死循环。

原因:(你中有我,我中有你)

具体分析 在于你要转化的对象里配置了对另外一个对象的关联,而那个对象里又配置了对你这个对象的关联,两个对象配置了双向的一对多和多对一的关联关系。 JSON lib在把Dept部门对象转化为json字符串的时候,发现dept里有个Set<Employee>,它就会去级联的把Set<Employee>转化为json字符串,在它遍历Set的时候,发现Employee员工里又有一个Dept部门对象,这时候它又会去尝试把dept转化为json字符串,然后就发现dept里又有Set<Employee>,如此周而复始,就形成了死循环。

代码演示:一对多(部门对员工)、多对一(员工对部门)

//员工
@Getter
@Setter
public class Employee {
    private int empId;// 员工的编号
    private String empName;// 员工的名称
    private double salary;// 员工的薪资

    private Dept dept;// 员工和部门的关系 
}

//部门
@Getter
@Setter
public class Dept {
    private int deptId;// 部门编号
    private String deptName;// 部门名称

    private Set<Employee> emps;// 部门对应多个员工,即一对多的关系
}

Dept.hbm.xml

    <!--
      第三写其他映射,比如这里的set集合映射,set集合映射主要有以下几点:
        1 实体类申明的集合属性属性(name)
        2 集合属性对应的表(table)
        3 指定集合表的外键字段名称(key中的column)
        4 集合对象对应的实体类(noe-to-many中的class)
        -->
<!--一对多(一个部门对应多个员工)-->
<set name="emps" table="t_employee">
    <!--column指定了员工表的外键字段名称 -->
    <key column="deptId"/>
    <!-- class由于上面已经写了包名,这里直接使用即可 -->
    <one-to-many class="Employee" />
</set>

Employee.hbm.xml

    <!--
    多对一的映射配置:多个员工对应一个部门
    name是实体类中申明的属性;
    column外键字段名称,对应多的一方的数据库表中的外键字段名;
    class对应的部门实体类;
    -->
<many-to-one name="dept" column="deptId" class="Dept"/>

出现死循环:用浏览器访问显示

{ "deptId" : 1, "deptName" : "人事部", "emps" : [ { "empId" : 1, "empName" : "员工1", "salary" : 1000.0, "dept" : { "deptId" : 1, "deptName" : "人事部", "emps" : [ { "empId" : 1, "empName" : "员工1", "salary" : 1000.0, "dept" : { "deptId" : 1, "deptName" : "人事部", "emps" : [ { "empId" : 1, "empName" : "员工1", "salary" : 1000.0, "dept" : { "deptId" : 1, "deptName" : "人事部", "emps" : [ { "empId" : 1, "empName" : "员工1", "salary" : 1000.0, "dept" : { "deptId" : 1, "deptName" : "人事部", "emps" : [ { "empId" : 1, "empName" : "员工1", "salary" : 1000.0, "dept" : { "deptId" : 1, "deptName" : "人事部", "emps" : [ { "empId" : 1, "empName" : "员工1", "salary" : 1000.0, "dept" : { "deptId" : 1, "deptName" : "人事部", "emps" : [ { "empId" : 1, "empName" : "员工1", "salary" : 1000.0, "dept" : { "deptId" : 1, "deptName" : "人事部", "emps" : [ { "empId" : 1, "empName" : "员工1", "salary" : 1000.0, "dept" : { "deptId" : 1, "deptName" : "人事部", "emps" : [ { "empId" : 1, "empName" : "员工1", "salary" : 1000.0, "dept" : { "deptId" : 1, "deptName" : "人事部", "emps" : 

后台出现异常:java.lang.StackOverflowError 死循环

要考虑两个大的方面:

1、只有一方需要数据

解决思路:由于引起死循环查询的原因是序列化产生的,那么,针对序列化做处理,让不需要返回给前端的一方的对象不序列化。

解决方式1:使用@JsonIgnore

@Getter
@Setter
public class Dept {
    private int deptId;// 部门编号
    private String deptName;// 部门名称

    @JsonIgnore //忽略,返回给前端时忽略此属性
    private Set<Employee> emps;// 部门对应多个员工,即一对多的关系
}

@Getter
@Setter
public class Employee {
    private int empId;// 员工的编号
    private String empName;// 员工的名称
    private double salary;// 员工的薪资

    @JsonIgnore //忽略,返回给前端时忽略此属性
    private Dept dept;// 员工和部门的关系
}

注:不够友好,双方都要配置注解,若只配置一方,则另一方会报异常。

效果:

//查询部门信息
{
    "deptId":1,
    "deptName":"人事部"
}

//查询员工信息
{
    "empId":1,
    "empName":"员工1",
    "salary":1000.0
}

解决方式2:使用fastjson

    <!--解决hibernate 关联关系死循环问题-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.59</version>
        </dependency>
        

spring-mvc.xml配置

    <!--开启springMVC注解驱动. SpringMVC整合FastJson:用"最快的json转换工具"替换SpringMVC的默认json转换-->
    <mvc:annotation-driven>
        <!--不使用默认消息转换器 -->
        <mvc:message-converters register-defaults="false">
            <!--spring消息转换器 -->
            <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter"/>
            <bean class="org.springframework.http.converter.BufferedImageHttpMessageConverter"/>

            <!--解决@Responcebody中文乱码问题 -->
            <bean class="org.springframework.http.converter.StringHttpMessageConverter">
                <constructor-arg value="UTF-8"/>
            </bean>
            <!--配合fastjson支持 -->
            <bean class="com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter">
                <property name="defaultCharset" value="UTF-8"/>
                <property name="supportedMediaTypes">
                    <list>
                        <!--顺序保持这样,避免IE下载出错 -->
                        <value>text/html;charset=UTF-8</value>
                        <value>text/json;charset=UTF-8</value>
                        <value>application/json</value>
                    </list>
                </property>
                <property name="fastJsonConfig" ref="fastJsonConfig"/>
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <!--fastJsonConfig -->
    <bean id="fastJsonConfig" class="com.alibaba.fastjson.support.config.FastJsonConfig">
        <!--默认编码格式 -->
        <property name="charset" value="UTF-8"/>
        <property name="serializerFeatures">
            <list>
                <value>WriteNullListAsEmpty</value>
                <value>WriteDateUseDateFormat</value>
                <value>PrettyFormat</value>
                <value>WriteMapNullValue</value>
                <value>WriteNullStringAsEmpty</value>
                <value>WriteNullListAsEmpty</value>
                <value>DisableCircularReferenceDetect</value>
            </list>
        </property>
    </bean>
    <!--fastjson支持配置结束 -->

使用fastjson注解 @JSONField(serialize=false)

@Getter
@Setter
public class Dept {
    private int deptId;// 部门编号
    private String deptName;// 部门名称

    @JSONField(serialize=false)
    private Set<Employee> emps;// 部门对应多个员工,即一对多的关系
}

@Getter
@Setter
public class Employee {
    private int empId;// 员工的编号
    private String empName;// 员工的名称
    private double salary;// 员工的薪资

    //@JSONField(serialize=false)
    private Dept dept;// 员工和部门的关系
}

效果:

//查询部门信息
{
    "deptId":1,
    "deptName":"人事部"
}

//查询员工信息
{
    "dept":{
        "deptId":1,
        "deptName":"人事部"
    },
    "empId":1,
    "empName":"员工1",
    "salary":1000.0
}

这种,比"解决方式1"更友好,只需要单方面取关联数据,完美解决序列化时导致的死循环问题

解决方式3:@JsonBackReference

web.xml配置

    <!--hibernate延迟加载,配置在所有filter之前 解决无限循环和no Session问题,在get方法上使用@JsonBackReference-->
    <filter>
        <filter-name>hibernateFilter</filter-name>
        <filter-class>org.springframework.orm.hibernate5.support.OpenSessionInViewFilter</filter-class>
        <init-param>
            <param-name>sessionFactoryBeanName</param-name>
            <param-value>sessionFactory</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>hibernateFilter</filter-name>
        <url-pattern>/*</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
    </filter-mapping>
    

使用@JsonBackReference

@Getter
@Setter
public class Dept {
    private int deptId;// 部门编号
    private String deptName;// 部门名称

    //@JsonBackReference //尽量放到get方法上,在序列化时,@JsonBackReference的作用相当于@JsonIgnore
    private Set<Employee> emps;// 部门对应多个员工,即一对多的关系
}

@Getter
@Setter
public class Employee {
    private int empId;// 员工的编号
    private String empName;// 员工的名称
    private double salary;// 员工的薪资
    
    @JsonBackReference //尽量放到get方法上,在序列化时,@JsonBackReference的作用相当于@JsonIgnore
    private Dept dept;// 员工和部门的关系
}

效果:

//查询部门信息
{
  "deptId": 1,
  "deptName": "人事部",
  "emps": [
    {
      "empId": 2,
      "empName": "员工2",
      "salary": 2000
    },
    {
      "empId": 1,
      "empName": "员工1",
      "salary": 1000
    }
  ]
}

//查询员工信息
{
  "empId": 1,
  "empName": "员工1",
  "salary": 1000
}

效果相当于"解决方式2"的效果。

2、双方数据都需要的情况

解决方式1:返回给前端DTO

DTO与实体类一样,下面是DTO

//部门DTO
@Getter
@Setter
public class DeptDTO {
    private int deptId;// 部门编号
    private String deptName;// 部门名称

    private Set<EmployeeDTO> employeeDTOSet;// 部门对应多个员工,即一对多的关系
}

//员工DTO
@Getter
@Setter
public class EmployeeDTO {
    private int empId;// 员工的编号
    private String empName;// 员工的名称
    private double salary;// 员工的薪资

    private DeptDTO deptDTO;// 员工和部门的关系
}

实体类:

//部门实体类
@Getter
@Setter
public class Dept {
    private int deptId;// 部门编号
    private String deptName;// 部门名称

    private Set<Employee> emps;// 部门对应多个员工,即一对多的关系
}

//员工实体类
@Getter
@Setter
public class Employee {
    private int empId;// 员工的编号
    private String empName;// 员工的名称
    private double salary;// 员工的薪资

    private Dept dept;// 员工和部门的关系
}

使用DTO获取双向数据

    //获取部门
    @Override
    public Object getDept(int deptId){
        Dept dept = this.hibernateTemplate.get(Dept.class, deptId);
        System.out.println("部门名称:"+dept.getDeptName());
        Set<Employee> emps = dept.getEmps();
        Iterator<Employee> iterator = emps.iterator();
        Set<EmployeeDTO> employeeDTOSet = new HashSet<>();
        while (iterator.hasNext()){
            Employee employee = iterator.next();
            System.out.println("员工名称:"+employee.getEmpName());

            EmployeeDTO employeeDTO =new EmployeeDTO();
            employeeDTO.setEmpId(employee.getEmpId());
            employeeDTO.setEmpName(employee.getEmpName());
            employeeDTO.setSalary(employee.getSalary());
            employeeDTOSet.add(employeeDTO);
        }

        DeptDTO deptDTO = new DeptDTO();
        deptDTO.setDeptId(dept.getDeptId());
        deptDTO.setDeptName(dept.getDeptName());
        deptDTO.setEmployeeDTOSet(employeeDTOSet);

        return deptDTO;
        //return dept;
    }
    //获取所有部门
    @Override
    public Object getAllDept(){
        return this.hibernateTemplate.loadAll(Dept.class);
    }

    //获取员工
    @Override
    public Object getEmployee(int empId){
        Employee employee = this.hibernateTemplate.get(Employee.class, empId);
        System.out.println("员工名称:"+employee.getEmpName());
        Dept dept = employee.getDept();
        System.out.println("员工【 "+employee.getEmpName()+" 】属于 部门:"+dept.getDeptName());

        EmployeeDTO employeeDTO = new EmployeeDTO();
        employeeDTO.setEmpId(employee.getEmpId());
        employeeDTO.setEmpName(employee.getEmpName());
        employeeDTO.setSalary(employee.getSalary());

        DeptDTO deptDTO = new DeptDTO();
        deptDTO.setDeptId(employee.getDept().getDeptId());
        deptDTO.setDeptName(employee.getDept().getDeptName());
        employeeDTO.setDeptDTO(deptDTO);

        return employeeDTO;
        //return employee;
    }
    

效果:

//获取部门信息
{
  "deptId": 1,
  "deptName": "人事部",
  "employeeDTOSet": [
    {
      "empId": 2,
      "empName": "员工2",
      "salary": 2000,
      "deptDTO": null
    },
    {
      "empId": 1,
      "empName": "员工1",
      "salary": 1000,
      "deptDTO": null
    }
  ]
}

//获取员工信息
{
  "empId": 1,
  "empName": "员工1",
  "salary": 1000,
  "deptDTO": {
    "deptId": 1,
    "deptName": "人事部",
    "employeeDTOSet": null
  }
}

当然"只有一方需要数据",也是可以使用这种DTO方式的,这种方式比较全能,推荐使用。

上一篇下一篇

猜你喜欢

热点阅读