Mapping JPA entities to successfully transcode them using XML

Comments

Typically, when you model using Java™ Persistence Architecture (JPA) or any other Object Relational Model (ORM), the entities refer to each other to enable relational persistence. Using these circular references works well at the object model layer but when serializing the objects to send data from client to server or from server to client using XML, this practice introduces a cycle.

Object models designed in the Java programming language often have cycles, which prevent straightforward conversion to XML by Java Architecture for XML Binding (JAXB). When you try to marshal an object tree that contains a cycle, the JAXB Marshaller reports an error and points out the objects that formed the cycle. This approach is used because by itself, JAXB cannot determine how to cut cycles into a tree.

Therefore, you must annotate classes and use other means to indicate how JAXB is to handle a cycle. Listing 1 shows a sample cycle error.

Listing 1. Typical cycle error
[com.sun.istack.internal.SAXException2: A cycle is detected in the object graph. This will cause 
infinitely deep XML: entities.tr.library.Department@a420a42 -> entities.tr.library.Employee@4bc84bc8 -
> entities.tr.library.Department@a420a42]
    at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:317)
    at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:243)
    at javax.xml.bind.helpers.AbstractMarshallerImpl.marshal(AbstractMarshallerImpl.java:74)

To resolve cycle issues, consider the following solutions:

  • Add @XmlTransient to the getter of the property that introduces the cycle.
  • Use @XmlInverseReference to get the data that becomes null because of @XmlTransient.
  • Implement XmlAdapter to resolve the cycle error and get the data, because @XmlInverseReference fails for chained entities.
  • Use @XmlID-@XmlIDREF annotation to resolve the cycle error and define a container class to get the complete data that is missing as a result of the @XmlIDREF annotation.

This article explains the implementation and limitations of each solution, based on the following scenario.

A Company entity has bidirectional relationships with Department and Employee entities. These entities, in turn, have bidirectional relationships defined with each other, as shown in Figure 1.

Figure 1. Example entities
Bidirectional relationships

As shown in Listing 2, the entity classes are defined and the definitions show the relationship between the entities.

Listing 2. Entity class definitions show relationship between entities

// Parent entity

public class Company implements Serializable {
    private static final long serialVersionUID = 0;

    @Column(name = "ID", unique = true, nullable = false, updatable = false)
    @Id
    protected Integer id;
       …
       …
       …
    @OneToMany(mappedBy = "company", cascade = CascadeType.ALL) 
    Set<Employee> employees; 
       …
       …
       …
    @OneToMany(mappedBy = "company", cascade = ALL) 
    Set<Department> departments; 
       …
       …
       …
}

// Child1 entity

public class Department implements Serializable {
    private static final long serialVersionUID = 0; 

    @Column(name = "ID", unique = true, nullable = false, updatable = false) 
    @Id
    protected Integer id; 
       …
       …
       …
    @ManyToOne(optional = true, cascade = ALL) 
    @JoinColumn(name = "COMPANY_ID", referencedColumnName = "ID")
    Company company; 
       …
       …
       …
    @OneToMany(mappedBy = "department", cascade = ALL) 
    Set<Employee> employees; 
       …
       …
       …
}

// Child2 entity

public class Employee implements Serializable {
    private static final long serialVersionUID = 0; 
   
    @Column(name = "ID", unique = true, nullable = false) 
    @Id
    protected Integer id; 
       …
       …
       …
    @ManyToOne(optional = true, cascade = ALL) 
    @JoinColumn(name = "DEPARTMENT_ID", referencedColumnName = "ID")
    Department department; 
       …
       …
       …
    @ManyToOne(optional = true, cascade = ALL) 
    @JoinColumn(name = "COMPANY_ID", referencedColumnName = "ID")
    Company company; 
       …
       …
       …
}

If you marshal these entities by using XML serialization, a cycle error is introduced for the following relationships:

  • Company > Department > Company
  • Company > Employee > Company
  • Department > Employee > Department

The following solutions can resolve the cycle errors.

Use @XmlTransient to resolve the error

This is the most popular solution to resolve cycle errors detected by JAXB when serializing entity objects that contain bidirectional relationships. However, the effect of this annotation is that the XmlTransient data is null after deserializing. This solution, shown in Listing 3, works well as long as the parent entity is being used to send and get the entity data.

