You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openjpa.apache.org by ht...@apache.org on 2014/05/01 04:34:15 UTC

svn commit: r1591536 - in /openjpa/branches/2.1.x: openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/ openjpa-kernel/src/main/java/org/apache/openjpa/kernel/ openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/ openjpa-...

Author: hthomann
Date: Thu May  1 02:34:15 2014
New Revision: 1591536

URL: http://svn.apache.org/r1591536
Log:
OPENJPA-2476: Fixed OptimisticLockEx due to rounding of a Timestamp

Added:
    openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/
    openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/
    openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/TestTimestampOptLockEx.java
    openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/VersionTSEntity.java
Modified:
    openjpa/branches/2.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java
    openjpa/branches/2.1.x/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/StateManagerImpl.java
    openjpa/branches/2.1.x/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml

Modified: openjpa/branches/2.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java
URL: http://svn.apache.org/viewvc/openjpa/branches/2.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java?rev=1591536&r1=1591535&r2=1591536&view=diff
==============================================================================
--- openjpa/branches/2.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java (original)
+++ openjpa/branches/2.1.x/openjpa-jdbc/src/main/java/org/apache/openjpa/jdbc/sql/DBDictionary.java Thu May  1 02:34:15 2014
@@ -28,7 +28,6 @@ import java.io.OutputStream;
 import java.io.Reader;
 import java.io.StringReader;
 import java.io.Writer;
-import java.lang.reflect.Method;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.sql.Array;
@@ -96,6 +95,7 @@ import org.apache.openjpa.jdbc.schema.Fo
 import org.apache.openjpa.kernel.Filters;
 import org.apache.openjpa.kernel.OpenJPAStateManager;
 import org.apache.openjpa.kernel.Seq;
+import org.apache.openjpa.kernel.StateManagerImpl;
 import org.apache.openjpa.kernel.exps.Path;
 import org.apache.openjpa.lib.conf.Configurable;
 import org.apache.openjpa.lib.conf.Configuration;
@@ -1248,24 +1248,13 @@ public class DBDictionary
     public void setTimestamp(PreparedStatement stmnt, int idx,
         Timestamp val, Calendar cal, Column col)
         throws SQLException {
-        // ensure that we do not insert dates at a greater precision than
-        // that at which they will be returned by a SELECT
-        int rounded = (int) Math.round(val.getNanos() /
-            (double) datePrecision);
-        int nanos = rounded * datePrecision;
-        if (nanos > 999999999) {
-            // rollover to next second
-            val.setTime(val.getTime() + 1000);
-            nanos = 0;
-        }
         
-        Timestamp valForStmnt = new Timestamp(val.getTime());
-        valForStmnt.setNanos(nanos);
-
+        val = StateManagerImpl.roundTimestamp(val, datePrecision);
+        
         if (cal == null)
-            stmnt.setTimestamp(idx, valForStmnt);
+            stmnt.setTimestamp(idx, val);
         else
-            stmnt.setTimestamp(idx, valForStmnt, cal);
+            stmnt.setTimestamp(idx, val, cal);
     }
 
     /**

Modified: openjpa/branches/2.1.x/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/StateManagerImpl.java
URL: http://svn.apache.org/viewvc/openjpa/branches/2.1.x/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/StateManagerImpl.java?rev=1591536&r1=1591535&r2=1591536&view=diff
==============================================================================
--- openjpa/branches/2.1.x/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/StateManagerImpl.java (original)
+++ openjpa/branches/2.1.x/openjpa-kernel/src/main/java/org/apache/openjpa/kernel/StateManagerImpl.java Thu May  1 02:34:15 2014
@@ -25,6 +25,7 @@ import java.io.ObjectOutput;
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
 import java.lang.reflect.Modifier;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.BitSet;
@@ -34,7 +35,6 @@ import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Set;
 import java.util.TimeZone;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -157,6 +157,8 @@ public class StateManagerImpl
     
     private transient ReentrantLock _instanceLock = null;
 
+    private int _datePrecision = -1;
+        
     /**
      * Constructor; supply id, type metadata, and owning persistence manager.
      */
@@ -729,13 +731,48 @@ public class StateManagerImpl
     public void setNextVersion(Object version) {
         assignVersionField(version);
     }
