History | Log In     View a printable version of the current page.  
Issue Details (XML | Word | Printable)

Key: HHH-1401
Type: Bug Bug
Status: Resolved Resolved
Resolution: Fixed
Priority: Major Major
Assignee: Steve Ebersole
Reporter: David Trott
Votes: 6
Watchers: 5
Operations

If you were logged in you would be able to see more operations.
Hibernate Core

session.merge() executes unnecessary updates when one-to-many relationship is defined.

Created: 27/Jan/06 12:49 AM   Updated: 06/Nov/06 04:18 PM
Component/s: core
Affects Version/s: 3.1.1
Fix Version/s: 3.2.1

Time Tracking:
Not Specified

File Attachments: 1. Zip Archive HHH-1401.zip (6 kb)
2. Java Source File HHH1401Test.java (2 kb)
3. Text File TEST-org.hibernate.test.optlock.OptimisticLockTest.txt (109 kb)

Image Attachments:

1. Screenshot-Debug - AbstractPersistentCollection.class - Eclipse SDK .png
(262 kb)
Environment:
Hibernate 3.1.1
Postgres 8.03
Java 1.4.2_09
Issue Links:
Prerequisite
 
Relates


 Description  « Hide
I am attempting to use the session.merge() functionality in order to synchronize the state of the data coming from the web tier with the database, however I am seeing unnecessary updates (when nothing has changed).

In order to track down the problem I created a test case with four tables and four classes (A,B,C and D)

Where:
A is the parent of B.
B is the parent of C.
C is the parent of D.

And there are no other relationships present.

All these relationships are bi-directional with the one-to-many side marked as inverse="true" and cascade="all-delete-orphan".
The merge() is working fine (the data gets updated correctly) except that when there is no change to the data hibernate still runs updates on A,B and C however not on D (D has no one-to-many relationships).


I am including the code and hibernate mapping for B as it is representative of the code for all the other classes.


I am no expect on the hibernate implementation, but my suspicion of the cause is when hibernate substitutes a PersistentBag for the ArrayList in the merged object (associated with the session) it detects this as a change and hence triggers the update, unfortunately the data itself has not changed hence no update is necessary.

FYI: If I change the initialization of the bags from "new ArrayList()" to "new PersistentBag()" the extra updates go away, however then it doesn't save real changes correctly.







package com.mycompany.dal.transfer.impl;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.lang.builder.ToStringBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.mycompany.dal.transfer.interfaces.ADTO;
import com.mycompany.dal.transfer.interfaces.BDTO;
import com.mycompany.dal.transfer.interfaces.CDTO;
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.CollectionUtils;

public class BDTOImpl implements BDTO {

    public BDTOImpl () {
    }



    private Long bId;
   
    public Long getBId() {
        return bId;
    }

    public void setBId(Long bId) {
        this.bId = bId;
    }

    private Long concurrentVersion;
   
    public Long getConcurrentVersion() {
        return concurrentVersion;
    }

    public void setConcurrentVersion(Long concurrentVersion) {
        this.concurrentVersion = concurrentVersion;
    }

    // Package level protection so that overrides can access it.
    boolean deleting = false;



    private String name;

    /**
     * Returns the Name.
     *
     * @return String - The Name
     */
    public String getName() {
        return name;
    }

    /**
     * Set the Name.
     *
     * @param name String - The Name.
     */
    public void setName(String name) {
        this.name = name;
    }



    private ADTO a;

    /**
     * Returns the A.
     *
     * @return ADTO - The A.
     */
    public ADTO getA() {
        return a;
    }
   
    public ADTO getAInternal() {
        return a;
    }

    /**
     * Updates the A.
     *
     * @param a - ADTO The A.
     */
    public void setA(ADTO a) {
        if (this.a == a) {
            return;
        }

        if (this.a != null) {
            ((ADTOImpl) this.a).removeBInternal(this);
        }

        this.a = a;

        if (a != null) {
            ((ADTOImpl) a).addBInternal(this);
        }
    }

    public void setAInternal(ADTO a) {
        if (deleting) {
            return;
        }
        if (this.a != a &&
            this.a != null && a != null) {
            throw new IllegalStateException("BDTO cannot be a member of two A collections: " + toString());
        }

        this.a = a;
    }