Listing 3. @XmlTransient implementation

// Parent entity

public class Company implements Serializable {
    private static final long serialVersionUID = 0; 

    @Column(name = "ID", unique = true, nullable = false, updatable = false) 
    @Id
    protected Integer id; 
       …
       …
       …
    @OneToMany(mappedBy = "company", cascade = CascadeType.ALL) 
    Set<Employee> employees; 
       …
       …
       …
    @OneToMany(mappedBy = "company", cascade = ALL) 
    Set<Department> departments; 
       …
       …
       …
}

// Child1 entity

public class Department implements Serializable {
    private static final long serialVersionUID = 0; 

    @Column(name = "ID", unique = true, nullable = false, updatable = false) 
    @Id
    protected Integer id; 
       …
       …
       …
    @XmlTransient
    @ManyToOne(optional = true, cascade = ALL) 
    @JoinColumn(name = "COMPANY_ID", referencedColumnName = "ID")
    Company company; 
       …
       …
       …
    @OneToMany(mappedBy = "department", cascade = ALL) 
    Set<Employee> employees; 
       …
       …
       …
}

// Child2 entity

public class Employee implements Serializable {
    private static final long serialVersionUID = 0; 
   
    @Column(name = "ID", unique = true, nullable = false) 
    @Id
    protected Integer id; 
       …
       …
       …
    @XmlTransient
    @ManyToOne(optional = true, cascade = ALL) 
    @JoinColumn(name = "DEPARTMENT_ID", referencedColumnName = "ID")
    Department department; 
       …
       …
       …
    @XmlTransient
    @ManyToOne(optional = true, cascade = ALL) 
    @JoinColumn(name = "COMPANY_ID", referencedColumnName = "ID")
    Company company; 
       …
       …
       …
}

As shown in Listing 3, the property that introduces the cycle in the XML graph is made null using @XmlTransient annotation. This approach stops the cycle but it also makes the data null, at that point. As long as the parent entity is used to persist and retrieve the child entity, this solution works well. If Company is used to both save Department and get Department data, this model works well. But if Department is used to get the data, Company becomes null. For example, if you need to get Company data in the Department entity when the Department entity is sent from the server to the client or from the client to server using XML, this solution fails.

Use @XmlInverseReference to resolve the error

Listing 4 shows the solution as suggested by theEclipseLink MOXy implementation to solve cycle errors detected by JAXB when serializing entity objects that contain bidirectional relationships.

Listing 4. @XmlInverseReference implementation for the example entities

// Parent entity

public class Company implements Serializable {
    private static final long serialVersionUID = 0; 

    @Column(name = "ID", unique = true, nullable = false, updatable = false) 
    @Id
    protected Integer id; 
       …
       …
       …
    @OneToMany(mappedBy = "company", cascade = CascadeType.ALL) 
    Set<Employee> employees; 
       …
       …
       …
    @OneToMany(mappedBy = "company", cascade = ALL) 
    Set<Department> departments; 
       …
       …
       …
}

// Child1 entity

public class Department implements Serializable {
    private static final long serialVersionUID = 0; 

    @Column(name = "ID", unique = true, nullable = false, updatable = false) 
    @Id
    protected Integer id; 
       …
       …
       …
    @ManyToOne(optional = true, cascade = ALL) 
    @JoinColumn(name = "COMPANY_ID", referencedColumnName = "ID")
    @XmlElement
    @XmlInverseReference(mappedBy="departments") Company company; … … … @OneToMany(mappedBy = "department", cascade = ALL) Set<Employee> employees; … … … }

// Child2 entity

public class Employee implements Serializable {
    private static final long serialVersionUID = 0; 
   
    @Column(name = "ID", unique = true, nullable = false) 
    @Id
    protected Integer id; 
       …
       …
       …
    @ManyToOne(optional = true, cascade = ALL) 
    @JoinColumn(name = "DEPARTMENT_ID", referencedColumnName = "ID")
    @XmlElement
    @XmlInverseReference(mappedBy="employees") Department department; … … … @ManyToOne(optional = true, cascade = ALL) @JoinColumn(name = "COMPANY_ID", referencedColumnName = "ID") @XmlElement
    @XmlInverseReference(mappedBy="employees") Company company; … … … }

