Hibernate 关联时的问题及解决
项目类型: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.只有一方需要数据(如果你的业务单向关联就可以满足)
-
2.双方数据都需要的情况(如果你的业务必须使用双向关联)
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方式的,这种方式比较全能,推荐使用。