mirror of https://github.com/apache/openjpa.git
OPENJPA-2476: Fixed OptimisticLockEx due to rounding of a Timestamp
git-svn-id: https://svn.apache.org/repos/asf/openjpa/branches/2.1.x@1591536 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
7dec044a1d
commit
9221876ed7
|
@ -28,7 +28,6 @@ import java.io.OutputStream;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.sql.Array;
|
import java.sql.Array;
|
||||||
|
@ -96,6 +95,7 @@ import org.apache.openjpa.jdbc.schema.ForeignKey.FKMapKey;
|
||||||
import org.apache.openjpa.kernel.Filters;
|
import org.apache.openjpa.kernel.Filters;
|
||||||
import org.apache.openjpa.kernel.OpenJPAStateManager;
|
import org.apache.openjpa.kernel.OpenJPAStateManager;
|
||||||
import org.apache.openjpa.kernel.Seq;
|
import org.apache.openjpa.kernel.Seq;
|
||||||
|
import org.apache.openjpa.kernel.StateManagerImpl;
|
||||||
import org.apache.openjpa.kernel.exps.Path;
|
import org.apache.openjpa.kernel.exps.Path;
|
||||||
import org.apache.openjpa.lib.conf.Configurable;
|
import org.apache.openjpa.lib.conf.Configurable;
|
||||||
import org.apache.openjpa.lib.conf.Configuration;
|
import org.apache.openjpa.lib.conf.Configuration;
|
||||||
|
@ -1248,24 +1248,13 @@ public class DBDictionary
|
||||||
public void setTimestamp(PreparedStatement stmnt, int idx,
|
public void setTimestamp(PreparedStatement stmnt, int idx,
|
||||||
Timestamp val, Calendar cal, Column col)
|
Timestamp val, Calendar cal, Column col)
|
||||||
throws SQLException {
|
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());
|
val = StateManagerImpl.roundTimestamp(val, datePrecision);
|
||||||
valForStmnt.setNanos(nanos);
|
|
||||||
|
|
||||||
if (cal == null)
|
if (cal == null)
|
||||||
stmnt.setTimestamp(idx, valForStmnt);
|
stmnt.setTimestamp(idx, val);
|
||||||
else
|
else
|
||||||
stmnt.setTimestamp(idx, valForStmnt, cal);
|
stmnt.setTimestamp(idx, val, cal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -25,6 +25,7 @@ import java.io.ObjectOutput;
|
||||||
import java.io.ObjectOutputStream;
|
import java.io.ObjectOutputStream;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.lang.reflect.Modifier;
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.sql.Timestamp;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.BitSet;
|
import java.util.BitSet;
|
||||||
|
@ -34,7 +35,6 @@ import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
|
@ -157,6 +157,8 @@ public class StateManagerImpl
|
||||||
|
|
||||||
private transient ReentrantLock _instanceLock = null;
|
private transient ReentrantLock _instanceLock = null;
|
||||||
|
|
||||||
|
private int _datePrecision = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor; supply id, type metadata, and owning persistence manager.
|
* Constructor; supply id, type metadata, and owning persistence manager.
|
||||||
*/
|
*/
|
||||||
|
@ -729,13 +731,48 @@ public class StateManagerImpl
|
||||||
public void setNextVersion(Object version) {
|
public void setNextVersion(Object version) {
|
||||||
assignVersionField(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) {
|
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;
|
_version = version;
|
||||||
FieldMetaData vfield = _meta.getVersionField();
|
FieldMetaData vfield = _meta.getVersionField();
|
||||||
if (vfield != null)
|
if (vfield != null)
|
||||||
store(vfield.getIndex(), JavaTypes.convert(version,
|
store(vfield.getIndex(), JavaTypes.convert(version, vfield.getTypeCode()));
|
||||||
vfield.getTypeCode()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public PCState getPCState() {
|
public PCState getPCState() {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1377,9 +1377,36 @@ This value is usually one million, meaning that the database is able
|
||||||
to store time values with a precision of one millisecond. Particular
|
to store time values with a precision of one millisecond. Particular
|
||||||
databases may have more or less precision.
|
databases may have more or less precision.
|
||||||
OpenJPA will round all time values to this degree of precision
|
OpenJPA will round all time values to this degree of precision
|
||||||
before storing them in the database.
|
before storing them in the database. This property can be set to one
|
||||||
Defaults to 1000000.
|
of the following precisions:
|
||||||
</para>
|
</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>
|
||||||
<listitem id="DBDictionary.DateMillisecondBehavior">
|
<listitem id="DBDictionary.DateMillisecondBehavior">
|
||||||
<para>
|
<para>
|
||||||
|
|
Loading…
Reference in New Issue