[#1547] Support "optimistic locking" in UpdatableRecord.store()
This commit is contained in:
parent
1c42b05ad6
commit
add0476024
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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<?>) {
|
||||
|
||||
@ -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
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user