Make delicious recipes!


Dirty checking in Hibernate


Checkout the map-type fields example where we store an Employee object.
Employee has 4 fields:
Integer id;
String name;
Map<Integer, Job> jobs;
Map<Manager, Company> companyMap;

Suppose we want to change only the name of one employee.
In Hibernate, this can be done by:
  1. Load the entity first.
  2. Change its name.
  3. Save the entity.

For the given example, following queries are fired:
// 1) Load the entity to make it persistent
//    (Replaced column-names by star for simplicity)
    select *
    from
        EMPLOYEE employee0_ 
    left outer join
        COMPANY companymap1_ 
            on employee0_.id=companymap1_.EMPLOYEE_ID 
    left outer join
        MANAGER manager2_ 
            on companymap1_.MANAGER_ID=manager2_.id 
    left outer join
        JOB jobs3_ 
            on employee0_.id=jobs3_.EMPLOYEE_ID 
    where
        employee0_.id=?

// 2) Update the changed entities
    update
        EMPLOYEE 
    set
        name=? 
    where
        id=?
However, this approach is not very performant because it involves loading the entire entity-graph first. Lazy loading can give us some performance boost for this specific case, but in general, it may not be very useful since the property to be changed may be deep down the entity graph and then lazy-loading will have to fire additional queries to fetch the same.


