[#1547] Support "optimistic locking" in UpdatableRecord.store()

This commit is contained in:
Lukas Eder 2012-07-14 16:14:10 +02:00
parent 1c42b05ad6
commit add0476024
9 changed files with 335 additions and 12 deletions

View File

@ -56,11 +56,11 @@ import org.jooq.InsertQuery;
import org.jooq.Record;
import org.jooq.SQLDialect;
import org.jooq.StoreQuery;
import org.jooq.Table;
import org.jooq.TableRecord;
import org.jooq.UDTRecord;
import org.jooq.UpdatableRecord;
import org.jooq.exception.DataAccessException;
import org.jooq.exception.DataChangedException;
import org.jooq.exception.InvalidResultException;
import org.jooq.test.BaseTest;
import org.jooq.test.jOOQAbstractTest;
@ -327,7 +327,7 @@ extends BaseTest<A, AP, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658,
// No ON DELETE CASCADE constraints for Sybase ASE
if (getDialect() == SQLDialect.ASE) {
create().truncate((Table) table("t_book_to_book_store")).execute();
create().truncate(table("t_book_to_book_store")).execute();
}
// Delete the modified record
@ -574,4 +574,69 @@ extends BaseTest<A, AP, B, S, B2S, BS, L, X, DATE, BOOL, D, T, U, I, IPK, T658,
assertEquals(2, record.deleteUsing(T785_NAME()));
assertEquals(0, create().fetch(T785()).size());
}
@Test
public void testStoreLocked() throws Exception {
jOOQAbstractTest.reset = false;
// Storing without changing shouldn't execute any queries
B book1 = create().fetchOne(TBook(), TBook_ID().equal(1));
assertEquals(0, book1.storeLocked());
assertEquals(0, book1.storeLocked());
// Succeed if there are no concurrency issues
book1.setValue(TBook_TITLE(), "New Title 1");
assertEquals(1, book1.storeLocked());
assertEquals("New Title 1", create().fetchOne(TBook(), TBook_ID().equal(1)).getValue(TBook_TITLE()));
// Get new books
B book2 = create().fetchOne(TBook(), TBook_ID().equal(1));
B book3 = create().fetchOne(TBook(), TBook_ID().equal(1));
// Still won't fail, but this will cause book3 to be stale
book2.setValue(TBook_TITLE(), "New Title 2");
assertEquals(1, book2.storeLocked());
assertEquals("New Title 2", create().fetchOne(TBook(), TBook_ID().equal(1)).getValue(TBook_TITLE()));
// Storing without changing shouldn't execute any queries
assertEquals(0, book3.storeLocked());
// This should fail as book3 is stale
book3.setValue(TBook_TITLE(), "New Title 3");
try {
book3.storeLocked();
fail();
}
catch (DataChangedException expected) {}
assertEquals("New Title 2", create().fetchOne(TBook(), TBook_ID().equal(1)).getValue(TBook_TITLE()));
// Refreshing first will work, though
book3.refresh();
book3.setValue(TBook_TITLE(), "New Title 3");
assertEquals(1, book3.storeLocked());
assertEquals("New Title 3", create().fetchOne(TBook(), TBook_ID().equal(1)).getValue(TBook_TITLE()));
// Get new books
B book4 = create().fetchOne(TBook(), TBook_ID().equal(1));
B book5 = create().fetchOne(TBook(), TBook_ID().equal(1));
// Delete the book
assertEquals(1, book4.delete());
// Storing without changing shouldn't execute any queries
assertEquals(0, book5.storeLocked());
// This should fail, as the database record no longer exists
book5.setValue(TBook_TITLE(), "New Title 5");
try {
book5.storeLocked();
fail();
}
catch (DataChangedException expected) {}
// Restore the book, then it should work
assertEquals(1, book4.storeLocked());
assertEquals(1, book5.storeLocked());
assertEquals("New Title 5", create().fetchOne(TBook(), TBook_ID().equal(1)).getValue(TBook_TITLE()));
}
}

View File

