Search code examples
javajsonjpaeclipselinkone-to-many

JPA OneToMany Relationship: Foreign Key Not Persisted in Child Table


I'm encountering an issue when persisting a parent JPA entity that has a OneToMany relationship with its child entities. When saving the parent entity using EclipseLink, both the parent and child entities are saved to their respective tables, but the foreign key in the child table that links back to the parent is left null, breaking the relationship in the database.

To be specific, the problem is with the application_address table. I can't get the configuration right between TblApplicationAdress and TblApplicationMembership so that JPA saves the application_membership_id foreign key link into the application_address table. (For context, an application contains one membership which contains many addresses.)

Below are the details of my setup:

  • Database: PostgreSQL 14
  • Java Version: Java 11
  • Annotations: Using Lombok's @Getter and @Setter
  • Persistence: EclipseLink with JPA

Tables:

create table application
(
    application_id                bigserial primary key,
    application_membership_id     bigint references application_membership,
    application_reference_number  varchar,
    -- other fields...
);

create table application_membership
(
    application_membership_id   bigserial primary key,
    membership_reference_number varchar,
    -- other fields...
);

create table application_address
(
    application_address_id    bigserial primary key,
    application_membership_id bigint references application_membership,
    address_type_code         varchar,
    address_line1_number      varchar,
    -- other fields...
);

JPA Entities:

@Getter
@Setter
@Entity
@Table(name = "application", schema = "obd")
public class TblApplication {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "application_id")
    private Long applicationId;

    @Column(name = "application_reference_number")
    private String applicationReferenceNumber;

    // Other fields...

    @ManyToOne
    @JoinColumn(name = "application_membership_id", referencedColumnName = "application_membership_id")
    private TblApplicationMembership applicationMembershipByApplicationMembershipId;
}

@Getter
@Setter
@Entity
@Table(name = "application_membership", schema = "obd")
public class TblApplicationMembership {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "application_membership_id")
    private Long applicationMembershipId;

    @Column(name = "membership_reference_number")
    private String membershipReferenceNumber;

    // Other fields...

    @OneToMany(mappedBy = "applicationMembershipByApplicationMembershipId", cascade = CascadeType.PERSIST)
    @JsonManagedReference
    private Collection<TblApplicationAddress> applicationAddressesByApplicationMembershipId;
}

@Getter
@Setter
@Entity
@Table(name = "application_address", schema = "obd")
public class TblApplicationAddress {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "application_address_id")
    private Long applicationAddressId;

    @Column(name = "address_type_code")
    private String addressTypeCode;

    @Column(name = "address_line1_number")
    private String addressLine1Number;

    // Other fields...

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "application_membership_id", referencedColumnName = "application_membership_id")
    @JsonBackReference
    private TblApplicationMembership applicationMembershipByApplicationMembershipId;
}

Service Layer:

tblApplication = applicationDataService.save(tblApplication);

Data Service:

public T save(T t) throws DataServiceException {
    try {
        t = em.merge(t);
        em.flush();
        em.clear();
        return t;
    } catch (Exception ex) {
        throw new DataServiceException("Error BaseDataService.save...", ex);
    }
}

I’ve tried using @JsonBackReference, @JsonManagedReference, and cascade = CascadeType.PERSIST, as suggested in these discussions:

Unfortunately, these approaches haven't resolved the issue. If I can't get this working, I may have to manually save the parent entity first, then assign the foreign key ID to the child entity before saving it, but I'd prefer to avoid this workaround due to the complexity and size of the object structure.

Question: What could be causing the foreign key in the child table to remain null when persisting these entities? Is there a configuration or annotation I might be missing, or is there a better way to ensure the foreign key is correctly set when saving the parent entity?


Solution

  • I could not find the resolution I wanted, so resorted to a more manual approach.

    Firstly, it was extremely important to set the cascade to CascadeType.ALL on the parent JPA classes.

    Secondly, the @JsonManagedReference and @JsonBackReference made no difference, so I removed the annotations.

    Here are the updated JPA entities:

    @Getter
    @Setter
    @Entity
    @Table(name = "application", schema = "obd")
    public class TblApplication {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "application_id")
        private Long applicationId;
    
        @Column(name = "application_reference_number")
        private String applicationReferenceNumber;
    
        // Other fields...
    
        @ManyToOne
        @JoinColumn(name = "application_membership_id", referencedColumnName = "application_membership_id", cascade = CascadeType.ALL)
        private TblApplicationMembership applicationMembershipByApplicationMembershipId;
    }
    
    @Getter
    @Setter
    @Entity
    @Table(name = "application_membership", schema = "obd")
    public class TblApplicationMembership {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "application_membership_id")
        private Long applicationMembershipId;
    
        @Column(name = "membership_reference_number")
        private String membershipReferenceNumber;
    
        // Other fields...
    
        @OneToMany(mappedBy = "applicationMembershipByApplicationMembershipId", cascade = CascadeType.ALL)
        private Collection<TblApplicationAddress> applicationAddressesByApplicationMembershipId;
    }
    
    @Getter
    @Setter
    @Entity
    @Table(name = "application_address", schema = "obd")
    public class TblApplicationAddress {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "application_address_id")
        private Long applicationAddressId;
    
        @Column(name = "address_type_code")
        private String addressTypeCode;
    
        @Column(name = "address_line1_number")
        private String addressLine1Number;
    
        // Other fields...
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "application_membership_id", referencedColumnName = "application_membership_id")
        private TblApplicationMembership applicationMembershipByApplicationMembershipId;
    }
    

    And here is the manual code in the service layer I was trying to avoid

    tblApplication = applicationDataService.save(tblApplication);
    Collection<TblApplicationAddress> addresses = tblApplicationMembership.getApplicationAddressesByApplicationMembershipId();
    for (TblApplicationAddress entity : addresses) {
        entity.setApplicationMembershipByApplicationMembershipId(tblApplicationMembership);
    }
    tblApplication = applicationDataService.save(tblApplication);
    

    I am certain there is a way to configure the JPA classes to avoid this manual workaround, but if anyone finds themselves under time pressure, and needing a solution, this will do it.