Hibernate provide a way to deal with this by providing a CustomEntityDirtinessStrategy interface. Applications can override this interface and themselves determine what entities are dirty and what has changed in those dirty entities. Below is an example to show how this works.


  1. Implement the CustomEntityDirtinessStrategy:
    package com.prismoskills.hibernate.hierarchy;
    
    import org.hibernate.CustomEntityDirtinessStrategy;
    import org.hibernate.Session;
    import org.hibernate.persister.entity.EntityPersister;
    
    import com.prismoskills.hibernate.hierarchy.pojos.fields.CustomDirtyCheckable;
    
    public class Strategy implements CustomEntityDirtinessStrategy
    {
        public static final Strategy INSTANCE = new Strategy();
    
        int canDirtyCheckCount = 0;
        int isDirtyCount = 0;
        int resetDirtyCount = 0;
        int findDirtyCount = 0;
    
        public boolean canDirtyCheck(Object entity, EntityPersister persister, Session session)
        {
            canDirtyCheckCount++;
            return CustomDirtyCheckable.class.isInstance( entity );
        }
    
        public boolean isDirty(Object entity, EntityPersister persister, Session session)
        {
            isDirtyCount++;
            boolean isDirty = ! CustomDirtyCheckable.class.cast(entity).getChangedValues().isEmpty();
            System.out.println( entity + ": isDirty = " + isDirty );
            return isDirty;
        }
    
        public void resetDirty(Object entity, EntityPersister persister, Session session)
        {
            resetDirtyCount++;
            System.out.println( entity + ": resetDirty" );
            CustomDirtyCheckable.class.cast(entity).getChangedValues().clear();
        }
    
        public void findDirty(final Object entity, EntityPersister persister,
          Session session, DirtyCheckContext dirtyCheckContext)
        {
            findDirtyCount++;
            System.out.println( entity + ": findDirty" );
            dirtyCheckContext.doDirtyChecking(
                    new AttributeChecker()
                    {
                        public boolean isDirty(AttributeInformation attributeInformation)
                        {
                            String attrName = attributeInformation.getName();
                            CustomDirtyCheckable checkable = CustomDirtyCheckable.class.cast(entity); 
                            boolean isDirty = checkable.getChangedValues().containsKey(attrName);
                            System.out.println( entity + ": isDirty for " + attrName + " = " + isDirty );
                            return isDirty;
                        }
                    }
                    );
        }
    
        void resetState()
        {
            canDirtyCheckCount = 0;
            isDirtyCount = 0;
            resetDirtyCount = 0;
            findDirtyCount = 0;
        }
    
        @Override
        public String toString()
        {
            return "Strategy [canDirtyCheckCount=" + canDirtyCheckCount + ", isDirtyCount=" + isDirtyCount
                    + ", resetDirtyCount=" + resetDirtyCount + ", findDirtyCount=" + findDirtyCount + "]";
        }
    
    }
    


  2. Above strategy-class assumes that all entity classes implement the following interface:
    package com.prismoskills.hibernate.hierarchy.pojos.fields;
    
    import java.util.Map;
    
    public interface CustomDirtyCheckable
    {
        public Map<String, Object> getChangedValues();
    }
    


  3. BaseEntity.java - Base class from which all other entities are derived.
    (Note that it also stores a Transient map to store the dirtiness of each entity)
    package com.prismoskills.hibernate.hierarchy.pojos.fields;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import javax.persistence.Id;
    import javax.persistence.MappedSuperclass;
    import javax.persistence.Transient;
    
    @MappedSuperclass
    public class BaseEntity implements CustomDirtyCheckable
    {
        @Id
        Integer id;
    
        public BaseEntity()
        {
            getId();
        }
    
        public Integer getId()
        {
            if (id == null)
            {
                id = (int)System.currentTimeMillis();
            }
            return id;
        }
    
        public void setId(Integer id)
        {
            this.id = id;
        }
    
        @Override
        public String toString()
        {
            return this.getClass().getSimpleName();
        }
    
        public Map<String, Object> getChangedValues() {
            return changedValues;
        }
    
        @Transient
        protected final Map<String,Object> changedValues = new HashMap<String, Object>();
    }
    


  4. Employee.java (Note dirty-setting being done in all setter functions)
    package com.prismoskills.hibernate.hierarchy.pojos.fields;
    
    import java.util.Map;
    
    import javax.persistence.CascadeType;
    import javax.persistence.Entity;
    import javax.persistence.FetchType;
    import javax.persistence.MapKey;
    import javax.persistence.MapKeyJoinColumn;
    import javax.persistence.OneToMany;
    import javax.persistence.Table;
    
    @Entity
    @Table(name = "EMPLOYEE")
    public class Employee extends BaseEntity
    {
        String name;
    
        @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "employee")
        @MapKey
        Map<Integer, Job> jobs;
    
        @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "engineer")
        @MapKey
        @MapKeyJoinColumn(name = "MANAGER_ID")
        Map<Manager, Company> companyMap;
    
        public Employee()
        {
            super();
        }
    
        public Employee(String name)
        {
            this.name = name;
        }
    
        public Map<Integer, Job> getJobs()
        {
            return jobs;
        }
    
        public void setJobs(Map<Integer, Job> jobs)
        {
            this.jobs = jobs;
            changedValues.put( "jobs", this.jobs );
        }
    
        public Map<Manager, Company> getCompanyMap()
        {
            return companyMap;
        }
    
        public void setCompanyMap(Map<Manager, Company> companyMap)
        {
            this.companyMap = companyMap;
            changedValues.put( "companyMap", this.companyMap );
        }
    
        public String getName()
        {
            return name;
        }
    
        public void setName(String name)
        {
            this.name = name;
            changedValues.put( "name", this.name );
        }
    }
    


  5. Company.java (Note dirty-setting being done in all setter functions)
    package com.prismoskills.hibernate.hierarchy.pojos.fields;
    
    import javax.persistence.CascadeType;
    import javax.persistence.Entity;
    import javax.persistence.FetchType;
    import javax.persistence.JoinColumn;
    import javax.persistence.ManyToOne;
    import javax.persistence.OneToOne;
    import javax.persistence.Table;
    
    @Entity
    @Table(name = "COMPANY")
    public class Company extends BaseEntity
    {
        String name;
    
        @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
        @JoinColumn(name = "EMPLOYEE_ID")
        Employee engineer;
    
        @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
        @JoinColumn(name = "MANAGER_ID", nullable = false)
        Manager manager;
    
        public Company()
        {
            super();
        }
    
        public Company(String name)
        {
            super();
            this.name = name;
        }
    
        public String getName()
        {
            return name;
        }
    
        public void setName(String name)
        {
            this.name = name;
            changedValues.put( "name", this.name );
        }
    
        public Employee getEngineer()
        {
            return engineer;
        }
    
        public void setEngineer(Employee engineer)
        {
            this.engineer = engineer;
            changedValues.put( "engineer", this.engineer );
        }
    
        public Manager getManager()
        {
            return manager;
        }
    
        public void setManager(Manager manager)
        {
            this.manager = manager;
            changedValues.put( "manager", this.manager );
        }
    
        @Override
        public String toString()
        {
            return "Company [name=" + name + "]";
        }
    }
    


  6. Job.java (Note dirty-setting being done in all setter functions)
    package com.prismoskills.hibernate.hierarchy.pojos.fields;
    
    import javax.persistence.CascadeType;
    import javax.persistence.Entity;
    import javax.persistence.FetchType;
    import javax.persistence.JoinColumn;
    import javax.persistence.ManyToOne;
    import javax.persistence.Table;
    
    @Entity
    @Table(name = "JOB")
    public class Job extends BaseEntity
    {
        String designation;
    
        @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
        @JoinColumn(name = "EMPLOYEE_ID", nullable = false)
        Employee employee;
    
        public Job(String designation, Employee employee)
        {
            this.designation = designation;
            this.employee = employee;
        }
    
        public Job()
        {
            super();
        }
    
        public String getDesignation()
        {
            return designation;
        }
    
        public void setDesignation(String designation)
        {
            this.designation = designation;
            changedValues.put( "designation", this.designation );
        }
    
        public Employee getEmployee()
        {
            return employee;
        }
    
        public void setEmployee(Employee employee)
        {
            this.employee = employee;
            changedValues.put( "employee", this.employee );
        }
    }
    


  7. Manager.java (Note dirty-setting being done in all setter functions)
    package com.prismoskills.hibernate.hierarchy.pojos.fields;
    
    import javax.persistence.Entity;
    import javax.persistence.Table;
    
    @Entity
    @Table(name = "MANAGER")
    public class Manager extends BaseEntity
    {
        String name;
    
        public Manager()
        {
        }
    
        public Manager(String name)
        {
            this.name = name;
        }
    
        public String getName()
        {
            return name;
        }
    
        public void setName(String name)
        {
            this.name = name;
            changedValues.put( "name", this.name );
        }
    }
    


  8. hibernate.cfg.xml
    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE hibernate-configuration SYSTEM
    "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
     
    <hibernate-configuration>
    <session-factory>
     
    <!-- Database Type -->
    <property name="hibernate.dialect">
    org.hibernate.dialect.Oracle10gDialect
    </property>
    <property name="hibernate.connection.driver_class">
    oracle.jdbc.driver.OracleDriver
    </property>
     
    <!-- Database Properties -->
    <property name="hibernate.connection.url">
    jdbc:oracle:thin:@//localhost:1522/XE
    </property>
    <property name="hibernate.connection.username">system</property>
    <property name="hibernate.connection.password">passwordpassword</property>
    
    <!--------------------------------------------------------------------------------->
    <!----- Glue the CustomEntityDirtinessStrategy by the following configuration ----->
    <!--------------------------------------------------------------------------------->
    <property name="hibernate.entity_dirtiness_strategy">
    com.prismoskills.hibernate.hierarchy.Strategy
    </property>
     
     
    <!-- Debugging and Formatting -->
    <property name="show_sql">true</property>
    <property name="format_sql">true</property>
     
    <!-- Required for annotations to work without *.hbm.xml -->
    <mapping class="com.prismoskills.hibernate.hierarchy.pojos.fields.Employee" />
    <mapping class="com.prismoskills.hibernate.hierarchy.pojos.fields.Company" />
    <mapping class="com.prismoskills.hibernate.hierarchy.pojos.fields.Job" />
    <mapping class="com.prismoskills.hibernate.hierarchy.pojos.fields.Manager" />
     
    </session-factory>
    </hibernate-configuration>
    


  9. META-INF/persistence.xml
    <persistence xmlns="http://java.sun.com/xml/ns/persistence"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
            version="2.0">
            
        <persistence-unit name="com.prismoskills.hibernate">
          <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
          
          <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.Oracle10gDialect" />
            <property name="javax.persistence.jdbc.url" value="jdbc:oracle:thin:@//localhost:1522/XE"/>
            <property name="javax.persistence.jdbc.user" value="system"/>
            <property name="javax.persistence.jdbc.password" value="passwordpassword"/>
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
            <property name="hibernate.entity_dirtiness_strategy" value="com.prismoskills.hibernate.hierarchy.Strategy"/>
          </properties>
        </persistence-unit>
    </persistence>
    


  10. Main class to run the above
    package com.prismoskills.hibernate.hierarchy;
    
    import java.io.Serializable;
    import java.util.HashMap;
    import java.util.Map;
    
    import javax.persistence.EntityManager;
    import javax.persistence.EntityManagerFactory;
    import javax.persistence.EntityTransaction;
    import javax.persistence.Persistence;
    
    import org.hibernate.EmptyInterceptor;
    import org.hibernate.Session;
    import org.hibernate.SessionFactory;
    import org.hibernate.cfg.Configuration;
    import org.hibernate.type.Type;
    
    import com.prismoskills.hibernate.hierarchy.pojos.fields.Company;
    import com.prismoskills.hibernate.hierarchy.pojos.fields.Employee;
    import com.prismoskills.hibernate.hierarchy.pojos.fields.Job;
    import com.prismoskills.hibernate.hierarchy.pojos.fields.Manager;
    
    public class HibernateDirtyCheckingExample
    {
        public static class UpdateInterceptor extends EmptyInterceptor
        {
            @Override
            public Boolean isTransient(Object entity)
            {
                System.out.println (entity + ": isTransient called");
                return Boolean.FALSE;
            }
        }
    
        EntityManagerFactory emf = null;
        EntityManager em = null;
        EntityTransaction tx = null;
        public static void main(String args[])
        {
            (new HibernateDirtyCheckingExample()).testDirtyChecking();
        }
    
        public void testDirtyChecking ()
        {
            try
            {
                emf = Persistence.createEntityManagerFactory("com.prismoskills.hibernate");
                em = emf.createEntityManager();
                tx = em.getTransaction();
                tx.begin();
                Employee employee = createEmployee();
                em.persist(employee);
                em.flush();
                tx.commit();
                em.close(); em=null;
                System.out.println ("-----Put in DB-----");
    
                em = emf.createEntityManager();
                Employee result = em.find(Employee.class, employee.getId());
                System.out.println ("-----Read " + result.getName() + "-----");
                em.close(); em=null;
                emf.close(); emf=null;
    
    
                System.out.println ("\n\n---Testing dirty checking---");
                SessionFactory factory = new Configuration().configure().buildSessionFactory();
                Session session = factory.withOptions().interceptor(new UpdateInterceptor()).openSession();
                session.beginTransaction();
                //result = (Employee) session.get(Employee.class, employee.getId());
                //result = (Employee) session.merge(result);
                result.setName("Johnny");
                session.saveOrUpdate(result);
                session.getTransaction().commit();
                session.close();
    
            }
            catch (Exception e)
            {
                e.printStackTrace();
                if (tx!=null) tx.rollback();
                System.exit(1);
            }
            finally
            {
                if (em != null)
                    em.close();
                if (emf != null)
                    emf.close();
            }
            System.out.println ("Finished execution");
        }
    
        private static Employee createEmployee()
        {
            final Employee employee = new Employee("Michael");
    
            // Add map with primitive key Integer
            Job job1 = new Job("Software Engineer", employee);
            Map<Integer, Job> jobs = new HashMap<Integer, Job>();
            jobs.put(job1.getId(), job1);
            employee.setJobs(jobs);
    
            // Add map with entity key Manager
            Manager john = new Manager("John");
            Company apple = new Company("Apple");
            apple.setManager(john);
            apple.setEngineer(employee);
    
            Manager rambo = new Manager("Rambo");
            Company google = new Company("Google");
            google.setManager(rambo);
            google.setEngineer(employee);
    
            Map<Manager, Company> companyMap = new HashMap<Manager, Company>();
            companyMap.put(john, apple);
            //companyMap.put(rambo, google);
            employee.setCompanyMap(companyMap);
            return employee;
        }
    }
    


