如何避免在Hibernate中获取双向集合时出现无限循环?

4
我正在尝试在一个非常简单的Hibernate示例中填充一些实体对象。我的数据库由两个表组成,"Departments"(Id,Name)和"Employees"(Id,DepartmentsId,FirstName,LastName)。我的SQL查询只是将Employees左连接到Departments。
我已经按照Hibernate文档中指定的注释设置进行了设置,但每当我尝试序列化实体时,Hibernate就会进入无限循环,并最终抛出StackOverFlowError异常。回答我的另一个问题的人能够确定堆栈溢出是因为"Department"对象包含一组"Employee"对象,每个对象都包含一个"Department"对象,其中包含一组Employee对象,依此类推。
这种双向关系应该是合法的,根据上面链接的文档(Department中的“mappedBy”参数应该能够提示Hibernate;我还尝试使用了下面代码中被注释掉的“joinColumn”注解),我读到的其他信息表明Hibernate应该聪明到不会在这种情况下进入无限循环,但是对于我的示例却没有起作用。如果我通过从Employee类中删除Department对象将双向关系改为单向关系,则一切正常运行,但显然这会导致许多功能的丧失。
我还尝试放弃旧的xml映射文件的注释,并为子表设置“inverse”参数,但仍然产生相同的问题。我该如何使这种双向关系按照预期工作?
部门:
package com.test.model;

import java.util.HashSet;
import java.util.Set;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.JoinTable;

import static javax.persistence.GenerationType.IDENTITY;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.JoinColumn;

import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;

@Entity
@Table(name="Departments"
,catalog="test"
)
public class Department implements java.io.Serializable {

 private Integer id;
 private String name;
 public Set<Employee> employees = new HashSet<Employee>(0);

public Department() {
}


public Department(String name) {
    this.name = name;
}
public Department(String name, Set employees) {
   this.name = name;
   this.employees = employees;
}

 @Id @GeneratedValue(strategy=IDENTITY)


@Column(name="Id", unique=true, nullable=false)
public Integer getId() {
    return this.id;
}

public void setId(Integer id) {
    this.id = id;
}


@Column(name="Name", nullable=false)
public String getName() {
    return this.name;
}

public void setName(String name) {
    this.name = name;
}

@OneToMany(fetch=FetchType.LAZY, mappedBy="department")
/*@OneToMany
@JoinColumn(name="DepartmentsId")*/
public Set<Employee> getEmployees() {
    return this.employees;
}

public void setEmployees(Set employees) {
    this.employees = employees;
}
}

员工:
package com.test.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.JoinTable;

import static javax.persistence.GenerationType.IDENTITY;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="Employees"
,catalog="test"
)
public class Employee  implements java.io.Serializable {


 private Integer id;
 private Department department;
 private String firstName;
 private String lastName;

public Employee() {
}

public Employee(Department department, String firstName, String lastName) {
   this.department = department;
   this.firstName = firstName;
   this.lastName = lastName;
}

 @Id @GeneratedValue(strategy=IDENTITY)


@Column(name="Id", unique=true, nullable=false)
public Integer getId() {
    return this.id;
}

public void setId(Integer id) {
    this.id = id;
}

@ManyToOne
@JoinColumn(name="DepartmentsId", nullable=false, insertable=false, updatable=false)
public Department getDepartment() {
    return this.department;
}

public void setDepartment(Department department) {
    this.department = department;
}


@Column(name="FirstName", nullable=false)
public String getFirstName() {
    return this.firstName;
}

public void setFirstName(String firstName) {
    this.firstName = firstName;
}


@Column(name="LastName", nullable=false)
public String getLastName() {
    return this.lastName;
}

public void setLastName(String lastName) {
    this.lastName = lastName;
}
}

部门经理(包含HQL查询):

package com.test.controller;

import java.util.Collections;
import java.util.List;

import java.util.Iterator;

import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.Session;

import com.test.model.Department;
import com.test.util.HibernateUtil;

public class DepartmentManager extends HibernateUtil {
public List<Department> list() {
    Session session = HibernateUtil.getSessionFactory().getCurrentSession();
    session.beginTransaction();
    List<Department> set = null;
    try {
        Query q = session.createQuery("FROM Department d JOIN FETCH d.employees e");
        q.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
        set = (List<Department>) q.list();
    } catch (HibernateException e) {
        e.printStackTrace();
        session.getTransaction().rollback();
    }
    session.getTransaction().commit();
    return set;
}
}

这些演示代码中是否有任何一个引起了异常,还是只是在序列化期间出现的?因为如果只是在集合序列化期间出现异常,那就很有道理。这是因为在序列化期间,每个实体都充当代理。如果你拿到一个部门并说“给我它的员工”,然后对于每个员工你说“给我部门”,然后对于每个部门说“给我员工”……我会预计会出现堆栈溢出。这只是猜测,但如果你要序列化为JSON,那么可以使用struts2-json-plugin。该序列化程序提供包含和排除参数。 - Quaternion
这将让您修剪树,阻止它在您想要的深度处。在您提供的关系的情况下,参数可以为不同目的修剪树,因此这个单一查询可能最好由几个不同的操作表示。 - Quaternion
我相信异常只是在序列化期间发生的,所以你说的很有道理。我使用的是XSLT结果,而不是JSON,但问题可能是一样的。Struts2曾经提供一些包含/排除模式,但这个功能在某个时候被破坏了,从未修复过。它提供了一个“exposedValue”参数,使用OGNL表达式,但我从来没有让它正常工作过(我可以用“[0].departmentsList”指定Department对象列表,但无法指定特定字段,比如“[0].departmentsList.id”)。也许我将不得不走Bozho下面提到的DTO路线。 - J Ellis
Struts2的版本可能会有所不同:http://struts.apache.org/2.3.1.2/struts2-core/apidocs/index.html 在DTO案例(+1)中,您可以使用“new”创建实体,而不是通过Hibernate,并将数据从Hibernate对象传输到POJO,而无需任何代理魔法。 xslt结果类型将能够处理这些循环依赖关系,问题在于当前可能正在构造新对象...因此,尽管对象包含相同的数据,但它们可能不是相同的对象(不确定实体如何被过氧化,因此可能是错误的)。 - Quaternion
2个回答

7
通常情况下,您不应该序列化实体。循环依赖关系和代理会使此操作变得困难。相反,您应手动将要发送的数据转移到DTO(一个新的仅包含数据的类)中,并对其进行序列化。这样做将不包含懒加载集合、代理等内容。

我考虑过使用DTO,但没有看到好的例子来让它们正常工作。例如,如果我要获取集合并将其传输到DTO中,而不触发循环引用,该怎么办?如果集合未填充(比如我没有进行连接),我如何访问它以查找是否有任何需要填充到DTO中的内容,而不会触发LazyInitializationException或导致Hiberante自动运行另一个查询来填充它(当使用OpenSessionInView过滤器时会这样做)?一些在Hibernate中如何填充DTO的好例子将非常有帮助。 - J Ellis

-1

为了补充前面的回答,我写了一个通用的转换方法,可以将实体值转移到DTO对象中,您只需要将DTO字段与映射实体的字段名称相同即可。

以下是源代码。

/** * 将一个对象的相应字段的值分配给目标对象。已填充目标对象的字段不会被替换 * * @param objetoOrigem * @param objetoDestino * @return * @throws NegocioException */

public static <T1, T2> T2  convertEntity(T1 objetoOrigem, T2 objetoDestino) throws NegocioException {

    if (objetoOrigem != null && objetoDestino != null) {
        Class<? extends Object> classe = objetoOrigem.getClass();
        Class<? extends Object> classeDestino = objetoDestino.getClass();

        Field[] listaCampos = classe.getDeclaredFields();
        for (int i = 0; i < listaCampos.length; i++) {
            Field campo = listaCampos[i];
            try {
                Field campoDestino = classeDestino.getDeclaredField(campo.getName());
                campo.setAccessible(true);
                campoDestino.setAccessible(true);
                atribuiValorAoDestino(objetoOrigem, objetoDestino, campo, campoDestino);
            } catch (NoSuchFieldException e) {
                LOGGER.log(Logger.Level.TRACE, (Object) e);
                continue;
            } catch (IllegalArgumentException | IllegalAccessException e) {
                LOGGER.error(e.getMessage(), e);
                throw new NegocioException(e.getMessage(), EnumTypeException.ERROR);
            }
        }
    }
    return objetoDestino;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接