JPA - JpaRepository child record has parent id instead of entity record if parent already fetched

Viewed 1323

I'm still grasping JPA concepts and can't seem to find the answer to my question anywhere!

Assume

Both classes are annotated with @GeneratedValue(strategy = GenerationType.IDENTITY), There are all getters and setters.

Parent{
    ....
    @OneToMany(mappedBy = "parent")
    Collection<Child> children;
    ....
}

Child{
    ...
    @JoinColumn(name = "parent", referencedColumnName = "id", nullable = true)
    @ManyToOne(optional = false)
    Parent parent;
    ...
}

I then implemented the standard JpaRepository and setup my controller

Here is the problem
When I query all child records, only the first child record that was mapped to a particular parent will have the parent entity object in it. the rest will have an id referencing the parent enity.

Here is an example: Getting all children from POSTMAN returns:

[
    {
        "id": 1,
        "name": "child1",
        "parent": {
            "id": 1,
            "firstName": "..."
            ...
            }
    },
    {
        "id": 2,
        "name": "child2",
        "parent": 1
    }
    {
        "id": 3,
        "name": "child3",
        "parent": {
            "id": 2,
            "firstName": "..."
            ...
            }
    },
    {
        "id": 4,
        "name": "child4",
        "parent": 2
    }
]

As you can see child2 only has "parent": 1 since child1 mapped to that parent first! Similarly child4 only has "parent": 2 since child3 mapped to that parent first!

Can anyone explain this behaviour please? I tried fetch = FetchType.EAGER on parent but it did not help! I expect all children to have a comprehensive parent object to prevent another DB trip.

Thanks in advance!

UPDATING THE QUESTION WITH THE ACTUAL CLASSES:


PARENT

package backend.application.payroll.models;

import com.fasterxml.jackson.annotation.*;
import lombok.Data;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import javax.persistence.*;

@Data
@Entity
@Table(name = "employees")
@JsonIdentityInfo(
        generator = ObjectIdGenerators.PropertyGenerator.class,
        property = "id")
public class Employee implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @Column(name = "emp_code", nullable = false)
    private String empCode;
    @Column(name = "first_name", nullable = false)
    private String firstName;
    @Column(name = "middle_name", nullable = true)
    private String middleName;
    @Column(name = "last_name", nullable = false)
    private String lastName;
    @Column(name = "dob", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date dob;
    @Column(name = "id_number", nullable = true)
    private String idNumber;
    @Column(name = "passport_number", nullable = true)
    private String passportNumber;
    @Column(name = "email_address", nullable = true)
    private String emailAddress;
    @OneToOne(cascade = CascadeType.DETACH)
    @JoinColumn(name = "pay_grade", referencedColumnName = "id", nullable = true)
    private Salary payGrade;
    @Column(name = "basic_pay", nullable = true)
    private BigDecimal basicPay;
    @OneToOne(cascade = CascadeType.DETACH)
    @JoinColumn(name = "department", referencedColumnName = "id", nullable = true)
    private Department department;
    @OneToOne(cascade = CascadeType.DETACH)
    @JoinColumn(name = "position", referencedColumnName = "id", nullable = true)
    private Position position;
    @Column(name = "tax_number", nullable = true)
    private String taxNumber;
    @Column(name = "hire_date", nullable = true)
    @Temporal(TemporalType.DATE)
    private Date hireDate;
    @Column(name = "address1", nullable = true)
    private String address1;
    @Column(name = "address2", nullable = true)
    private String address2;
    @Column(name = "postal_code", nullable = true)
    private String postalCode;
    //country
    @Column(name = "phone_number", nullable = true)
    private String phoneNumber;
    //banking details

    //HERE IT WORKS FINE SINCE IT'S ONETOONE - YOU CAN IGNORE
    @OneToOne(mappedBy = "employee")
    //@JsonManagedReference//used in conjunction with @JsonBackReference on the other end - works like @JsonIdentityInfo class annotation.
    private User user;

    //THIS IS WHAT CAUSING THE PROBLEM
    @OneToMany(mappedBy = "owner", fetch = FetchType.LAZY)
    //@JsonBackReference
    @JsonIgnore
    private Set<Costcentre> costcentres = new HashSet<>();

    public Employee() {

    }
}

CHILD

package backend.application.payroll.models;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonManagedReference;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import lombok.Data;

import javax.persistence.*;
import java.io.Serializable;


@Entity
@Table(name = "costcentres")
@Data
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Costcentre implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    @Column(name = "name", nullable = false)
    private String name;
    @Column(name = "description", nullable = true)
    private String description;
    @ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.EAGER)
    @JoinColumn(name = "owner", referencedColumnName = "id", nullable = true)
    //@JsonManagedReference
    private Employee owner; //CULPRIT

    public Costcentre() {

    }
    public Costcentre(long id, String name, String description) {
        super();
        this.id = id;
        this.name = name;
        this.description = description;
    }
}
1 Answers

Add JsonIdentityInfo to the parent and child and you can add fetch = FetchType.EAGER on the parent and JsonIgnore to ignore get cyclic child and parents

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")

, Like this:

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
Parent{
    ....
    @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
    @JsonIgnore
    Collection<Child> children;
    ....
}

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
Child{
    ...
    @JoinColumn(name = "parent", referencedColumnName = "id", nullable = true)
    @ManyToOne(optional = false, fetch = FetchType.EAGER)
    Parent parent;
    ...
}
Related