Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

No cascade insert for child with foreign key on parent

I have checked many posts on the internet and none address my problem. Really appreciate if anyone can help! I have OneToMany parent child relationship. Hibernate does not cascade insert child (Level2) when one of the child composite keys is the foreign key to the parent (Level1). I specified cascade="all" and no matter the inverse is true or false, the results are the same. There is no exception, and it simply does not insert. Below print out shows Level1 was inserted successfully but Level2 was only selected, no insert.

Hibernate: insert into sst.level_1 (STATUS, NAME) values (?, ?)
Hibernate: select level2x_.LEVEL_1_ID, level2x_.TIMESEGMENT, level2x_.CATEGORY as CATEGORY4_ from sst.level_2 level2x_ where level2x_.LEVEL_1_ID=? and level2x_.TIMESEGMENT=?

The APIs are Hibernate 4.1.6/Spring 3.1.2.

Here is the table definition for mySQL:

CREATE TABLE level_1
(
ID int NOT NULL PRIMARY KEY AUTO_INCREMENT,
STATUS int,
NAME varchar(255)
);

CREATE TABLE level_2
(
LEVEL_1_ID int,
TIMESEGMENT int,
CATEGORY varchar(2),
PRIMARY KEY (LEVEL_1_ID, TIMESEGMENT),
FOREIGN KEY (LEVEL_1_ID) REFERENCES level_1(ID) ON DELETE CASCADE
);

Here is the test code.

public class Test {

   public static void main(String[] args) {
      ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
      doInsert(context);
   }

   private static void doInsert(ApplicationContext context) {

      Level1 level1 = new Level1();
      Level2 level2 = new Level2();

      level1.setName("LEVEL 1 NAME");
      level1.setStatus(1);
      level1.getLevel2s().add(level2);

      level2.setLevel1(level1);
      level2.setCategory("CA");
      level2.setId(new Level2Id(level1.getId(), 10));

      Level1DAO level1DAO = (Level1DAO) context.getBean("Level1DAO");
      level1DAO.save(level1);
   }
}

Mapping for Level1:

<hibernate-mapping>
    <class name="com.jc.hibernate.Level1" table="level_1" catalog="sst">
        <id name="id" type="java.lang.Integer">
            <column name="ID" />
            <generator class="identity" />
        </id>
        <property name="status" type="java.lang.Integer">
            <column name="STATUS" />
        </property>
        <property name="name" type="java.lang.String">
            <column name="NAME" />
        </property>
        <set name="level2s" inverse="true" cascade="all">
            <key>
                <column name="LEVEL_1_ID" not-null="true" />
            </key>
            <one-to-many class="com.jc.hibernate.Level2" />
        </set>
    </class>
</hibernate-mapping>

Mapping for Level2:

<hibernate-mapping>
    <class name="com.jc.hibernate.Level2" table="level_2" catalog="sst">
        <composite-id name="id" class="com.jc.hibernate.Level2Id">
            <key-property name="level1Id" type="java.lang.Integer">
                <column name="LEVEL_1_ID" />
            </key-property>
            <key-property name="timesegment" type="java.lang.Integer">
                <column name="TIMESEGMENT" />
            </key-property>
        </composite-id>
        <many-to-one name="level1" class="com.jc.hibernate.Level1" update="false" insert="false" fetch="select">
            <column name="LEVEL_1_ID" not-null="true" />
        </many-to-one>
        <property name="category" type="java.lang.String">
            <column name="CATEGORY" length="2" />
        </property>
    </class>
</hibernate-mapping>
like image 723
John Avatar asked Sep 10 '12 20:09

John


1 Answers

Your mapping seems to be correct.

Your problem looks like is the line

level2.setId(new Level2Id(level1.getId(), 10));

When this line is executed, the level1 still does not have its id assigned to it, so it will be null. As level_1_id and segment are composite primary key, none of it can be null. So you would need to do first

Integer generatedId = (Integer) Session.save(level1)

This does an immediate insert in database and returns the identifier. Then you can use the identifier to create Lever2Id

level2.setId(new Level2Id(generatedId  , 10));

I tried this on MySql with the mappings you provided and its working fine.

If I don't first save the level1 first, it gives me error

Column 'LEVEL_1_ID' cannot be null

UPDATE

Unfortunately it looks like the cascade does not work automatically when there are composite keys in child and one of the column in composite key refers to the primary key of parent. So you have to set the id's and relations manually as shown above.

Note that if the Child has a single column id and your relation is one to one then Hibernate can figure out that the child primary column comes from Parent and can set automatically. See here and here

The docs too suggest this here

“You cannot use an IdentifierGenerator to generate composite keys. Instead the application must assign its own identifiers.”

The hibernate class representing composite id mapping is org.hibernate.mapping.Component ( which is used for component, composite element, composite identifier, etc) and the default id generation strategy is “assigned” unless you have provided a custom id generator which takes you to a hackish way by creating your own CompositeUserType ( for composite Id) and use a custom Identifier generator as mentioned here. But this looks ugly and an overkill.

Regarding saving both parent and child and setting relation manually in a transaction First remove the cascade all option from your Parent set element or set it to "none", so that there is no transitive persistence by Hibernate. Saving parent will only save parent and not child.

Then prepare the Parent and Child with all the properties you have available and set the object relation from both sides like this ( to ensure model consistency).

 Parent parent = new Parent();
    parent.setStatus( 1  );
    parent.setName( "Parent" );  
    ChildId childId = new ChildId();    
    Child child = new Child(childId);    
    child.setCategory( "TH" );    
    parent.addChild( child );
    child.getId().setTimesegment( 10 );

Parent's addChild method adds child to the collection as well as set's the child parent in one convenient method

 public void addChild(Child child){
    if (child !=null){
      getChildrens().add( child );
      child.setParent( this );
    }

  }

Then to have a method in DAO which takes Child as parameter and also is transactional, something like this ( although the better way is to make the service layer transactional instead of DAO layer)

public void save(Child child){

     //get Session 
    Session session =.......
    Transaction tx = session.beginTransaction();
    Integer generatedId = (Integer)session.save( child.getParent() );
    ChildId childId = child.getId();
    childId.setChildId( generatedId );     
    session1.save( child );   
    tx1.commit();
    HibernateUtil.closeSession();
}

As of now this is what I can suggest.

like image 199
Shailendra Avatar answered Nov 01 '22 01:11

Shailendra