In the solution shown in Listing 4, @XmlInverseReference annotation acts in a manner similar to @XmlTransient during the marshal operation. This action stops the cycle. It also enables you to map a back pointer during the unmarshal operation, by pairing it with the @XmlElement annotation. Therefore, the data is not null and is available after deserialization.

In this case, you can use Company or Department to set and get the data. The complete data is available in both the cases. This approach works when the entities have bidirectional relationships with each other only. It does not work when the relationship goes beyond two entities, as in the case where entity relationships are chained: Company > Department > Employee > Address > Company.

@XmlInverseReference fails to resolve the cycle error, even though @XmlInverseReference is set for relationships between Company and Department at one end and between Address and Company on the other end. Therefore, marshalling for such a relationship fails and results in the XML serialization error: A cycle is detected in the object graph.

Use @XmlAdapter to resolve the error

The generic solution to solve this issue is to define an XmlAdapter per entity.

In the XmlAdapter implementation class, override the marshal and unmarshal operations and code to remove cycle errors in the XML marshalling and to identify how to set the data correctly during unmarshalling. To remove the cycle errors, use an ArrayList to store the entity, as shown in Listing 5. Use the list to return the entity reference if the entity is referred to again during marshalling. This action breaks the cycle. In unmarshalling, use a map to save the reference with the entity. This data is used to create the entity when its reference is found in the XML. The entity classes are extended for the XmlAdapter implementation.

Listing 5. @XmlAdapter implementation

// Parent entity

public class Company extends XmlAdapter<Company, Company> 
implements Serializable, Cloneable {
    private static final long serialVersionUID = 0; 

    // List and Map for XmlAdapter implementation. 
    // This is used to reference the same entity if it already exists in the 
    // XML and hence avoid cyclic references in marshalling and get back the 
    // data in unmarshalling. 
    @XmlTransient		// Not required to store in the XML. 
    private List<Company> entityList = new ArrayList<Company>();
    @XmlTransient		// Not required to store in the XML. 
    private Map<String, Company> entityMap = new HashMap<String, Company>();

    // Entity reference for XmlAdapter implementation. 
    @XmlAttribute
    @Transient		// Not required to store reference in the DB. 
    private String reference; 
    
    @Column(name = "ID", unique = true, nullable = false, updatable = false) 
    @Id
    protected Integer id; 
       …
       …
       …
    @OneToMany(mappedBy = "company", cascade = CascadeType.ALL) 
    @XmlJavaTypeAdapter(Employee.class) 
    Set<Employee> employees; 
       …
       …
       …
    @OneToMany(mappedBy = "company", cascade = ALL) 
    @XmlJavaTypeAdapter(Department.class) 
    Set<Department> departments; 
       …
       …
       …

    public Company() {
    }


    @Override
    public Company marshal(Company arg0) throws Exception {
        if (arg0 != null) {
            // Check if the entity has been marshalled before. 
            // If so return only the reference. 
            if (entityList.contains(arg0)) {
                Company adaptedEntity = new Company();
                adaptedEntity.reference = Integer.toString(arg0.getId());
                return adaptedEntity; 
            } else {
                Company adaptedEntity = (Company) arg0.clone();
                entityList.add(arg0); 
                return adaptedEntity; 
            }
        } else {
            return null; 
        }
    }

    @Override
    public Company unmarshal(Company arg0) throws Exception {
        Company entity = entityMap.get(arg0.reference); 
        if (null == entity) {
            entity = (Company) arg0.clone();
            entityMap.put(Integer.toString(entity.getId()), entity); 
        }
        return entity; 
    }
   
}

// Child1 entity

