Wednesday, October 3, 2018

How to perform ALL Cascade operations (PERSIST, UPDATE, REMOVE) to multilevel child in JPA

Leave a Comment

I have 3 tables (Table 1, Table 2, Table 3). Table 1 is related to Table 2 by @onetomany using primary key. Table 2 is related to Table 3 by @manytoone. Table 2 has EmbeddedId.

When I get the details using Table 1's primary key, I am able to fetch the data in Table 2 and Table 3. But I can't do save and delete. Save and delete is happening on the Table 1's child table (ie Table 2), but does not impact Table 3(which is child to Table 2)

Below are the entity models of all the three tables

 @Entity     @Table(name = "FEATUREMASTER")     @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})      public class FeatureMaster implements Serializable {         /**          *           */         private static final long serialVersionUID = 1L;          @Id          @Column(name = "FGID")         private String featureid;          @Column(name = "FEATURENAME", nullable = false, unique = false)         private String featurename;          @Column(name = "DESCRIPTION", nullable = true, unique = false)         private String description;          @Column(name = "LIBNAME", nullable = true, unique = false)         private String libname;          @Column(name = "ISENABLED", nullable = false, unique = false)         private String isenabled;          @Column(name = "EDRULEGRP", nullable = true, unique = false)         private String edrulegrp;          // Do Not use - [orphanRemoval = true & CascadeType.ALL]- If used, deletion is not happening          @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})         @JoinColumn(name = "FGID")         private List<CfgMaster> parameters;  // Getters and Setters }       @Entity     @Table(name = "CFGMASTER")     public class CfgMaster implements Serializable {          /**          *           */         private static final long serialVersionUID = 1L;          @EmbeddedId         private CfgMasterPK id;          @Column(name = "CONFIGNAME", length = 45, nullable = true, unique = false)         private String parameter_name;          @Column(name = "CONFIGTYPE", length = 20, nullable = true, unique = false)         private String type;          @Column(name = "SUBPARAM", nullable = true, unique = false)         private Integer subparam;          @Column(name = "CONFIGDESCRIPTION", nullable = true, unique = false)         private String description;          @Column(name = "CONFIGLIMITFROM", nullable = true, unique = false)         private String from;          @Column(name = "CONFIGLIMITTO", nullable = true, unique = false)         private String to;          @ManyToOne(cascade = {CascadeType.ALL}, optional = true, fetch = FetchType.LAZY )     //  @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)         @NotFound(action=NotFoundAction.IGNORE) // This is required to handle when no CfgData is found         @JoinColumns({                 @JoinColumn(name = "FGID", insertable = false, updatable = false),                 @JoinColumn(name = "DATAKEY", insertable = false, updatable = false)                  })         private CfgData cfgData;  //Getters and Setters     }  @Entity @Table(name = "CFGDATA") public class CfgData implements Serializable {      /**      *       */     private static final long serialVersionUID = 1L;       /*@EmbeddedId     private CfgDataPK id;*/      @Id     @Column(name = "FGID")     private String fgid;      @Id     @Column(name = "DATAKEY")     private String datakey;      @Column(name = "EPID", nullable = false, unique = false)     private int epid;       @Column(name = "RESERVED1", length = 45, nullable = true, unique = false)     private String reserved1;      @Column(name = "VALUE1", length = 100, nullable = true, unique = false)     private String value1;      @Column(name = "VALUE2", length = 100, nullable = true, unique = false)     private String value2;      //Getters and Setters } 

The problem I am facing is, I am not able to delete/save the entities of CfgData by passing FeatureMaster's primary id. Any operation I do is affecting only parent &child, not the grand child (CfgData) I tried a lot googling, but I cant find the solution.

2 Answers

Answers 1

(There's an assumption here that the PK for CfgMaster is FGID - hope this right. If so ... I think I can explain what's happening, though solving it with the current table mappings is tricky)

It looks like the problem relates to the presence or otherwise of the insertable = false, updatable = false on the foreign keys.

The usual reason for using these if there are two properties on the same entity that map to the same column(s). Hibernate needs to know which of the properties to set the column value from, so at most one of the properties can be writeable.

It looks like this is the problem here, but twice:


Firstly on FeatureMaster, the parameters collection uses a foreign key join column of FGID. Because this is a @OneToMany this is actually a column on CFGMASTER, which is (assumed to be) already mapped by the id property, this second mapping needs to be read-only.

With this change, a cascade delete from FeatureMaster to CfgMaster started working:

@OneToMany(cascade = { CascadeType.ALL }, orphanRemoval = true) @JoinColumn(name = "FGID", insertable = false, updatable = false) private List<CfgMaster> parameters = new ArrayList<>(); 

Secondly on CfgMaster, the cfgData property is using read-only @JoinColumns. I would think the reason for this is the overlap on the (assumed) FGID column?

Unfortunately, because these are foreign key columns on CFGMASTER, this effectively makes the CfgMaster.cfgData property read-only too. E.g. switching to non-overlapping, writeable columns enabled the cascade delete here too:

@ManyToOne(cascade = {         CascadeType.ALL }, optional = true, fetch = FetchType.LAZY) //  @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY) @NotFound(action = NotFoundAction.IGNORE) // This is required to handle when no CfgData is found @JoinColumns({         @JoinColumn(name = "FGID2"),         @JoinColumn(name = "DATAKEY") }) private CfgData cfgData; 

This even worked for the grandchild cascade delete.

However, clearly this raises the next question - is there any way to have the PK of CfgData also be part of a two-part foreign key? I have seen examples where the foreign key was the primary key, but not previously with an extra column. Clearly you have managed it here, but the side-effect is that the relationship is read-only too, at least for cascades.

Although it's not what you'll be hoping to hear, this does make some sense from Hibernate's perspective. E.g. if the @ManyToOne property were to be null'ed, Hibernate would want to blank both columns, which is a problem for the primary key. Unless someone else knows better, I think the choice is to change the DB mappings, or if that's not an option, you'll need to code the cascade delete of the CfgData's.

Answers 2

Problem Description

the issue is rooted from where the deleted instance is re-persisted if a PERSIST operation is applied to it. A common mistake is to ignore that a PERSIST operation has been applied to a removed instance (usually, because it is cascaded from another instance at the flush time), because the section 3.2.2 of the JPA specification mandates that such instance is to be persisted again in such a case.

In your example, this phenomenon can be seen where you defined a @ManyToOne association from CfgMaster to CfgData. In this case, if the removed CfgData is referenced by a CfgMaster, the PERSIST operation is cascaded from CfgMaster to CfgData because the association is marked with cascade = CascadeType.ALL and the deletion will be unscheduled.

  • To verify that this is happening, you can enable trace log level for the org.hibernate package and search for entries such as un-scheduling entity deletion.

Alternative workaround

CascadeType.ALL should not be used on @ManyToOne as the state transitions of entities should be propagated from Parent entities (the owner side) to Child ones, so as @ManyToOne is always the Child association, cascading on it should be avoided.

You should set CascadeType.ALL from the @ManyToOne association to its corresponding @OneToMany which mappedBy is also already set on it.


More Info:

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment