/* * 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(); } }