@ -1155,6 +1155,11 @@ public abstract class jOOQAbstractTest<
new CRUDTests(this).testUpdatablesUK();
}
@Test
public void testStoreLocked() throws Exception {
new CRUDTests(this).testStoreLocked();
}
@Test
public void testNonUpdatables() throws Exception {
new CRUDTests(this).testNonUpdatables();

View File

@ -35,9 +35,11 @@
*/
package org.jooq;
import java.sql.ResultSet;
import java.sql.Statement;
import org.jooq.exception.DataAccessException;
import org.jooq.exception.DataChangedException;
/**
* A record originating from a single table
@ -107,6 +109,47 @@ public interface TableRecord<R extends TableRecord<R>> extends Record {
*/
int storeUsing(TableField<R, ?>... keys) throws DataAccessException;
/**
* Store this record back to the database assuming an optimistic lock.
* <p>
* This performs the same action as {@link #storeUsing(TableField...)},
* except that if an <code>UPDATE</code> is performed, this record will be
* compared with the latest state in the database.
* <p>
* Note that in order to compare this record with the latest state, the
* database record will be locked pessimistically using a
* <code>SELECT .. FOR UPDATE</code> statement. Not all databases support
* the <code>FOR UPDATE</code> clause natively. Namely, the following
* databases will show slightly different behaviour:
* <ul>
* <li> {@link SQLDialect#CUBRID} and {@link SQLDialect#SQLSERVER}: jOOQ will
* try to lock the database record using JDBC's
* {@link ResultSet#TYPE_SCROLL_SENSITIVE} and
* {@link ResultSet#CONCUR_UPDATABLE}.</li>
* <li> {@link SQLDialect#SQLITE}: No pessimistic locking is possible. Client
* code must assure that no race-conditions can occur between jOOQ's
* checking of database record state and the actual <code>UPDATE</code></li>
* </ul>
* <p>
* See {@link LockProvider#setForUpdate(boolean)} for more details
* <p>
* Unlike {@link #storeUsing(TableField...)}, this will fail if several
* records are concerned.
*
* @param keys The key fields used for deciding whether to execute an
* <code>INSERT</code> or <code>UPDATE</code> statement. If an
* <code>UPDATE</code> statement is executed, they are also the
* key fields for the <code>UPDATE</code> statement's
* <code>WHERE</code> clause.
* @return The number of stored records.
* @throws DataAccessException if something went wrong executing the query
* @throws DataChangedException if the record has already been changed in
* the database
* @see #storeUsing(TableField...)
* @see LockProvider#setForUpdate(boolean)
*/
int storeLockedUsing(TableField<R, ?>... keys) throws DataAccessException, DataChangedException;
/**
* Deletes this record from the database, based on the value of the provided
* keys.

View File

@ -35,9 +35,11 @@
*/
package org.jooq;
import java.sql.ResultSet;
import java.sql.Statement;
import org.jooq.exception.DataAccessException;
import org.jooq.exception.DataChangedException;
/**
* A common interface for records that can be stored back to the database again.
@ -134,6 +136,41 @@ public interface UpdatableRecord<R extends UpdatableRecord<R>> extends Updatable
*/
int store() throws DataAccessException;
/**
* Store this record back to the database assuming an optimistic lock.
* <p>
* This performs the same action as {@link #store()}, except that if an
* <code>UPDATE</code> is performed, this record will be compared with the
* latest state in the database.
* <p>
* Note that in order to compare this record with the latest state, the
* database record will be locked pessimistically using a
* <code>SELECT .. FOR UPDATE</code> statement. Not all databases support
* the <code>FOR UPDATE</code> clause natively. Namely, the following
* databases will show slightly different behaviour:
* <ul>
* <li> {@link SQLDialect#CUBRID} and {@link SQLDialect#SQLSERVER}: jOOQ will
* try to lock the database record using JDBC's
* {@link ResultSet#TYPE_SCROLL_SENSITIVE} and
* {@link ResultSet#CONCUR_UPDATABLE}.</li>
* <li> {@link SQLDialect#SQLITE}: No pessimistic locking is possible. Client
* code must assure that no race-conditions can occur between jOOQ's
* checking of database record state and the actual <code>UPDATE</code></li>
* </ul>
* <p>
* See {@link LockProvider#setForUpdate(boolean)} for more details
*
* @return <code>1</code> if the record was stored to the database. <code>0
* </code> if storing was not necessary.
* @throws DataAccessException if something went wrong executing the query
* @throws DataChangedException if the record has already been changed in
* the database
* @see #store()
* @see #storeLockedUsing(TableField...)
* @see LockProvider#setForUpdate(boolean)
*/
int storeLocked() throws DataAccessException, DataChangedException;
/**
* Deletes this record from the database, based on the value of the primary
* key or main unique key.

View File

@ -0,0 +1,73 @@
/**
* Copyright (c) 2009-2012, Lukas Eder, lukas.eder@gmail.com
* All rights reserved.
*
* This software is licensed to you under the Apache License, Version 2.0
* (the "License"); You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* . Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* . Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* . Neither the name "jOOQ" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.jooq.exception;
import org.jooq.UpdatableRecord;
/**
* An error occurred while storing a record whose underlying data had already
* been changed
*
* @see UpdatableRecord#storeLocked()
* @author Lukas Eder
*/
public class DataChangedException extends DataAccessException {
/**
* Generated UID
*/
private static final long serialVersionUID = -6460945824599280420L;
/**
* Constructor for DataChangedException.
*
* @param message the detail message
*/
public DataChangedException(String message) {
super(message);
}
/**
* Constructor for DataChangedException.
*
* @param message the detail message
* @param cause the root cause (usually from using a underlying data access
* API such as JDBC)
*/
public DataChangedException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -54,7 +54,9 @@ import org.jooq.TableField;
import org.jooq.TableRecord;
import org.jooq.UpdatableRecord;
import org.jooq.UpdateQuery;
import org.jooq.exception.DataChangedException;
import org.jooq.exception.InvalidResultException;
import org.jooq.tools.StringUtils;
/**
* A record implementation for a record originating from a single table
@ -91,6 +93,15 @@ public class TableRecordImpl<R extends TableRecord<R>> extends TypeRecord<Table<
@Override
public final int storeUsing(TableField<R, ?>... keys) {
return storeUsing0(keys, false);
}
@Override
public final int storeLockedUsing(TableField<R, ?>... keys) {
return storeUsing0(keys, true);
}
private final int storeUsing0(TableField<R, ?>[] keys, boolean checkLocked) {
boolean executeUpdate = false;
for (TableField<R, ?> field : keys) {
@ -110,7 +121,7 @@ public class TableRecordImpl<R extends TableRecord<R>> extends TypeRecord<Table<
int result = 0;
if (executeUpdate) {
result = storeUpdate(keys);
result = storeUpdate(keys, checkLocked);
}
else {
result = storeInsert();
@ -169,7 +180,7 @@ public class TableRecordImpl<R extends TableRecord<R>> extends TypeRecord<Table<
}
@SuppressWarnings("unchecked")
private final int storeUpdate(TableField<R, ?>... keys) {
private final int storeUpdate(TableField<R, ?>[] keys, boolean checkIfChanged) {
UpdateQuery<R> update = create().updateQuery(getTable());
for (Field<?> field : getFields()) {
@ -182,9 +193,48 @@ public class TableRecordImpl<R extends TableRecord<R>> extends TypeRecord<Table<
addCondition(update, field);
}
// [#1547] If optimistic locking checks are requested, try fetching the
// Record again first, and compare this Record's original values with
// the ones in the database
if (checkIfChanged && update.isExecutable()) {
checkIfChanged(keys);
}
return update.execute();
}
private void checkIfChanged(TableField<R, ?>[] keys) {
SimpleSelectQuery<R> select = create().selectQuery(getTable());
for (Field<?> field : keys) {
addCondition(select, field);
}
// [#1547] SQLite doesn't support FOR UPDATE. CUBRID and SQL Server
// can simulate it, though!
if (create().getDialect() != SQLDialect.SQLITE) {
select.setForUpdate(true);
}
R record = select.fetchOne();
if (record == null) {
throw new DataChangedException("Database record no longer exists");
}
for (Field<?> field : getFields()) {
Value<?> thisValue = getValue0(field);
Value<?> thatValue = ((AbstractRecord) record).getValue0(field);
Object thisObject = thisValue.getOriginal();
Object thatObject = thatValue.getOriginal();
if (!StringUtils.equals(thisObject, thatObject)) {
throw new DataChangedException("Database record has been changed");
}
}
}
@Override
public final int deleteUsing(TableField<R, ?>... keys) {
try {

View File

@ -76,6 +76,11 @@ public class UpdatableRecordImpl<R extends UpdatableRecord<R>> extends TableReco
return storeUsing(getMainKey().getFieldsArray());
}
@Override
public final int storeLocked() {
return storeLockedUsing(getMainKey().getFieldsArray());
}
@Override
public final int delete() {
return deleteUsing(getMainKey().getFieldsArray());

View File

@ -37,28 +37,37 @@ package org.jooq.impl;
import java.io.Serializable;
/**
* @author Lukas Eder
*/
class Value<T> implements Serializable {
/**
* Generated UID
*/
private static final long serialVersionUID = -9065797545428164533L;
private final T original;
private T value;
private boolean isChanged;
Value(T value) {
this.original = value;
this.value = value;
}
T getValue() {
final T getValue() {
return value;
}
T getValue(T defaultValue) {
final T getValue(T defaultValue) {
return value != null ? value : defaultValue;
}
void setValue(T val) {
final T getOriginal() {
return original;
}
final void setValue(T val) {
// The flag is always set to true:
// [#945] To avoid bugs resulting from setting the same value twice
@ -67,7 +76,7 @@ class Value<T> implements Serializable {
setValue(val, false);
}
void setValue(T val, boolean primaryKey) {
final void setValue(T val, boolean primaryKey) {
// [#948] Force setting of val in most cases, to allow for controlling
// the number of necessary hard-parses, and to allow for explicitly
@ -91,14 +100,18 @@ class Value<T> implements Serializable {
value = val;
}
boolean isChanged() {
final boolean isChanged() {
return isChanged;
}
void setChanged(boolean isChanged) {
final void setChanged(boolean isChanged) {
this.isChanged = isChanged;
}
// ------------------------------------------------------------------------
// XXX: Object API
// ------------------------------------------------------------------------
@Override
public boolean equals(Object obj) {
if (obj instanceof Value<?>) {

View File

@ -1096,8 +1096,40 @@ public final class StringUtils {
private StringUtils() {}
// -------------------------------------------------------------------------
// Custom additions to StringUtils. The following methods are not part of
// Apache's commons-lang library
// XXX: The following methods are taken from ObjectUtils
// -------------------------------------------------------------------------
/**
* <p>Compares two objects for equality, where either one or both
* objects may be {@code null}.</p>
*
* <pre>
* ObjectUtils.equals(null, null) = true
* ObjectUtils.equals(null, "") = false
* ObjectUtils.equals("", null) = false
* ObjectUtils.equals("", "") = true
* ObjectUtils.equals(Boolean.TRUE, null) = false
* ObjectUtils.equals(Boolean.TRUE, "true") = false
* ObjectUtils.equals(Boolean.TRUE, Boolean.TRUE) = true
* ObjectUtils.equals(Boolean.TRUE, Boolean.FALSE) = false
* </pre>
*
* @param object1 the first object, may be {@code null}
* @param object2 the second object, may be {@code null}
* @return {@code true} if the values of both objects are the same
*/
public static boolean equals(Object object1, Object object2) {
if (object1 == object2) {
return true;
}
if ((object1 == null) || (object2 == null)) {
return false;
}
return object1.equals(object2);
}
// -------------------------------------------------------------------------
// XXX: The following methods are not part of Apache's commons-lang library
// -------------------------------------------------------------------------
/**