+    
+    public static Timestamp roundTimestamp(Timestamp val, int datePrecision) {
+        // ensure that we do not insert dates at a greater precision than
+        // that at which they will be returned by a SELECT
+        int rounded = (int) Math.round(val.getNanos() / (double) datePrecision);
+        long time = val.getTime();
+        int nanos = rounded * datePrecision;
+        if (nanos > 999999999) {
+            // rollover to next second
+            time = time + 1000;
+            nanos = 0;
+        }
 
+        val = new Timestamp(time);
+        val.setNanos(nanos);
+        return val;
+    }
+    
     private void assignVersionField(Object version) {
+
+        if (version instanceof Timestamp) {
+            if (_datePrecision == -1) {
+                try {
+                    OpenJPAConfiguration conf = _broker.getConfiguration();
+                    Class confCls = Class.forName("org.apache.openjpa.jdbc.conf.JDBCConfigurationImpl");
+                    if (confCls.isAssignableFrom(conf.getClass())) {
+                        Object o = conf.getClass().getMethod("getDBDictionaryInstance").invoke(conf, (Object[]) null);
+                        _datePrecision = o.getClass().getField("datePrecision").getInt(o);
+                    } else {
+                        _datePrecision = 1000;
+                    }
+                } catch (Throwable e) {
+                    _datePrecision = 1000;
+                }
+            }
+
+            version = roundTimestamp((Timestamp) version, _datePrecision);
+        }
         _version = version;
         FieldMetaData vfield = _meta.getVersionField();
         if (vfield != null)
-            store(vfield.getIndex(), JavaTypes.convert(version,
-                vfield.getTypeCode()));
+            store(vfield.getIndex(), JavaTypes.convert(version, vfield.getTypeCode()));
     }
 
     public PCState getPCState() {

Added: openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/TestTimestampOptLockEx.java
URL: http://svn.apache.org/viewvc/openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/TestTimestampOptLockEx.java?rev=1591536&view=auto
==============================================================================
--- openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/TestTimestampOptLockEx.java (added)
+++ openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/TestTimestampOptLockEx.java Thu May  1 02:34:15 2014
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.    
+ */
+package org.apache.openjpa.persistence.optlockex.timestamp;
+
+import javax.persistence.EntityManager;
+import javax.persistence.EntityTransaction;
+
+import org.apache.openjpa.persistence.test.SingleEMFTestCase;
+
+/*
+ * Test create for JIRA OPENJPA-2476, see it for a very detailed
+ * description of the issue.
+ */
+public class TestTimestampOptLockEx extends SingleEMFTestCase {    
+
+    @Override
+    public void setUp() {
+        // By default we'd round a Timestamp to the nearest millisecond on Oracle (see DBDictionary.datePrecision 
+        // and DBDictionary.setTimestamp) and nearest microsecond on DB2 (see DB2Dictionary.datePrecision and 
+        // DBDictionary.setTimestamp) when sending the value to the db...if we change datePrecision to 1, we round to 
+        // the nearest nanosecond.  On DB2 and Oracle, it appears the default precision is microseconds but it seems
+        // DB2 truncates (no rounding) to microsecond for anything it is given with greater precision, whereas Oracle
+        // rounds.  So in the case of DB2, this test will pass if datePrecision=1, but still fails on Oracle.
+        // On the other hand, if we set the datePrecision to 1000000 and run against DB2, the test will fail.
+
+        // This test requires datePrecision to be set to the same precision as the Timestamp column.
+        // I've only been testing on Oracle and DB2 and not sure how other DBs treat a Timestamps precision
+        // by default.  In VersionTSEntity I use a Timestamp(3) but this is not supported on, at least, Derby
+        // and older versions of DB2...at this time I'll enable only on Oracle.
+        setSupportedDatabases(org.apache.openjpa.jdbc.sql.OracleDictionary.class);
+        if (isTestsDisabled()) {
+            return;
+        }        
+  
+        // Set datePrecision=1000000 for Oracle since we are using Timestamp(3)....on Oracle
+        // the default is 1000000 so we shouldn't need to set it, but lets set it to future
+        // proof the test.
+        super.setUp(DROP_TABLES, "openjpa.jdbc.DBDictionary", "datePrecision=1000000", VersionTSEntity.class);
+    }
+    
+    public void testUpdate() {
+        poplulate();
+        //This loop is necessary since we need a timestamp which has been rounded up
+        //by the database, or by OpenJPA such that the in-memory version of the Timestamp
+        //varies from that which is in the database.
+        for (int i = 0; i < 50000; i++) {
+            EntityManager em = emf.createEntityManager();
+            EntityTransaction tx = em.getTransaction();
+
+            // Find an existing VersionTSEntity:
+            // stored with microsecond precision, e.g. 2014-01-21 13:16:46.595428
+            VersionTSEntity t = em.find(VersionTSEntity.class, 1);
+
+            tx.begin();
+            t.setSomeInt(t.getSomeInt() + 1);
+            t = em.merge(t);
+
+            tx.commit();
+            // If this clear is removed the test works fine.
+            em.clear();
+
+            // Lets say at this point the 'in-memory' timestamp is: 2014-01-22 07:22:11.548778567.  What we 
+            // actually sent to the DB (via the previous merge) is by default rounded (see DBDictionary.setTimestamp) 
+            // to the nearest millisecond on Oracle (see DBDictionary.datePrecision) and nearest microsecond on 
+            // DB2 (see DB2Dictionary.datePrecision) when sending the value to the db.
+            // Therefore, what we actually send to the db is: 2014-01-22 07:22:11.548779 (for DB2) or 
+            // 2014-01-22 07:22:11.549 (for Oracle).  Notice in either case we rounded up.
+
+            // now, do a merge with the unchanged entity
+            tx = em.getTransaction();
+            tx.begin();
+
+            t = em.merge(t); // this results in a select of VersionTSEntity
+            
+            //This 'fixes' the issue (but customer doesn't really want to add this):
+            //em.refresh(t);
+            
+            // Here is where things get interesting.....an error will happen here when the timestamp 
+            // has been rounded up, as I'll explain:
+            // As part of this merge/commit, we select the timestamp from the db to get its value 
+            // (see method ColumnVersionStrategy.checkVersion below), i.e: 
+            // 'SELECT t0.updateTimestamp FROM VersionTSEntity t0 WHERE t0.id = ?'.  
+            // We then compare the 'in-memory' timestamp to that which we got back from the DB, i.e. on 
+            // DB2 we compare:
+            // in-mem:  2014-01-22 07:22:11.548778567
+            // from db: 2014-01-22 07:22:11.548779
+            // Because these do not 'compare' properly (the db version is greater), we throw the OptimisticLockEx!!
+            // For completeness, lets look at an example where the timestamp is as follows after the above 
+            // update: 2014-01-22 07:22:11.548771234.  We would send to DB2
+            // the following value: 2014-01-22 07:22:11.548771.  Then, as part of the very last merge/commit, we'd 
+            // compare:
+            // in-mem:  2014-01-22 07:22:11.548771234
+            // from db: 2014-01-22 07:22:11.548771
+            // These two would 'compare' properly (the db version is lesser), as such we would not throw an 
+            // OptLockEx and the test works fine.
+            tx.commit();
+            em.close();
+        }
+    }
+    
+    public void poplulate(){
+        EntityManager em = emf.createEntityManager();
+        EntityTransaction tx = em.getTransaction();
+        tx.begin();
+        VersionTSEntity r = new VersionTSEntity();
+        
+        r.setId(1L);
+        r.setSomeInt(0);
+        em.persist(r);
+        tx.commit();
+        em.close();        
+    }
+}

Added: openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/VersionTSEntity.java
URL: http://svn.apache.org/viewvc/openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/VersionTSEntity.java?rev=1591536&view=auto
==============================================================================
--- openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/VersionTSEntity.java (added)
+++ openjpa/branches/2.1.x/openjpa-persistence-jdbc/src/test/java/org/apache/openjpa/persistence/optlockex/timestamp/VersionTSEntity.java Thu May  1 02:34:15 2014
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.    
+ */
+package org.apache.openjpa.persistence.optlockex.timestamp;
+
+import java.io.Serializable;
+import java.sql.Timestamp;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Version;
+
+@Entity
+public class VersionTSEntity implements Serializable {
+
+    private static final long serialVersionUID = 2948711625184868242L;
+
+    @Id
+    private Long id;
+    
+    @Version
+    @Column(columnDefinition="TIMESTAMP(3)")
+    private Timestamp updateTimestamp;
+
+    private Integer someInt;
+
+    public Long getId() {
+        return this.id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Timestamp getUpdateTimestamp() {
+        return this.updateTimestamp;
+    }
+
+    public void setSomeInt(Integer someInt) {
+        this.someInt = someInt;
+        
+    }
+
+    public Integer getSomeInt() {
+        return someInt;
+    }
+}

Modified: openjpa/branches/2.1.x/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml
URL: http://svn.apache.org/viewvc/openjpa/branches/2.1.x/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml?rev=1591536&r1=1591535&r2=1591536&view=diff
==============================================================================
--- openjpa/branches/2.1.x/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml (original)
+++ openjpa/branches/2.1.x/openjpa-project/src/doc/manual/ref_guide_dbsetup.xml Thu May  1 02:34:15 2014
@@ -1377,9 +1377,36 @@ This value is usually one million, meani
 to store time values with a precision of one millisecond.  Particular
 databases may have more or less precision.
 OpenJPA will round all time values to this degree of precision
-before storing them in the database.
-Defaults to 1000000.
-                    </para>
+before storing them in the database.  This property can be set to one 
+of the following precisions:
+                </para>
+                <itemizedlist>
+                    <listitem>
+                        <para>
+<literal>DECI</literal>: 100000000
+                        </para>
+                    </listitem>
+                    <listitem>
+                        <para>
+<literal>CENIT</literal>: 10000000
+                        </para>
+                    </listitem>
+                    <listitem>
+                        <para>
+<literal>MILLI (default precision)</literal>: 1000000
+                        </para>
+                    </listitem>
+                    <listitem>
+                        <para>
+<literal>MICRO</literal>: 1000
+                        </para>
+                    </listitem>
+                    <listitem>
+                        <para>
+<literal>NANO (max precision)</literal>: 1
+                        </para>
+                    </listitem>
+                </itemizedlist>
                 </listitem>
                 <listitem id="DBDictionary.DateMillisecondBehavior">
                     <para>