Using @XmlID and @XmlIDREF in JAXB for self referencing relationships.


Update

It seems this entry is keep viewed. I must admit that using @XmlID and @XmlIDREF is not the best nor the only way to go.
The problem or concern is that we should be aware of the actual meaning of those annotations and their purposes in documents.
When we annotate any element with the @XmlID, that means those elements are unique in the XML instance document.
So, I’m intending to introduce an another way to work around the problem.

@Entity
@XmlRootElement
public class Parent {

    @Id
    @XmlAttribute
    private Long id;

    @OneToMany(mappedBy = "parent")
    @XmlElement(name = "child")
    private List<Child> children;
}

@Entity
@XmlRootElement
public class Child {

    @Id
    @XmlAttribute
    private Long id;

    @ManyToOne
    @XmlElement
    private Parent parent;
}

I’m not sure this problem is already handled by any JAXB implementations or not but the point is that the external(presentation) serialization rule (JAXB) doesn’t need to follow the internal(backend) serialization rule (JPA) especially when it comes to JAX-RS.

@Entity
@XmlRootElement
public class Parent {

    @Id
    @XmlAttribute
    private Long id;

    @OneToMany(mappedBy = "parent")
    @XmlTransient //@XmlElement(name = "child")
    private List<Child> children;
}

@Entity
@XmlRootElement
public class Child {

    @Id
    @XmlAttribute
    private Long id;

    @ManyToOne
    @XmlTransient //@XmlElement
    private Parent parent;
}

As you can see I just cut off JAXB annotations from both sides. The information about childrens and their parents can be easily retrieved JAX-RS facilities.

@Path("/parents/{parent_id: \\d+}")
public Parent read(@PathParam("parent_id") final long parentId) {

    // SELECT p FROM Parent AS p WHERE p.id=parentId
}
@Path("/parents/{parent_id: \\d+}/children")
public List<Child> read(@PathParam("parent_id") final long parentId,
                        @QueryParam("first_result") @DefaultValue("0") final int firstResult,
                        @QueryParam("max_results") @DefaultValue("128") final int maxResults)

    // SELECT c FROM Child AS c WHERE c.parent.id=parentId
@Path("/parents/{parent_id: \\d+}/children/{child_id: \\d+}")
public Child read(@PathParam("parent_id") final long parentId,
                  @PathParam("child_id") final long childId) {

    // SELECT c FROM Child AS c WHERE c.parent.id=parentId AND c.id=childId
}

References

When you have to use JPA’s annotations and JAXB’s annotations in the same class there is one huge problem with @Id and @XmlID with their type mismatch.

I just want my codes talk to JAXB-RI and JAXB-RI only. (not to EclipseLink MOXy)

And I found myself that ‘class defined’ callback methods such as beforeMarshal or afterUnmarshal don’t work.
It seems that some ‘xml id checking‘ codes execute before the beforeMarshal method which contains something like

private void beforeMarshal(final Marshaller marshaller) {
    employeeId = Long.toString(id);
}

Here comes modified version of Blaise Doughan‘s Company.java and Employee.java.

Some codes are removed.
Full sources are here.

@Entity
@XmlRootElement
@XmlAccessorType(XmlAccessType.NONE) // only annotated properties/fields come to play
public class Employee {

    @XmlAttribute
    @XmlID
    public String getEmployeeId() {
        return Long.toString(id);
    }

    public void setEmployeeId(final String employeeId) {
        id = Long.parseLong(employeeId);
    }

    // database's primary key
    // we can't annotate this field with @XmlID because it's not String
    @Id
    //@XmlTransient // transient by XmlAccessType.NONE
    private Long id;

    @ManyToOne
    @XmlIDREF
    private Employee manager;

    @OneToMany(mappedBy = "manager")
    @XmlElement(name = "subordinate")
    @XmlIDREF
    private Collection<Employee> subordinates;
}
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Company {

    public static void main(final String[] args) throws Exception {

        final JAXBContext context = JAXBContext.newInstance(Company.class);

        final Company company = new Company();

        final Employee employee1 = new Employee();
        employee1.setId(1L);
        employee1.setName("Jane Doe");
        company.getEmployees().add(employee1);

        final Employee employee2 = new Employee();
        employee2.setId(2L);
        employee2.setName("John Smith");
        employee2.setManager(employee1);
        employee1.getSubordinates().add(employee2);
        company.getEmployees().add(employee2);

        final Employee employee3 = new Employee();
        employee3.setId(3L);
        employee3.setName("Anne Jones");
        employee3.setManager(employee1);
        employee1.getSubordinates().add(employee3);
        company.getEmployees().add(employee3);

        final StringWriter writer = new StringWriter();
        final Marshaller marshaller = context.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        marshaller.marshal(company, writer);
        writer.flush();

        final String xml = writer.toString();
        System.out.println(xml);

        final StringReader reader = new StringReader(xml);
        final Company after =
            (Company) context.createUnmarshaller().unmarshal(reader);
    }

    @XmlElement(name = "employee")
    private Collection<Employee> employees;
}
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<company>
    <employee employeeId="1">
        <name>Jane Doe</name>
        <subordinate>2</subordinate>
        <subordinate>3</subordinate>
    </employee>
    <employee employeeId="2">
        <name>John Smith</name>
        <manager>1</manager>
    </employee>
    <employee employeeId="3">
        <name>Anne Jones</name>
        <manager>1</manager>
    </employee>
</company>
Advertisements

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s