Noteworthy points:

  1. Every entity class implements an interface whose method is called by CustomEntityDirtinessStrategy.

  2. Every entity does a basic dirty-checking by recording state change whenever a setter method is called.

  3. The CustomEntityDirtinessStrategy implementation has to be specified using the hibernate.cfg.xml file.

  4. The main-class HibernateDirtyCheckingExample has some interesting features too:

    • On line 65, it uses a DETATCHED entity to fire a Session.saveOrUpdate()
    • It does not load the entity first before persisting it to the DB.
    • It uses UpdateInterceptor to override isTransient() (Explained below)


Results

On running the above example with UpdateInterceptor.isTransient() commented, following is the SQL produced.
    select
        employee_.id,
        employee_.name as name2_1_ 
    from
        EMPLOYEE employee_ 
    where
        employee_.id=?


    select
        company_.id,
        company_.EMPLOYEE_ID as EMPLOYEE_ID3_0_,
        company_.MANAGER_ID as MANAGER_ID4_0_,
        company_.name as name2_0_ 
    from
        COMPANY company_ 
    where
        company_.id=?
 
 
    select
        manager_.id,
        manager_.name as name2_3_ 
    from
        MANAGER manager_ 
    where
        manager_.id=?


    select
        job_.id,
        job_.designation as designation2_2_,
        job_.EMPLOYEE_ID as EMPLOYEE_ID3_2_ 
    from
        JOB job_ 
    where
        job_.id=?