public class Department extends XmlAdapter<Department, Department> 
implements Serializable, Cloneable {
    private static final long serialVersionUID = 0; 
    
    // List and Map for XmlAdapter implementation. 
    // This is used to reference the same entity if it already exists in the 
    // XML and hence avoid cyclic references in marshalling and get back the 
    // data in unmarshalling. 
    @XmlTransient		// Not required to store in the XML. 
    private List<Department> entityList = new ArrayList<Department>();
    @XmlTransient		// Not required to store in the XML. 
    private Map<String, Department> entityMap = new HashMap<String, Department>();

    // Entity reference for XmlAdapter implementation. 
    @XmlAttribute
    @Transient		// Not required to store reference in the DB. 
    private String reference; 

    @Column(name = "ID", unique = true, nullable = false, updatable = false) 
    @Id
    protected int id; 

    public int getId() {
        return id; 
    }
       …
       …
       …
    @OneToMany(mappedBy = "department", cascade = ALL) 
    @XmlJavaTypeAdapter(Employee.class) 
    Set<Employee> employees; 
       …
       …
       …
    @ManyToOne(optional = true, cascade = ALL) 
    @JoinColumn(name = "COMPANY_ID", referencedColumnName = "ID")
    @XmlJavaTypeAdapter(Company.class) 
    Company company; 
       …
       …
       …
    public Department() {
    }


    @Override
    public Department marshal(Department arg0) throws Exception {
        if (arg0 != null) {
            // Check if the entity has been marshalled before. 
            // If so return only the reference. 
            if (entityList.contains(arg0)) {
                Department adaptedEntity = new Department();
                adaptedEntity.reference = Integer.toString(arg0.getId());
                return adaptedEntity; 
            } else {
                Department adaptedEntity = (Department) arg0.clone();
                entityList.add(arg0); 
                return adaptedEntity; 
            }
        } else {
            return null; 
        }
    }

    @Override
    public Department unmarshal(Department arg0) throws Exception {
        Department entity = entityMap.get(arg0.reference); 
        if (null == entity) {
            entity = (Department) arg0.clone();
            entityMap.put(Integer.toString(entity.getId()), entity); 
        }
        return entity; 
    }
}

// Child2 entity

public class Employee extends XmlAdapter<Employee, Employee> 
implements Serializable, Cloneable {
    private static final long serialVersionUID = 0; 
   
    // List and Map for XmlAdapter implementation. 
    // This is used to reference the same entity if it already exists in the 
    // XML and hence avoid cyclic references in marshalling and get back the 
    // data in unmarshalling. 
    @XmlTransient		// Not required to store in the XML. 
    private List<Employee> entityList = new ArrayList<Employee>();
    @XmlTransient		// Not required to store in the XML. 
    private Map<String, Employee> entityMap = new HashMap<String, Employee>();

    // Entity reference for XmlAdapter implementation. 
    @XmlAttribute
    @Transient		// Not required to store reference in the DB. 
    private String reference; 

    @Column(name = "ID", unique = true, nullable = false) 
    @Id
    protected int id; 

    /** 
     * @return the id
     */
    public int getId() {
        return id; 
    }
       …
       …
       …
    @ManyToOne(fetch = FetchType.LAZY, optional = true) 
    @JoinColumn(name = "DEPARTMENT_ID", referencedColumnName = "ID")
    @XmlJavaTypeAdapter(Department.class) 
    Department department; 
       …
       …
       …
    @ManyToOne(fetch = FetchType.LAZY, optional = true) 
    @JoinColumn(name = "COMPANY_ID", referencedColumnName = "ID")
    @XmlJavaTypeAdapter(Company.class) 
    Company company; 
       …
       …
       …
    public Employee() {
    }


    @Override
    public Employee marshal(Employee arg0) throws Exception {
        if (arg0 != null) {
            // Check if the entity has been marshalled before. 
            // If so return only the reference. 
            if (entityList.contains(arg0)) {
                Employee adaptedEntity = new Employee();
                adaptedEntity.reference = Integer.toString(arg0.getId());
                return adaptedEntity; 
            } else {
                Employee adaptedEntity = (Employee) arg0.clone();
                entityList.add(arg0); 
                return adaptedEntity; 
            }
        } else {
            return null; 
        }
    }

    @Override
    public Employee unmarshal(Employee arg0) throws Exception {
        Employee entity = entityMap.get(arg0.reference); 
        if (null == entity) {
            entity = (Employee) arg0.clone();
            entityMap.put(Integer.toString(entity.getId()), entity); 
        }
        return entity; 
    }
}

This approach does solve the XML cycle issue and also makes complete data available in all the entities but this method leads to the following JPA persistence error.

