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/trunk@1591541 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
4f23e3fc95
commit
3f33d76a34
|
@ -96,6 +96,7 @@ import org.apache.openjpa.jdbc.schema.ForeignKey.FKMapKey;
|
|||
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;
|
||||
|
@ -1288,24 +1289,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
@ -159,6 +160,8 @@ public class StateManagerImpl implements OpenJPAStateManager, Serializable {
|
|||
|
||||
private transient ReentrantLock _instanceLock = null;
|
||||
|
||||
private int _datePrecision = -1;
|
||||
|
||||
/**
|
||||
* <p>set to <code>false</code> to prevent the postLoad method from
|
||||
* sending lifecycle callback events.</p>
|
||||
|
@ -716,13 +719,48 @@ public class StateManagerImpl implements OpenJPAStateManager, Serializable {
|
|||
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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1405,9 +1405,36 @@ This value is usually one million, meaning that the database is able
|
|||
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>
|
||||
|
|
Loading…
Reference in New Issue