// Employee: isDirty = true
// Employee: findDirty
// Employee: isDirty for companyMap = false
// Employee: isDirty for jobs = false
// Employee: isDirty for name = true
// Company [name=Apple]: isDirty = false
// Manager: isDirty = false
// Job: isDirty = false

    update
        EMPLOYEE 
    set
        name=? 
    where
        id=?
       
// Employee: resetDirty
// Finished execution



So Hibernate is calling our CustomEntityDirtinessStrategy but the number of SQLs fired to get the entity-state have increased.
Earlier, hibernate was firing just one SQL to ascertain the dirtiness of entities, now its firing one SQL per entity!!
What went wrong?
Looking at the stack trace, it appears that the select queries in this case are being generated from:
ForeignKeys.isTransient() line: 255
DefaultSaveOrUpdateEventListener(AbstractSaveEventListener).getEntityState() line: 511
DefaultSaveOrUpdateEventListener.performSaveOrUpdate() line: 100	
DefaultSaveOrUpdateEventListener.onSaveOrUpdate() line: 90	
SessionImpl.fireSaveOrUpdate() line: 680	
SessionImpl.saveOrUpdate() line: 672	
CascadingActions$5.cascade() line: 235	
Cascade.cascadeToOne() line: 352	
Cascade.cascadeAssociation() line: 295	
Cascade.cascadeProperty() line: 161	
Cascade.cascadeCollectionElements() line: 381	
Cascade.cascadeCollection() line: 321	
Cascade.cascadeAssociation() line: 298	
Cascade.cascadeProperty() line: 161	
Cascade.cascade() line: 118	
Cascade.cascade() line: 86	
DefaultSaveOrUpdateEventListener.cascadeOnUpdate() line: 375	
DefaultSaveOrUpdateEventListener.performUpdate() line: 349	
DefaultSaveOrUpdateEventListener.entityIsDetached() line: 244	
DefaultSaveOrUpdateEventListener.performSaveOrUpdate() line: 109	
DefaultSaveOrUpdateEventListener.onSaveOrUpdate() line: 90	
SessionImpl.fireSaveOrUpdate() line: 680	
SessionImpl.saveOrUpdate() line: 672	
SessionImpl.saveOrUpdate() line: 667	
HibernateDirtyCheckingExample.testDirtyChecking() line: 72



