Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
Re: [eclipselink-users] Cache issue with EclipseLink?

The problem seems to be your equals and hashCode methods.  Generally these
should be left as the default (identity) or only check the primary key.  In
particular to Employee hashCode is wrong, a hashCode must be a consistent
value, but you are using name and dept which can change, an object's
hashCode should never change.  Because you are storing your employees in a
Set and changing your hashCode, a remove or contain will no longer work as
the objects hashCode has changed.

EclipseLink generally has no dependencies on equals and hashCode unlike
other JPA providers, but we still require to be able to remove objects from
collections, and that requires that the hashCode and equals are at least
correctly implemented.



Kevin Lester wrote:
> 
> All,
> 
> Recently we have noticed an issue with EclipseLink that appears to be
> related to the EclipseLink cache.  Essentially we have noticed that
> objects modified via the "merge" method are not always updated in the
> cache.  If we merge an object, and then immediately query for the same
> object in a different EntityManager, we sometimes get a stale copy of the
> object.
> Consider the simple test case below:
>  - Object A has 2 children (marked @PrivateOwned) and is persisted.  We
> see 2 child records in the DB.
>  - We then remove a child from Object A, so it now has 1 child.
>  - We merge Object A.  The database now has 1 child record in the DB
> (which is correct) and the object returned from the merge has 1 child
> (which is correct).
>  - We close the current EntityManager, get a new one, and then query for
> the object we just persisted.  The object returned has 2 children (which
> is incorrect).  The extra child record returned is the child record that
> we just deleted.  No one else has modified this record, so we would expect
> it to only have 1 child.
> 
> Based on this test case, the cache appears to be out of sync with the DB,
> and is returning stale data.  If we clear the cache after all merges (or
> disable the cache), everything works properly.  This is also not 100%
> reproducible.  We often see the first change or two work properly, but
> subsequent changes are not reflected in the cache.  
> 
> So the question is: what is going on?  This appears to be an EclipseLink
> bug to me, but it is definitely possible that we are not using it
> properly.
> 
> To help debug, I have created a "simple" test project that reproduces the
> issue the same way we see it in our application.  The test case uses
> EclipseLink 1.1-RC2, a mysql DB, and uses an alternative connection pool
> than the one provided by EclipseLink (due to some issues we saw).  My
> sample project has 4 test cases.  The last test case always fails, and
> sometimes the second test fails as well.  Sorry that my simple test case
> is so long, but I could not reproduce this issue with a smaller test case.
> 
> The relevant code for my test case is below.  I can post the entire
> project if required.  I thought I would post this issue here first before
> opening a Jira issue in case we are simply doing something wrong.
> 
> A few Notes:
> If I disable the cache in the persistence.xml file, then all test cases
> pass.  Similarly, if I remove the following line in the Employee.java
> file's hashCode method while leaving the cache enabled, the test cases
> pass:
>     ret += (manager != null ? manager.hashCode() : 0);
> Also, the "setupTest" method always creates new children for the Manager
> object, and just copies in their corresponding PK from the current child
> objects in the DB.  This is done to simulate how our objects are handled
> in the application.
> 
> Does anyone see anything obvious that we are doing wrong?
> Thanks in advance
> 
> Persistence.xml
> <?xml version="1.0" encoding="UTF-8"?>
> <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
>     persistence_1_0.xsd" version="1.0">
>     
>     <persistence-unit name="testPersistenceUnit" >
>     <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
>       
>       <jta-data-source>java:/comp/env/jdbc/testDs</jta-data-source>
>      
> <non-jta-data-source>java:/comp/env/jdbc/testDs</non-jta-data-source>
>       
>       <class>Employee</class>
>       <class>Manager</class
> 	  <!-- Provider-specific settings -->
> 	  <properties>
> 		  <property name="eclipselink.ddl-generation"
> value="drop-and-create-tables"/> 
> 		  <property name="eclipselink.logging.logger"
> value="Log4JEclipseLinkLogger"/>
> 		  <property name="eclipselink.logging.level" value="FINEST" />
> 		  <property name="eclipselink.logging.thread" value="true"/>
> 		  <property name="eclipselink.logging.session" value="true"/>
> 		  <property name="eclipselink.logging.timestamp" value="true"/>
> 		  <property name="eclipselink.logging.exceptions" value="true"/>
> 		  
> 		  <property name="eclipselink.create-ddl-jdbc-file-name"
> value="create-tables.sql"/>
> 		  <property name="eclipselink.drop-ddl-jdbc-file-name"
> value="drop-tables.sql"/>
> 		  <property name="eclipselink.target-database" value="MySQL4"/>
> 		  <property name="eclipselink.cache.type.default" value="SoftWeak" /> 
> 		  <property name="eclipselink.jdbc.batch-writing" value="JDBC"/>
> 		  <property name="eclipselink.jdbc.cache-statements" value="true"/>
> 		  <property name="eclipselink.jdbc.cache-statements.size" value="100"/>
> 		  
> 		  <!-- The test cases will pass if you uncomment the properties below.
> 		  <property name="eclipselink.cache.shared.default" value="false"/>
> 		  <property name="eclipselink.cache.size.default" value="0"/>
> 		  <property name="eclipselink.cache.type.default" value="None"/>
> 		  -->
> 	  </properties>  		
> 	</persistence-unit>
> </persistence>
> --------------------------------------------------------
> EclipseLinkCacheTest.java:
> --------------------------------------------------------
> import static org.junit.Assert.assertEquals;
> import static org.junit.Assert.fail;
> import java.util.Iterator;
> import java.util.List;
> import javax.naming.Context;
> import javax.naming.InitialContext;
> import javax.naming.NamingException;
> import javax.persistence.EntityManager;
> import javax.persistence.EntityManagerFactory;
> import javax.persistence.Persistence;
> import javax.persistence.Query;
> import org.apache.log4j.Logger;
> import org.junit.Before;
> import org.junit.BeforeClass;
> import org.junit.Test;
> import com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource;
> import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
> 
> public class EclipseLinkCacheTester {
>   protected static EntityManagerFactory emf;
>   private static Logger logger =
> Logger.getLogger(EclipseLinkCacheTester.class);
>   private static final String PERSISTENCE_UNIT = "testPersistenceUnit";
>   
>   @BeforeClass
>   public static void initialize(){
>     System.setProperty("LOG_DIR", System.getProperty("user.dir"));
>     
>     //set up the DB datasource 
>     setupInitialContext();
>     
>     //Set up the entity manager factory
>     logger.info("persistanceUnitName: " + PERSISTENCE_UNIT);
>     emf = Persistence.createEntityManagerFactory(PERSISTENCE_UNIT);
>   }
>   
>   @Before
>   public void setupTest(){
>     Manager manager = null;
>     try{
>       manager = this.findManagerByName("Homer", "Sector 7-G");
>     } catch (Exception e){
>       e.printStackTrace();
>       fail("An error occurred getting the manager");
>     }
>     
>     if (manager == null){
>       manager = new Manager();
>       manager.setName("Homer");
>       manager.setDept("Sector 7-G");
>     }
>     long now = System.currentTimeMillis();
>     
>     System.out.println("SETUP: Manager'employee count= " +
> manager.getEmployees().size());
>     Integer lennysID = null, carlsID = null;
>     for (Iterator<Employee> iter = manager.getEmployees().iterator();
> iter.hasNext();){
>       Employee emp = iter.next();
>       if (emp.getName().equals("Carl")) carlsID = emp.getEmpId();
>       if (emp.getName().equals("Lenny")) lennysID = emp.getEmpId();
>       //Remove the child node.
>       iter.remove();
>     }
>     
>     Employee lenny = createEmployee(manager, "Lenny", now);
>     lenny.setEmpId(lennysID);
>     Employee carl = createEmployee(manager, "Carl", now);
>     carl.setEmpId(carlsID);
>     manager.getEmployees().add(lenny);
>     manager.getEmployees().add(carl);
>     //Update the manager
>     manager = updateManager(manager);
>     assertEquals("The number of employees was different than
> expected",2,manager.getEmployees().size());
> 
>     System.out.println("END SETUP: Manager'employee count= " +
> manager.getEmployees().size());
>   }
>   
>   @Test
>   public void testDoNothing(){
>     Manager manager = null;
>     try{
>       manager = this.findManagerByName("Homer", "Sector 7-G");
>     } catch (Exception e){
>       fail("Error occurred finding the manager");
>     }
>     
>     assertEquals("The number of employees was different than
> expected",2,manager.getEmployees().size());
>   }
>   
>   
>   @Test
>   public void deleteEmployeeTest(){
>     Manager manager = null;
>     try{
>       manager = this.findManagerByName("Homer", "Sector 7-G");
>       Employee lenny = getLenny(manager);
>       manager.getEmployees().remove(lenny);
>       manager = updateManager(manager);
>       assertEquals("The number of employees was different than
> expected",1,manager.getEmployees().size());
>     } catch (Exception e){
>       fail("Error occurred finding the manager");
>     }
>     
>     try{
>       manager = this.findManagerByName("Homer", "Sector 7-G");
>     } catch (Exception e){
>       fail("Error occurred finding the manager");
>     }
>     assertEquals("The number of employees was different than
> expected",1,manager.getEmployees().size());
>   }
>   
>   
>   @Test
>   public void testAddEmployee(){
>     Manager manager = null;
>     try{
>       manager = this.findManagerByName("Homer", "Sector 7-G");
>       manager.getEmployees().add(this.createEmployee(manager, "My. Burns",
> System.currentTimeMillis()));
>       manager = updateManager(manager);
>       assertEquals("The number of employees was different than
> expected",3,manager.getEmployees().size());
>     } catch (Exception e){
>       fail("Error occurred finding the manager");
>     }
>     
>     try{
>       manager = this.findManagerByName("Homer", "Sector 7-G");
>     } catch (Exception e){
>       fail("Error occurred finding the manager");
>     }
>     assertEquals("The number of employees was different than
> expected",3,manager.getEmployees().size());
>   }
>   
>   
>   @Test
>   public void updateEmployeeTest(){
>     Manager manager = null;
>     try{
>       manager = this.findManagerByName("Homer", "Sector 7-G");
>       Employee lenny = getLenny(manager);
>       lenny.setRank("Eye-candy");
>       manager = updateManager(manager);
>       assertEquals("The number of employees was different than
> expected",2,manager.getEmployees().size());
>       assertEquals("Lenny's rank was not properly
> updated","Eye-candy",getLenny(manager).getRank());
>     } catch (Exception e){
>       fail("Error occurred finding the manager");
>     }
>     
>     try{
>       manager = this.findManagerByName("Homer", "Sector 7-G");
>     } catch (Exception e){
>       fail("Error occurred finding the manager");
>     }
>     assertEquals("The number of employees was different than
> expected",2,manager.getEmployees().size());
>     assertEquals("Lenny's rank was not properly
> updated","Eye-candy",getLenny(manager).getRank());
>   }
>   
>   
>   
>   private Employee getLenny(Manager manager){
>     for (Iterator<Employee> iter = manager.getEmployees().iterator();
> iter.hasNext();){
>       Employee emp = iter.next();
>       if (emp.getName().equals("Lenny")){
>         return emp;
>       }
>     }
>     return null;
>   }
>   
>   private Employee createEmployee(Manager manager, String name, long now){
>     Employee emp = new Employee();
>     emp.setManager(manager);
>     emp.setName(name);
>     emp.setRank("peon");
>     emp.setLastModified(now);
>     return emp;
>   }
>     
>   
>   /**
>    * Returns the Manager record that exists for the given Name and Dept if
> one exists 
>    * @param name the name of the <code>Manager</code>
>    * @param dept the department of the <code>Manager</code> 
>    * @return the Manager record if one exists
>    * @throws Exception if multiple Manager records exist.
>    */
>   @SuppressWarnings("unchecked")
>   private Manager findManagerByName(String name, String dept) throws
> Exception{
>     EntityManager em = getNewEntityManager();
>     try {
>       Query q = em.createNamedQuery("GetManagerByNameDept");
>       q.setParameter("name", name);
>       q.setParameter("dept", dept);
>       List<Manager> results = q.getResultList();
>       if (results == null || results.size() == 0){
>         return null;
>       }
>       if (results.size() != 1){
>         //We have duplicate Build records in the DB, this is illegal
>         String errorMsg = "Duplicate Manager records were found with name
> = " + name + " and department " + dept; 
>         logger.error(errorMsg);
>         throw new Exception(errorMsg);
>       }
>       return (Manager) results.get(0);
>     } finally {
>       em.close();
>     }
>   }
> 
>   
>   private Manager updateManager(Manager manager){
>     EntityManager em = getNewEntityManager();
>     try {
>       em.getTransaction().begin();
>       manager = em.merge(manager);
>       em.getTransaction().commit();
>       //Build newBuild=em.find(Build.class, inBuild.getBuildId());
>       //System.out.println("#Build files from Build in same session = " +
> newBuild.getFiles().size());
>     } finally {
>       em.close();
>     }
>     //The manger will be the original object passed in but with the ids
> set properly (which could potentially modify the hashCode, depending on
> how it is implemented)
>     //Since child objects were added prior to the ID being set, there is a
> chance that the child objects will not be 
>     //retrievable is they were stored in any Collections (because their
> hashcode affects their "bucket" in the collection).
>     //So we need to return a new object back from scratch which has
> everything set correctly.
>      return this.getManager(manager.getManagerId());
>   }
>  
>   
>   /**
>    * This method will get the Manager from the database.
>    * 
>    * @param managerId
>    */
>   private Manager getManager(Integer managerId){
>     EntityManager em = getNewEntityManager();
>     try {
>       return em.find(Manager.class, managerId);
>     } finally {
>       em.close();
>     }
>   }
>  
>   
>   /**
>    * @return
>    */
>   public EntityManager getNewEntityManager() {
>     EntityManager ret=emf.createEntityManager();
>     /**
>      * bug: http://forums.oracle.com/forums/thread.jspa?messageID=2284069
>      * https://glassfish.dev.java.net/issues/show_bug.cgi?id=3937
>      */
>     return ret;
>   }
>   
> 
>   private static void setupInitialContext(){
>     try {
>       // Create initial context
>       System.setProperty(Context.INITIAL_CONTEXT_FACTORY,
> "org.apache.naming.java.javaURLContextFactory");
>       System.setProperty(Context.URL_PKG_PREFIXES, "org.apache.naming");
>       InitialContext ic = new InitialContext();
> 
>       ic.createSubcontext("java:");
>       ic.createSubcontext("java:/comp");
>       ic.createSubcontext("java:/comp/env");
>       ic.createSubcontext("java:/comp/env/jdbc");      
>       
>       // Construct DataSource
>       MysqlDataSource dataSource = new MysqlConnectionPoolDataSource();
>       String dbURL="jdbc:mysql://HOST/klester_eclipselink_test";
>       dataSource.setURL(dbURL);
>       System.out.println("Running test against db "+dbURL);
>       dataSource.setUser(USER);
>       dataSource.setPassword(PASSWORD);
>       ic.bind("java:/comp/env/jdbc/testDs", dataSource);
>     } catch (NamingException ex) {
>       ex.printStackTrace();
>       logger.error("Error occurred setting the datasource", ex);
>     }
>   }
>   
> }
> --------------------------------------------------------
> Manager.java
> --------------------------------------------------------
> import java.util.HashSet;
> import java.util.Set;
> 
> import javax.persistence.CascadeType;
> import javax.persistence.Column;
> import javax.persistence.Entity;
> import javax.persistence.FetchType;
> import javax.persistence.GeneratedValue;
> import javax.persistence.Id;
> import javax.persistence.NamedQuery;
> import javax.persistence.OneToMany;
> import javax.persistence.Table;
> 
> import org.eclipse.persistence.annotations.PrivateOwned;
> 
> @Entity
> @Table(name = "Manager")
> @NamedQuery(name = "GetManagerByNameDept", query = "SELECT obj FROM
> Manager obj WHERE obj.name = :name and obj.dept = :dept")
> public class Manager {
>   private static final int HASH_PRIME = 37;
>   
>   @Id
>   @Column(name = "managerId")
>   @GeneratedValue
>   private Integer managerId;
>   
>   private String name = null;
>   
>   private String dept = null;
>   
>   @OneToMany (cascade=CascadeType.ALL, mappedBy="manager",
> fetch=FetchType.EAGER)
>   @PrivateOwned
>   private Set<Employee> employees = new HashSet<Employee>();
> 
>   public Integer getManagerId() {
>     return managerId;
>   }
> 
>   public void setManagerId(Integer managerId) {
>     this.managerId = managerId;
>   }
> 
>   public String getName() {
>     return name;
>   }
> 
>   public void setName(String name) {
>     this.name = name;
>   }
> 
>   public String getDept() {
>     return dept;
>   }
> 
>   public void setDept(String dept) {
>     this.dept = dept;
>   }
> 
>   public Set<Employee> getEmployees() {
>     return employees;
>   }
> 
>   public void setEmployees(Set<Employee> employees) {
>     this.employees = employees;
>   }
>   
>   @Override
>   public int hashCode() {
>     int ret = 0;
>     ret += HASH_PRIME * ((dept != null ? dept.hashCode() : 0));
>     ret += HASH_PRIME * ((name != null ? name.hashCode() : 0));
>     return ret;
>   } 
>   
> //  @Override
>   public boolean equals(Object obj) {
>     Manager other = (Manager) obj;
> 
>     return  ((dept != null ? dept.equals(other.dept) : (other.dept != null
> ? false : true)))
>         && ((name != null ? name.equals(other.name) : (other.name != null
> ? false : true)));
>   }
>   
> }
> --------------------------------------------------------
> Employee.java
> --------------------------------------------------------
> 
> import javax.persistence.CascadeType;
> import javax.persistence.Column;
> import javax.persistence.Entity;
> import javax.persistence.GeneratedValue;
> import javax.persistence.GenerationType;
> import javax.persistence.Id;
> import javax.persistence.JoinColumn;
> import javax.persistence.ManyToOne;
> import javax.persistence.Table;
> import javax.persistence.TableGenerator;
> 
> 
> @Entity
> @Table(name = "Employee")
> public class Employee {
>  
>   @Id
>   @Column(name = "empId")
>   @TableGenerator(
>         name="EMP_ID_SEQ",
>         table="SEQUENCE",
>         allocationSize=100
>     )
>   @GeneratedValue(strategy = GenerationType.AUTO, generator =
> "EMP_ID_SEQ")
>   private Integer empId;
>   
>   private String name = null;
>   
>   private String rank = null;
>   
>   @ManyToOne (cascade=CascadeType.REFRESH)
>   @JoinColumn(name="managerId")
>   private Manager manager = null;
> 
>   private long lastModified = 0L;
>   
>   public String getName() {
>     return name;
>   }
> 
>   public void setName(String name) {
>     this.name = name;
>   }
> 
>   public String getRank() {
>     return rank;
>   }
> 
>   public void setRank(String rank) {
>     this.rank = rank;
>   }
> 
>   public Manager getManager() {
>     return manager;
>   }
> 
>   public void setManager(Manager manager) {
>     this.manager = manager;
>   }
> 
>   public Integer getEmpId() {
>     return empId;
>   }
> 
>   public void setEmpId(Integer empId) {
>     this.empId = empId;
>   }
> 
>   public long getLastModified() {
>     return lastModified;
>   }
> 
>   public void setLastModified(long lastModified) {
>     this.lastModified = lastModified;
>   }
> 
>   @Override
>   public int hashCode() {
>     int ret = 0;
>     ret += (name != null ? name.hashCode() : 0);
>     ret += (rank != null ? rank.hashCode() : 0);
>     //Removing the line of code below makes the tests pass.
>     ret += (manager != null ? manager.hashCode() : 0);
>     return ret;
>   }
> 
>   
>   @Override
>   public boolean equals(Object obj) {
>     Employee other = (Employee) obj;
>     
>     //return hashCode() == other.hashCode();
>     return  (name != null ? name.equals(other.name) : other.name != null ?
> false : true)
>       && (rank != null ? rank.equals(other.rank) : other.rank != null ?
> false : true)
>       && (manager != null ? manager.equals(other.manager) : other.manager
> != null ? false : true);
>   }
>   
> }
> 
> 


-----
---
http://wiki.eclipse.org/User:James.sutherland.oracle.com James Sutherland 
http://www.eclipse.org/eclipselink/
 EclipseLink ,  http://www.oracle.com/technology/products/ias/toplink/
TopLink 
Wiki:  http://wiki.eclipse.org/EclipseLink EclipseLink , 
http://wiki.oracle.com/page/TopLink TopLink 
Forums:  http://forums.oracle.com/forums/forum.jspa?forumID=48 TopLink , 
http://www.nabble.com/EclipseLink-f26430.html EclipseLink 
Book:  http://en.wikibooks.org/wiki/Java_Persistence Java Persistence 
-- 
View this message in context: http://www.nabble.com/Cache-issue-with-EclipseLink--tp22440339p22455696.html
Sent from the EclipseLink - Users mailing list archive at Nabble.com.



Back to the top