    private List cs;
    private List csMutable;
    { setCsMutable(new ArrayList()); }

    public List getCsMutable() {
        return csMutable;
    }

    public void setCsMutable(List cs) {
        this.cs = Collections.unmodifiableList(cs);
        this.csMutable = cs;
    }

    public List getCs() {
        return cs;
    }
   
    public void addC(CDTO c) {
        csMutable.add(c);
        ((CDTOImpl) c).setBInternal(this);
    }

    public void addCInternal(CDTO c) {
        csMutable.add(c);
    }
   
    public void removeC(CDTO c) {
        csMutable.remove(c);
        ((CDTOImpl) c).setBInternal(null);
    }

    public void removeCInternal(CDTO c) {
        if (!deleting) {
            csMutable.remove(c);
        }
    }

    public void beforeDelete() {
        // Guard to prevent infinite loop.
        if (deleting) {
            return;
        }
       
        deleting = true;

        if (this.a != null) {
            ((ADTOImpl) this.a).removeBInternal(this);
        }
        CollectionUtils.forAllDo(new ArrayList(csMutable), new Closure() {
            public void execute(Object ob) {
                ((CDTOImpl) ob).beforeDelete();
            }
        });
    }

    public int hashCode() {
        return (new HashCodeBuilder(17,37)
            .append(getBId())
            ).toHashCode();
    }

    public boolean equals(Object o) {
        boolean equals = false;

        if (o != null && o instanceof BDTO) {
            BDTO other = (BDTO) o;

            return (new EqualsBuilder()
                .append(getBId(), other.getBId())
                ).isEquals();
        }

        return equals;
    }
   
    public String toString() {
        return new ToStringBuilder(this)
            .append("bId", getBId())
            .append("name", getName())
            .toString();
    }
}




******************************
*** Mapping Document ****
******************************

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
                                   "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="com.mycompany.dal.transfer.impl" auto-import="true">
    <class name="com.mycompany.dal.transfer.impl.BDTOImpl" table="b">
        <id name="BId" type="long">
            <column name="b_id" not-null="true"/>
            <generator class="native"/>
        </id>

        <version name="concurrentVersion" column="concurrent_version" type="long"/>

        <property name="Name" type="string">
            <column name="name" length="60" not-null="false"/>
        </property>

        <many-to-one name="AInternal" class="com.mycompany.dal.transfer.impl.ADTOImpl">
            <column name="a_id" not-null="true"/>
        </many-to-one>

        <bag name="CsMutable" cascade="all-delete-orphan" inverse="true">
            <key>
                <column name="b_id" not-null="true"/>
            </key>
            <one-to-many class="com.mycompany.dal.transfer.impl.CDTOImpl"/>
        </bag>

    </class>
</hibernate-mapping>


*************************
*** Generated SQL ****
*************************



05:35:39,887 INFO [STDOUT] Hibernate: select adtoimpl0_.a_id as a1_162_2_, adtoimpl0_.concurrent_version as concurrent2_162_2_, adtoimpl0_.name as name162_2_, bsmutable1_.a_id as a4_4_, bsmutable1_.b_id as b1_4_, bsmutable1_.b_id as b1_164_0_, bsmutable1_.concurrent_version as concurrent2_164_0_, bsmutable1_.name as name164_0_, bsmutable1_.a_id as a4_164_0_, csmutable2_.b_id as b4_5_, csmutable2_.c_id as c1_5_, csmutable2_.c_id as c1_165_1_, csmutable2_.concurrent_version as concurrent2_165_1_, csmutable2_.name as name165_1_, csmutable2_.b_id as b4_165_1_ from a adtoimpl0_ left outer join b bsmutable1_ on adtoimpl0_.a_id=bsmutable1_.a_id left outer join c csmutable2_ on bsmutable1_.b_id=csmutable2_.b_id where adtoimpl0_.a_id=?
05:35:39,992 INFO [STDOUT] Hibernate: select ddtoimpl0_.d_id as d1_168_0_, ddtoimpl0_.concurrent_version as concurrent2_168_0_, ddtoimpl0_.name as name168_0_, ddtoimpl0_.c_id as c4_168_0_ from d ddtoimpl0_ where ddtoimpl0_.d_id=?
05:35:40,007 INFO [STDOUT] Hibernate: select dsmutable0_.c_id as c4_1_, dsmutable0_.d_id as d1_1_, dsmutable0_.d_id as d1_168_0_, dsmutable0_.concurrent_version as concurrent2_168_0_, dsmutable0_.name as name168_0_, dsmutable0_.c_id as c4_168_0_ from d dsmutable0_ where dsmutable0_.c_id=?