When Company gets marshaled, it starts marshalling all of its data elements, one by one (ID, Employees, Departments, and so on.) In the Employee entity, Company is encountered again, but this is the first occurrence of Company in the Employees entityList, because the marshaller does not know that the object that invoked the marshalling is the same object type (Company, in this case). So here Company gets marshalled as a complete entity and not as a reference. During unmarshalling, the Company entity in Employee becomes a separate Company object and does not point to the parent Company. Listing 6 shows the XML on marshalling the Company with Employee.

Listing 6. XML on marshalling Company with Employee
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<company>				// parent Company object
    <id>1</id>
…
…
    <employees>			// First Employee object
        <id>100</id>
…
        <company>			// First Company object
            <id>1</id>
…
…
            <employees reference=>	// Second instance of Employee object
                <id>100</id>	  // So reference points to the first object
            </employees>
…
        </company>
…
    </employees>
…
</company>

Additional problems occur during XML unmarshalling. The unmarshalling starts from the leaf nodes in XML. In this case, the leaf node does not have any data. It has only the reference, for example, <employees reference="100"> gets unmarshalled before the top Employee entity. These two Employee objects become different in memory and Company in the second Employee object after deserializing, is null.

This situation leads to new child entities being created during unmarshalling, because the entity is never found in entityMap. As a result, the parent-child relationship is lost after XML serialization and deserialization. Figure 2 shows the entity objects in the debug view.

Figure 2. Memory snapshot of the entity data in debug view
Different memory objects created for same entity
Different memory objects created for same entity

In the situation shown in Figure 2, two different Company objects are created in memory, rather than correctly identifying the objects as the same. Therefore, when entities are persisted, JPA sends the following error message:

Encountered unmanaged object "entities.tr.library.Company@cfae6620" in life cycle state unmanaged while cascading persistence via field "entities.tr.library.Employee.Company" during flush. However, this field does not allow cascade persist. You cannot flush unmanaged objects or graphs that have persistent associations to unmanaged objects.
Suggested actions: a) Set the cascade attribute for this field to CascadeType.PERSIST or CascadeType.ALL (JPA annotations) or "persist" or "all" (JPA orm.xml), 
b) enable cascade-persist globally, 
c) manually persist the related field value prior to flushing. 
d) if the reference belongs to another context, allow reference to it by setting StoreContext.setAllowReferenceToSiblingContext().

Although this solution seems correct, it does not work well where parent-child relationships must be maintained across XML transcoding.

The solution is to link the parent-child relationship in the XML marshalling and in the unmarshalling, as is done in the following solution.

Use @XmlID-@XmlIDREF to resolve the error

Use @XmlID-@XmlIDREF annotation shown in Listing 7 to maintain the bidirectional relationship in the entities in XML. As with @XmlTransient annotation, this method has the following limitation: @XmlIDREF annotation stores only the reference of the entity and not the complete entity data to cut cycles. Therefore, the parent data is missing from the child entity because of @XmlIDREF annotation. To resolve this issue, use the wink container class to pass the missing data, by explicitly marshalling it separately.

Listing 7. @XmlID-@XmlIDREF implementation of the example entities

// Parent entity

public class Company implements Serializable {
    private static final long serialVersionUID = 0; 

    @Column(name = "ID", unique = true, nullable = false, updatable = false) 
    @Id
    protected Integer id; 

    @XmlAttribute
    @XmlID // should be unique across all entities.
    private String uuid; public String getUuid() { return uuid; } public void setUuid(UUID uuid) { this.uuid = uuid.toString(); } public Integer getId() { return id; } … … … @OneToMany(mappedBy = "company", cascade = CascadeType.ALL) Set<Employee> employees; … … … @OneToMany(mappedBy = "company", cascade = CascadeType.ALL) Set<Department> departments; … … … }

// Child1 entity

public class Department implements Serializable {
    private static final long serialVersionUID = 0; 

    @Column(name = "ID", unique = true, nullable = false, updatable = false) 
    @Id
    protected Integer id; 

