diff --git a/jOOQ-test/src/org/jooq/test/_/testcases/CRUDTests.java b/jOOQ-test/src/org/jooq/test/_/testcases/CRUDTests.java index ed601c8eea..e9070e5b0b 100644 --- a/jOOQ-test/src/org/jooq/test/_/testcases/CRUDTests.java +++ b/jOOQ-test/src/org/jooq/test/_/testcases/CRUDTests.java @@ -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> extends Record { */ int storeUsing(TableField... keys) throws DataAccessException; + /** + * Store this record back to the database assuming an optimistic lock. + *

+ * This performs the same action as {@link #storeUsing(TableField...)}, + * except that if an UPDATE is performed, this record will be + * compared with the latest state in the database. + *

+ * Note that in order to compare this record with the latest state, the + * database record will be locked pessimistically using a + * SELECT .. FOR UPDATE statement. Not all databases support + * the FOR UPDATE clause natively. Namely, the following + * databases will show slightly different behaviour: + *

+ *

+ * See {@link LockProvider#setForUpdate(boolean)} for more details + *

+ * Unlike {@link #storeUsing(TableField...)}, this will fail if several + * records are concerned. + * + * @param keys The key fields used for deciding whether to execute an + * INSERT or UPDATE statement. If an + * UPDATE statement is executed, they are also the + * key fields for the UPDATE statement's + * WHERE 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... keys) throws DataAccessException, DataChangedException; + /** * Deletes this record from the database, based on the value of the provided * keys. diff --git a/jOOQ/src/main/java/org/jooq/UpdatableRecord.java b/jOOQ/src/main/java/org/jooq/UpdatableRecord.java index a6e7f82a1b..ea3f49a901 100644 --- a/jOOQ/src/main/java/org/jooq/UpdatableRecord.java +++ b/jOOQ/src/main/java/org/jooq/UpdatableRecord.java @@ -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> extends Updatable */ int store() throws DataAccessException; + /** + * Store this record back to the database assuming an optimistic lock. + *

+ * This performs the same action as {@link #store()}, except that if an + * UPDATE is performed, this record will be compared with the + * latest state in the database. + *

+ * Note that in order to compare this record with the latest state, the + * database record will be locked pessimistically using a + * SELECT .. FOR UPDATE statement. Not all databases support + * the FOR UPDATE clause natively. Namely, the following + * databases will show slightly different behaviour: + *

+ *

+ * See {@link LockProvider#setForUpdate(boolean)} for more details + * + * @return 1 if the record was stored to the database. 0 + * 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. diff --git a/jOOQ/src/main/java/org/jooq/exception/DataChangedException.java b/jOOQ/src/main/java/org/jooq/exception/DataChangedException.java new file mode 100644 index 0000000000..eae01028eb --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/exception/DataChangedException.java @@ -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); + } +} diff --git a/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java b/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java index c9fcadcb32..497ae6e271 100644 --- a/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java @@ -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> extends TypeRecord... keys) { + return storeUsing0(keys, false); + } + + @Override + public final int storeLockedUsing(TableField... keys) { + return storeUsing0(keys, true); + } + + private final int storeUsing0(TableField[] keys, boolean checkLocked) { boolean executeUpdate = false; for (TableField field : keys) { @@ -110,7 +121,7 @@ public class TableRecordImpl> extends TypeRecord> extends TypeRecord... keys) { + private final int storeUpdate(TableField[] keys, boolean checkIfChanged) { UpdateQuery update = create().updateQuery(getTable()); for (Field field : getFields()) { @@ -182,9 +193,48 @@ public class TableRecordImpl> extends TypeRecord[] keys) { + SimpleSelectQuery 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... keys) { try { diff --git a/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java b/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java index 6e036f5c0f..c2e3deea0b 100644 --- a/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java @@ -76,6 +76,11 @@ public class UpdatableRecordImpl> extends TableReco return storeUsing(getMainKey().getFieldsArray()); } + @Override + public final int storeLocked() { + return storeLockedUsing(getMainKey().getFieldsArray()); + } + @Override public final int delete() { return deleteUsing(getMainKey().getFieldsArray()); diff --git a/jOOQ/src/main/java/org/jooq/impl/Value.java b/jOOQ/src/main/java/org/jooq/impl/Value.java index 821140204b..903a6f7785 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Value.java +++ b/jOOQ/src/main/java/org/jooq/impl/Value.java @@ -37,28 +37,37 @@ package org.jooq.impl; import java.io.Serializable; +/** + * @author Lukas Eder + */ class Value 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 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 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) { diff --git a/jOOQ/src/main/java/org/jooq/tools/StringUtils.java b/jOOQ/src/main/java/org/jooq/tools/StringUtils.java index 9800745e6c..48125cdd01 100644 --- a/jOOQ/src/main/java/org/jooq/tools/StringUtils.java +++ b/jOOQ/src/main/java/org/jooq/tools/StringUtils.java @@ -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 + // ------------------------------------------------------------------------- + + /** + *

Compares two objects for equality, where either one or both + * objects may be {@code null}.

+ * + *
+     * 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
+     * 
+ * + * @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 // ------------------------------------------------------------------------- /**