[#3300] UpdatableRecord.store() executes INSERT instead of UPDATE when nullable primary keys are NULL, and the updatablePrimaryKeys setting is active

This commit is contained in:
Lukas Eder 2014-06-02 18:59:55 +02:00
parent 5d16d7dc37
commit 29f31bd91b
13 changed files with 103 additions and 298 deletions

View File

@ -86,6 +86,7 @@ xxxxxx xxxxxxxxxxxxxxxxxxxx
xxxxxx xxxxxxxxxxxxxxx
xxxxxx xxxxxxxxxxxxxxxxxxxxx
xxxxxx xxxxxxxxxxxxxxxxxxxx
xxxxxx xxxxxxxxxxxxxxxxxx
xxxxxx xxxxxxxxxxxxxxxxxxxxxxxx
xxxxxx xxxxxxxxxxxxxxx
@ -1085,7 +1086,60 @@ xxxxxx xxxxx xxxxxxxxxxxxx xxxxxxx xxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx xxxxxxxxxxx
xxxxxxxxxxxxxxx xxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxx xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
x
xxxxx
xxxxxx xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx x
xxxxxxxxx xxx xxxx x xxxxxx xxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxx xxxxxx x xxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxx xx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxx xx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xx xxxxxxx xxxxx xxxxxxx xx xxxxx
xxxxxxxxxxxxx
xxxxxxxxxxxxxxxx
xxxxxxxxxxxxx
xxxxxxxxxxxxxxxx
xxxxxxxxxxxxxx
xxxxxxxxxxxxx
xxxxxxxxxxxxxxxx
xxxxxxxxxxxxx
xxxxxxxxxxxxxxxx
xxxxxxxxxxxxxx
xxxxxxxxxxxxxxx xxxxxxxxxxxx
xxxxxxxxxxxxxxx xxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxx xxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxx
xx xxxxxxx xxxxx xxxxxxx xx xxxxx
xxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xx x xxxxxxxxxxxxxxx
xx x xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx xxxxxxxxxxxx
xxxxxxxxxxxxxxx xxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxx xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xx xxxxxxx xxxxx xxxxxxx xxxx xxxxx
xxxxxxx x xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xx x xxxxxxxxxxxxxxx
xx x xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
xxxxxxxxxxxxxxx
xxxxx xxxxxx x xxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxx
xxxxxxxxxxxxxxx xxxxxxxxxxx
xxxxxxxxxxxxxxx xxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxx xxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
x
xxxxx

View File

@ -554,7 +554,7 @@ public abstract class jOOQAbstractTest<
if (clean != null && clean.length > 0) {
for (Table<?> table : clean) {
try {
create().delete(table);
create().delete(table).execute();
}
catch (Exception e) {
e.printStackTrace();

View File

@ -1,67 +0,0 @@
/**
* Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com)
* All rights reserved.
*
* This work is dual-licensed
* - under the Apache Software License 2.0 (the "ASL")
* - under the jOOQ License and Maintenance Agreement (the "jOOQ License")
* =============================================================================
* You may choose which license applies to you:
*
* - If you're using this work with Open Source databases, you may choose
* either ASL or jOOQ License.
* - If you're using this work with at least one commercial database, you must
* choose jOOQ License
*
* For more information, please visit http://www.jooq.org/licenses
*
* Apache Software License 2.0:
* -----------------------------------------------------------------------------
* Licensed 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.
*
* jOOQ License and Maintenance Agreement:
* -----------------------------------------------------------------------------
* Data Geekery grants the Customer the non-exclusive, timely limited and
* non-transferable license to install and use the Software under the terms of
* the jOOQ License and Maintenance Agreement.
*
* This library is distributed with a LIMITED WARRANTY. See the jOOQ License
* and Maintenance Agreement for more details: http://www.jooq.org/licensing
*/
package org.jooq;
/**
* An <code>FunctionalInterface</code> that wraps transactional code.
*
* @author Lukas Eder
*/
public interface Transactional<T> {
/**
* Run the transactional code.
* <p>
* If this method completes normally, and this is not a nested transaction,
* then the transaction will be committed. If this method completes with an
* exception, then the transaction is rolled back to the beginning of this
* <code>Transactional</code>.
*
* @param configuration The <code>Configuration</code> in whose context the
* transaction is run.
* @return The outcome of the transaction.
* @throws Exception Any exception that will cause a rollback of the code
* contained in this transaction. If this is a nested
* transaction, the rollback may be performed only to the state
* before executing this <code>Transactional</code>.
*/
T run(Configuration configuration) throws Exception;
}

View File

@ -91,7 +91,7 @@ abstract class AbstractRecord extends AbstractStore implements Record {
final Object[] values;
final Object[] originals;
final BitSet changed;
// final BitSet def;
boolean fetched;
AbstractRecord(Collection<? extends Field<?>> fields) {
this(new RowImpl(fields));
@ -108,7 +108,6 @@ abstract class AbstractRecord extends AbstractStore implements Record {
this.values = new Object[size];
this.originals = new Object[size];
this.changed = new BitSet(size);
// this.def = new BitSet(size);
}
// ------------------------------------------------------------------------
@ -299,9 +298,10 @@ abstract class AbstractRecord extends AbstractStore implements Record {
private final <T> void setValue(int index, Field<T> field, T value) {
// Relevant issues documenting this method's behaviour:
// [#945]
// [#948]
// [#979]
// [#945] Avoid bugs resulting from setting the same value twice
// [#948] To allow for controlling the number of hard-parses
// To allow for explicitly overriding default values
// [#979] Avoid modifying chnaged flag on unchanged primary key values
UniqueKey<?> key = getPrimaryKey();
@ -347,6 +347,8 @@ abstract class AbstractRecord extends AbstractStore implements Record {
}
final void setValues(Field<?>[] fields, AbstractRecord record) {
fetched = record.fetched;
for (Field<?> field : fields) {
int targetIndex = indexOrFail(fieldsRow(), field);
int sourceIndex = indexOrFail(record.fieldsRow(), field);
@ -385,7 +387,7 @@ abstract class AbstractRecord extends AbstractStore implements Record {
*/
@Override
public Record original() {
return Utils.newRecord((Class<AbstractRecord>) getClass(), fields.fields.fields, configuration())
return Utils.newRecord(fetched, (Class<AbstractRecord>) getClass(), fields.fields.fields, configuration())
.operate(new RecordOperation<AbstractRecord, RuntimeException>() {
@Override
@ -544,11 +546,11 @@ abstract class AbstractRecord extends AbstractStore implements Record {
@Override
public final <R extends Record> R into(Table<R> table) {
return Utils.newRecord(table, configuration()).operate(new TransferRecordState<R>());
return Utils.newRecord(fetched, table, configuration()).operate(new TransferRecordState<R>());
}
final <R extends Record> R intoRecord(Class<R> type) {
return Utils.newRecord(type, fields(), configuration()).operate(new TransferRecordState<R>());
return Utils.newRecord(fetched, type, fields(), configuration()).operate(new TransferRecordState<R>());
}
private class TransferRecordState<R extends Record> implements RecordOperation<R, MappingException> {

View File

@ -566,7 +566,7 @@ public abstract class AbstractRoutine<T> extends AbstractQueryPart implements Ro
xxxx xxxxxxx x
xx xxxxxxxx xx xxxxxxxxxxxxx x
xxxxxxxxxxxx xxxxxx x xxxxx
xxxxxxxxxxxxxxxxxxx xxxxxxx xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxx xxxxxxxx xxxxxxx xxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxx
x

View File

@ -444,7 +444,7 @@ abstract class AbstractStoreQuery<R extends Record> extends AbstractQuery implem
if (returning.size() == 1 && new Fields<Record>(returning).field(field) != null) {
for (final Number id : ids) {
getReturnedRecords().add(
Utils.newRecord(into, configuration)
Utils.newRecord(true, into, configuration)
.operate(new RecordOperation<R, RuntimeException>() {
@Override

View File

@ -1408,7 +1408,7 @@ class CursorImpl<R extends Record> implements Cursor<R> {
rs.updateRow();
}
record = Utils.newRecord((Class<AbstractRecord>) type, fields, ctx.configuration())
record = Utils.newRecord(true, (Class<AbstractRecord>) type, fields, ctx.configuration())
.operate(initialiser);
rows++;

View File

@ -131,7 +131,7 @@ class DefaultBindContext extends AbstractBindContext {
xx [/pro] */
// [#1126] Oracle's UDTs need to be bound with their type name
if (UDTRecord.class.isAssignableFrom(type)) {
String typeName = Utils.newRecord((Class<UDTRecord<?>>) type)
String typeName = Utils.newRecord(false, (Class<UDTRecord<?>>) type)
.<RuntimeException>operate(null)
.getUDT()
.getName();

View File

@ -1673,7 +1673,7 @@ public class DefaultDSLContext implements DSLContext, Serializable {
@Override
public Record newRecord(Field<?>... fields) {
return Utils.newRecord(RecordImpl.class, fields, configuration).<RuntimeException>operate(null);
return Utils.newRecord(false, RecordImpl.class, fields, configuration).<RuntimeException>operate(null);
}
// [jooq-tools] START [newRecord]
@ -1814,17 +1814,17 @@ public class DefaultDSLContext implements DSLContext, Serializable {
@Override
public <R extends UDTRecord<R>> R newRecord(UDT<R> type) {
return Utils.newRecord(type, configuration).<RuntimeException>operate(null);
return Utils.newRecord(false, type, configuration).<RuntimeException>operate(null);
}
@Override
public <R extends Record> R newRecord(Table<R> table) {
return Utils.newRecord(table, configuration).<RuntimeException>operate(null);
return Utils.newRecord(false, table, configuration).<RuntimeException>operate(null);
}
@Override
public <R extends Record> R newRecord(Table<R> table, final Object source) {
return Utils.newRecord(table, configuration)
return Utils.newRecord(false, table, configuration)
.operate(new RecordOperation<R, RuntimeException>() {
@Override

View File

@ -181,6 +181,7 @@ public class TableRecordImpl<R extends TableRecord<R>> extends AbstractRecord im
}
changed(false);
fetched = true;
}
return result;

View File

@ -141,19 +141,13 @@ public class UpdatableRecordImpl<R extends UpdatableRecord<R>> extends TableReco
TableField<R, ?>[] keys = getPrimaryKey().getFieldsArray();
boolean executeUpdate = false;
for (TableField<R, ?> field : keys) {
// [#2764] If primary key values are allowed to be changed,
// inserting is only possible without prior loading of pk values
if (updatablePrimaryKeys(settings(this))) {
if (original(field) == null) {
executeUpdate = false;
break;
}
}
// [#2764] Primary key value changes are interpreted as record copies
else {
// [#2764] If primary key values are allowed to be changed,
// inserting is only possible without prior loading of pk values
if (updatablePrimaryKeys(settings(this))) {
executeUpdate = fetched;
}
else {
for (TableField<R, ?> field : keys) {
// If any primary key value is null or changed
if (changed(field) ||
@ -163,10 +157,10 @@ public class UpdatableRecordImpl<R extends UpdatableRecord<R>> extends TableReco
executeUpdate = false;
break;
}
}
// Otherwise, updates are possible
executeUpdate = true;
// Otherwise, updates are possible
executeUpdate = true;
}
}
int result = 0;
@ -316,7 +310,7 @@ public class UpdatableRecordImpl<R extends UpdatableRecord<R>> extends TableReco
@Override
public final R copy() {
return Utils.newRecord(getTable(), configuration())
return Utils.newRecord(fetched, getTable(), configuration())
.operate(new RecordOperation<R, RuntimeException>() {
@Override

View File

@ -342,51 +342,51 @@ final class Utils {
/**
* Create a new record
*/
static final <R extends Record> RecordDelegate<R> newRecord(Class<R> type) {
return newRecord(type, null);
static final <R extends Record> RecordDelegate<R> newRecord(boolean fetched, Class<R> type) {
return newRecord(fetched, type, null);
}
/**
* Create a new record
*/
static final <R extends Record> RecordDelegate<R> newRecord(Class<R> type, Field<?>[] fields) {
return newRecord(type, fields, null);
static final <R extends Record> RecordDelegate<R> newRecord(boolean fetched, Class<R> type, Field<?>[] fields) {
return newRecord(fetched, type, fields, null);
}
/**
* Create a new record
*/
static final <R extends Record> RecordDelegate<R> newRecord(Table<R> type) {
return newRecord(type, null);
static final <R extends Record> RecordDelegate<R> newRecord(boolean fetched, Table<R> type) {
return newRecord(fetched, type, null);
}
/**
* Create a new record
*/
@SuppressWarnings("unchecked")
static final <R extends Record> RecordDelegate<R> newRecord(Table<R> type, Configuration configuration) {
return (RecordDelegate<R>) newRecord(type.getRecordType(), type.fields(), configuration);
static final <R extends Record> RecordDelegate<R> newRecord(boolean fetched, Table<R> type, Configuration configuration) {
return (RecordDelegate<R>) newRecord(fetched, type.getRecordType(), type.fields(), configuration);
}
/**
* Create a new UDT record
*/
static final <R extends UDTRecord<R>> RecordDelegate<R> newRecord(UDT<R> type) {
return newRecord(type, null);
static final <R extends UDTRecord<R>> RecordDelegate<R> newRecord(boolean fetched, UDT<R> type) {
return newRecord(fetched, type, null);
}
/**
* Create a new UDT record
*/
static final <R extends UDTRecord<R>> RecordDelegate<R> newRecord(UDT<R> type, Configuration configuration) {
return newRecord(type.getRecordType(), type.fields(), configuration);
static final <R extends UDTRecord<R>> RecordDelegate<R> newRecord(boolean fetched, UDT<R> type, Configuration configuration) {
return newRecord(fetched, type.getRecordType(), type.fields(), configuration);
}
/**
* Create a new record
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
static final <R extends Record> RecordDelegate<R> newRecord(Class<R> type, Field<?>[] fields, Configuration configuration) {
static final <R extends Record> RecordDelegate<R> newRecord(boolean fetched, Class<R> type, Field<?>[] fields, Configuration configuration) {
try {
R record;
@ -402,6 +402,10 @@ final class Utils {
record = Reflect.accessible(type.getDeclaredConstructor()).newInstance();
}
// [#3300] Records that were fetched from the database
if (record instanceof AbstractRecord)
((AbstractRecord) record).fetched = fetched;
return new RecordDelegate<R>(configuration, record);
}
catch (Exception e) {
@ -2989,7 +2993,7 @@ final class Utils {
return null;
}
return Utils.newRecord((Class<UDTRecord<?>>) type)
return Utils.newRecord(true, (Class<UDTRecord<?>>) type)
.operate(new RecordOperation<UDTRecord<?>, SQLException>() {
@Override

View File

@ -1,183 +0,0 @@
/**
* Copyright (c) 2009-2014, Data Geekery GmbH (http://www.datageekery.com)
* All rights reserved.
*
* This work is dual-licensed
* - under the Apache Software License 2.0 (the "ASL")
* - under the jOOQ License and Maintenance Agreement (the "jOOQ License")
* =============================================================================
* You may choose which license applies to you:
*
* - If you're using this work with Open Source databases, you may choose
* either ASL or jOOQ License.
* - If you're using this work with at least one commercial database, you must
* choose jOOQ License
*
* For more information, please visit http://www.jooq.org/licenses
*
* Apache Software License 2.0:
* -----------------------------------------------------------------------------
* Licensed 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.
*
* jOOQ License and Maintenance Agreement:
* -----------------------------------------------------------------------------
* Data Geekery grants the Customer the non-exclusive, timely limited and
* non-transferable license to install and use the Software under the terms of
* the jOOQ License and Maintenance Agreement.
*
* This library is distributed with a LIMITED WARRANTY. See the jOOQ License
* and Maintenance Agreement for more details: http://www.jooq.org/licensing
*/
package org.jooq.impl;
import java.io.Serializable;
/**
* @author Lukas Eder
*/
class Value0<T> implements Serializable {
/**
* Generated UID
*/
private static final long serialVersionUID = -9065797545428164533L;
private T original;
private T value;
private boolean isChanged;
Value0(T value) {
this(value, value, false);
}
Value0(T value, T original, boolean isChanged) {
this.value = value;
this.original = original;
this.isChanged = isChanged;
}
final T getValue() {
return value;
}
final T getValue(T defaultValue) {
return value != null ? value : defaultValue;
}
final T getOriginal() {
return original;
}
@SuppressWarnings("unchecked")
final void intern() {
// [#2177] Future versions of jOOQ may optimise this type check by
// performing type-decisions outside of Value
if (value instanceof String) {
value = (T) ((String) value).intern();
}
}
final void setValue(T val) {
// The flag is always set to false:
// [#945] To avoid bugs resulting from setting the same value twice
// [#948] To allow for controlling the number of hard-parses
// To allow for explicitly overriding default values
setValue(val, false);
}
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
// overriding default values with null
if (!primaryKey) {
isChanged = true;
}
// [#979] Avoid modifying isChanged on unchanged primary key values
else {
// [#945] Be sure that isChanged is never reset to false
if (value == null) {
isChanged = isChanged || (val != null);
}
else {
isChanged = isChanged || (!value.equals(val));
}
}
value = val;
}
final boolean isChanged() {
return isChanged;
}
final void setChanged(boolean isChanged) {
this.isChanged = isChanged;
// [#1995] If a value is meant to be "unchanged", the "original" should
// match the supposedly "unchanged" value.
if (!isChanged) {
original = value;
}
}
final void reset() {
isChanged = false;
value = original;
}
// ------------------------------------------------------------------------
// XXX: Object API
// ------------------------------------------------------------------------
//
// @Override
// public boolean equals(Object obj) {
// if (this == obj) {
// return true;
// }
//
// if (obj instanceof Value0<?>) {
// Value0<?> other = (Value0<?>) obj;
//
// if (value == null) {
// return other.getValue() == null;
// }
//
// return value.equals(other.getValue());
// }
//
// return false;
// }
//
// @Override
// public int hashCode() {
// if (value == null) {
// return 0;
// }
//
// return value.hashCode();
// }
@Override
public String toString() {
if (isChanged) {
return "*" + value;
}
else {
return "" + value;
}
}
}