Missing piece in the puzzle

To eliminate this extra-call, we override the Interceptor.isTransient() method ( ForeignKeys.isTransient() line: 236 )
If that is done, Hibernate does not fetch the data from DB just to find the entity-state (Transient or Detatched).
This yields the maximum performance by eliminating all the calls to the DB.
Uncomment the UpdateInterceptor.isTransient() method from the above and run again.
Employee: isTransient called
Company [name=Apple]: isTransient called
Manager: isTransient called
Job: isTransient called
Employee: isDirty = true
Employee: findDirty
Employee: isDirty for companyMap = false
Employee: isDirty for jobs = false
Employee: isDirty for name = true
Company [name=Apple]: isDirty = false
Manager: isDirty = false
Job: isDirty = false
Hibernate: 
    update
        EMPLOYEE 
    set
        name=? 
    where
        id=?
Employee: resetDirty
The first 4 lines of the above log show how the UpdateInterceptor.isTransient() method is called and it prevents a trip to the DB by eliminating all the SQLs.








Like us on Facebook to remain in touch
with the latest in technology and tutorials!


Got a thought to share or found a
bug in the code?
We'd love to hear from you:

Name:
Email: (Your email is not shared with anybody)
Comment:

Facebook comments:

Site Owner: Sachin Goyal