*** Start Extra Updates **
05:35:40,030 INFO [STDOUT] Hibernate: update b set concurrent_version=?, name=?, a_id=? where b_id=? and concurrent_version=?
05:35:40,038 INFO [STDOUT] Hibernate: update c set concurrent_version=?, name=?, b_id=? where c_id=? and concurrent_version=?
05:35:40,044 INFO [STDOUT] Hibernate: update a set concurrent_version=?, name=? where a_id=? and concurrent_version=?
*** End Extra Updates **



**************************
*** Accessing code ****
**************************




            DataAccessLayer dal = DataAccessLayerBuilder.getInstance();
           
            ADAO aDAO = dal.getADAO();
            BDAO bDAO = dal.getBDAO();
            CDAO cDAO = dal.getCDAO();
            DDAO dDAO = dal.getDDAO();
           
            ADTO a = aDAO.newA();
            a.setAId(new Long(1));
            a.setConcurrentVersion(new Long(0));
            a.setName("A");
                       
            BDTO b = bDAO.newB();
            CDTO c = cDAO.newC();
            DDTO d = dDAO.newD();

            b.setBId(new Long(2));
            c.setCId(new Long(3));
            d.setDId(new Long(4));

            b.setConcurrentVersion(new Long(0));
            c.setConcurrentVersion(new Long(0));
            d.setConcurrentVersion(new Long(0));

            b.setName("B");
            c.setName("C");
            d.setName("D");

            b.setA(a);
            c.setB(b);
            d.setC(c);
 
            aDAO.mergeA(a);


 All   Comments   Work Log   Change History   FishEye      Sort Order: Ascending order - Click to sort in descending order
David Trott - 01/Feb/06 02:35 AM
I have been able to narrow this issue down a little further:

The extra updates are **NOT** issued if I remove the mapping for versioning:

        <version name="concurrentVersion" column="concurrent_version" type="long"/>

Josh Moore - 19/Apr/06 02:46 PM
Hibernate 3.1.3
Postgres 7.4.8
Java 1.5.0_06

I fairly certain I'm having the same issue. Code of the form fails:

       // first session

        Experimenter e = (Experimenter) session.get(Experimenter.class, 0);
        expVersion = e.getVersion();

        Project p = new Project();
        p.getDetails().setOwner( e );
        p = session.merge( p );
        Long id = p.getId();

        // second session

        p = (Project) session.createQuery("from Project p "
                + " join fetch p.details.owner " + " where p.id = :id ").
                setParameter("id",id ).uniqueResult();
        p.setName( " updated " );
        p = (Project) session.merge( p );
  
       // third session

        Experimenter e = (Experimenter) session.get(Experimenter.class, 0);
        assertTrue(expVersion.equals(e.getVersion()));

Preparing a test case now. TypeFactory.replace seems to be setting the "dirty=true" in AbstractPersistentCollection by calling clear on PersistentSet which calls dirty(). (Will attach a screen shot momentarily)

Josh Moore - 19/Apr/06 02:48 PM
Screenshot showing the call to AbstractPersistentCollection.dirty() in Eclipse debugger.

Josh Moore - 19/Apr/06 03:31 PM
hhh1401 directory with test case, two java beans, two hbm files to be copied to <HIB>/test/org/hibernate/test. Output of intersted (with show_sql=true) is:

Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into experimenter (version, id) values (?, ?)
Hibernate: select experiment0_.id as id0_1_, experiment0_.version as version0_1_, groupexper1_.child as child3_, groupexper1_.id as id3_, groupexper1_.id as id1_0_, groupexper1_.version as version1_0_, groupexper1_.child as child1_0_ from experimenter experiment0_ left outer join groupexperimentermap groupexper1_ on experiment0_.id=groupexper1_.child where experiment0_.id=?
Hibernate: update experimenter set version=? where id=? and version=?