    @XmlAttribute
    @XmlID // should be unique across all entities.
    private String uuid; public String getUuid() { return uuid; } public void setUuid(UUID uuid) { this.uuid = uuid.toString(); } public Integer getId() { return id; } … … … @OneToMany(mappedBy = "department", cascade = CascadeType.ALL) Set<Employee> employees; … … … @ManyToOne(fetch = FetchType.LAZY, optional = true) @JoinColumn(name = "COMPANY_ID", referencedColumnName = "ID") @XmlIDREF Company company; … … … }

// Child2 entity

public class Employee implements Serializable {
    private static final long serialVersionUID = 0; 
   
    @Column(name = "ID", unique = true, nullable = false) 
    @Id
    protected Integer id; 

    /** 
     * @return the id
     */
    public Integer getId() {
        return id; 
    }

    @XmlAttribute
    @XmlID // should be unique across all entities.
    private String uuid; public String getUuid() { return uuid; } public void setUuid(UUID uuid) { this.uuid = uuid.toString(); } @ManyToOne(fetch = FetchType.LAZY, optional = true) @JoinColumn(name = "DEPARTMENT_ID", referencedColumnName = "ID") @XmlIDREF Department department; … … … @ManyToOne(fetch = FetchType.LAZY, optional = true) @JoinColumn(name = "COMPANY_ID", referencedColumnName = "ID") @XmlIDREF Company company; … … … }

In the parent entity you need to define a unique ID and annotate it with @XmlID. Use the generated entity UUID for this purpose. This unique ID is used as a reference in the other entities by using the @XmlIDREF annotation.

When the Employee entity is sent from the server to the client for example, the Department and Company data is missing because only the reference, the UUID for Department and Company, gets serialized in the XML. The solution is to explicitly marshal the parent entities with the child entity to make the whole set of data available in the XML. For this purpose, define a container class to be used instead of the entity in the HTTP GET, PUT, and POST methods. The entity container class contains all of the member entities annotated with @XmlIDREF along with the entity, so that the complete data is available, as shown in Listing 8.

Listing 8. Container class implementation
/** 
 * @author IBM Corporation
 */

@XmlAccessorType(XmlAccessType.FIELD) 
@XmlRootElement(name = "EmployeeContainer")
public class EmployeeContainer {

    Employee employee; 
    
    // The @XmlIDREF entities in  Employee are not available as in XML transcoding 
    // only the reference gets saved and not the data.
    // So we save this data separately as below so that it gets passed from the
    // server to the client or vice versa.
    Department department;
    Company company; /** * @return the employee */ public Employee getEmployee() { return this.employee; } /** * @param employee - the Employee to be set */ public void setEmployee(Employee employee) { this.employee = employee; department = employee.getDepartment();
        company = employee.getCompany(); } public EmployeeContainer() { employee = null; department = null;
        company = null; } /** * @param employee - the Employee to be set */ public EmployeeContainer(Employee employee) { setEmployee(employee); } } /** * @author IBM Corporation */ @XmlAccessorType(XmlAccessType.FIELD) @XmlRootElement(name = "DepartmentContainer") public class DepartmentContainer { Department department; // The @XmlIDREF entities in Department are not available as in XML transcoding only
    // the reference gets saved and not the data.
    // So we save this data separately as below so that it gets passed from the
    // server to the client or vice versa.
    Company company; /** @param department * the Department to set */ public void setDepartment(Department department) { this.department = department;
        this.company = department.getCompany(); } public DepartmentContainer() { department = null; company = null; } /** * @param department */ public DepartmentContainer(Department department) { setDepartment(department); } /** * @return the department */ public Department getDepartment() { return this.department; } }

Company has no @XmlIDREF annotated member; therefore, no container class is required for transferring this data from server to client or from client to server.

This final solution resolves the JAXB cycle error in XML, maintains the parent-child relationship for JPA persistence, and makes the complete data available after XML transfer.

Conclusion

This article describes how bidirectional relationships in entities cause cycles in XML serialization. It covers a variety of annotations that enable JAXB to cut cycles for XML transcoding of these entities. It explains how wink container classes, used with @XmlID-@XmlIDREF JAXB annotations, make it possible to maintain parent-child relationships in XML serialization of entities with bidirectional relationships.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Rational
ArticleID=978330
ArticleTitle=Mapping JPA entities to successfully transcode them using XML
publish-date=08192014