This final update is unnecessary since we are simply merging an extant Experimenter:

        // Create our data
        Session s = openSession();
        Transaction t = s.beginTransaction();
        Experimenter e = new Experimenter();
        e = (Experimenter) s.merge( e );
        s.flush();
        t.commit();
        s.close();

        // Do our problematic merge.
        s = openSession();
        t = s.beginTransaction();
        Experimenter e2 = (Experimenter) s.merge( e );
        s.flush();
        t.commit();
        s.close();

        assertTrue( "Version was incremented although we did nothing",
                e.getVersion().equals( e2.getVersion() )
                );

Josh Moore - 20/Apr/06 02:34 AM
I can also confirm that removing the <version/> mappings prevents the unnecessary updates. Output of interest (and not "intersted") is:

Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into experimenter (id) values (?)
Hibernate: select experiment0_.id as id0_1_, 0 as formula0_1_, groupexper1_.child as child3_, groupexper1_.id as id3_, groupexper1_.id as id1_0_, groupexper1_.child as child1_0_, 0 as formula1_0_ from experimenter experiment0_ left outer join groupexperimentermap groupexper1_ on experiment0_.id=groupexper1_.child where experiment0_.id=?


Josh Moore - 13/Jul/06 02:35 PM
Apparently there are others having similar issues without the merge method. See HHH-1564

Josh Moore - 19/Jul/06 07:58 AM
Confirmed test failure for:

  Revision: 10123 (~3.2.cr2)
  HQL dialect.


Josh Moore - 19/Jul/06 08:05 AM
These tests failed as well:

    [junit] TEST org.hibernate.test.cuk.CompositePropertyRefTest FAILED
    [junit] TEST org.hibernate.test.hql.ASTParserLoadingTest FAILED
    [junit] TEST org.hibernate.test.hql.CriteriaHQLAlignmentTest FAILED
    [junit] TEST org.hibernate.test.hql.HQLTest FAILED
    [junit] TEST org.hibernate.test.optlock.OptimisticLockTest FAILED
    [junit] TEST org.hibernate.test.orphan.PropertyRefTest FAILED
    [junit] TEST org.hibernate.test.propertyref.PropertyRefTest FAILED
    [junit] TEST org.hibernate.test.readonly.ReadOnlyTest FAILED
    [junit] TEST org.hibernate.test.sql.GeneralTest FAILED

 OptimisticLock, naturally, seems to be significant. Attaching the test log here.

Josh Moore - 13/Sep/06 03:24 AM
Attached new HHH1401Test (replaces version in zip). Has second test using collections which fail despite the fixes Steve made to HHH-1668.

Steve Ebersole - 03/Nov/06 12:44 PM
So in my testing (against the latest code), the only scenario in which I can reproduce this (I am using the entity's defined in org.hhibernate.test.ops) is when merging an unmodified entity which:
(1) is versioned
(2) has collection which actually has elements

I am still looking into that case.

Can anyone else confirm that they do not see this problem in any other scenarios? The collection I am using is specifically an inverse hierarchical Set...

David Trott - 06/Nov/06 03:53 PM
All the cases I tested were consistent with Steve Ebersole's test case (I am not aware of any other ones).

Steve Ebersole - 06/Nov/06 04:05 PM
So at this point, I have added a bunch of tests now to test this behavior. If anyone is still experiencing issues here, please refer to one of these tests as a baseline for the behavior you see...

The tests consist of a couple new test methods in org.hibernate.test.ops.MergeTest:
(1) testNoExtraUpdatesOnMerge()
(2) testNoExtraUpdatesOnMergeWithCollection()
(3) testNoExtraUpdatesOnMergeVersioned()
(4) testNoExtraUpdatesOnMergeVersionedWithCollection()

and the new/expanded org.hibernate.test.collection.CollectionSuite

For those previously experiencing issues in this area, please check out SVN (3.2 or trunk) and test with my changes...