[jOOQ/jOOQ#5394] Add a way to load POJOs into Records without setting all the changed flags to true

This commit is contained in:
Lukas Eder 2024-09-12 17:55:09 +02:00
parent 34b560bf82
commit a70b920c2f
19 changed files with 186 additions and 33 deletions

View File

@ -621,6 +621,12 @@ public class MetaExtensions {
setRecordClass(o);
}
public void recordTypeClass(Action<MatcherRuleExtension> action) {
MatcherRuleExtension o = objects.newInstance(MatcherRuleExtension.class, objects);
action.execute(o);
setRecordTypeClass(o);
}
public void interfaceClass(Action<MatcherRuleExtension> action) {
MatcherRuleExtension o = objects.newInstance(MatcherRuleExtension.class, objects);
action.execute(o);

View File

@ -456,6 +456,13 @@ fun MatchersUDTType.recordClass(block: MatcherRule.() -> Unit) {
block(recordClass)
}
fun MatchersUDTType.recordTypeClass(block: MatcherRule.() -> Unit) {
if (recordTypeClass == null)
recordTypeClass = MatcherRule()
block(recordTypeClass)
}
fun MatchersUDTType.interfaceClass(block: MatcherRule.() -> Unit) {
if (interfaceClass == null)
interfaceClass = MatcherRule()

View File

@ -37,6 +37,8 @@
*/
package org.jooq;
import org.jooq.conf.Settings;
import org.jetbrains.annotations.*;
// ...
@ -339,9 +341,11 @@ public interface InsertSetStep<R extends Record> {
* Set values in the <code>INSERT</code> statement.
* <p>
* This is the same as calling {@link #set(Map)} with the argument record
* treated as a <code>Map&lt;Field&lt;?&gt;, Object&gt;</code>, except that the
* {@link Record#touched()} flags are taken into consideration in order to
* update only touched values.
* treated as a <code>Map&lt;Field&lt;?&gt;, Object&gt;</code>, except that
* the {@link Record#touched()} flags (or {@link Record#modified()} flags,
* depending on the query's {@link Settings#getRecordDirtyTracking()}
* configuration) are taken into consideration in order to update only
* touched (or modified) values.
*
* @see #set(Map)
*/

View File

@ -56,6 +56,8 @@ import static org.jooq.SQLDialect.POSTGRES;
import java.util.Map;
import org.jooq.conf.Settings;
import org.jetbrains.annotations.NotNull;
/**
@ -153,9 +155,11 @@ public interface MergeMatchedSetStep<R extends Record> {
* statement's <code>WHEN MATCHED</code> clause.
* <p>
* This is the same as calling {@link #set(Map)} with the argument record
* treated as a <code>Map&lt;Field&lt;?&gt;, Object&gt;</code>, except that the
* {@link Record#touched()} flags are taken into consideration in order to
* update only touched values.
* treated as a <code>Map&lt;Field&lt;?&gt;, Object&gt;</code>, except that
* the {@link Record#touched()} flags (or {@link Record#modified()} flags,
* depending on the query's {@link Settings#getRecordDirtyTracking()}
* configuration) are taken into consideration in order to update only
* touched (or modified) values.
*
* @see #set(Map)
*/

View File

@ -739,7 +739,9 @@ public interface Record extends Fields, Attachable, Comparable<Record>, Formatta
* from the database.
* <p>
* When a record is {@link #modified()}, then it has always been
* {@link #touched()} as well.
* {@link #touched()} as well. Unlike the {@link #touched()} property, this
* property cannot be set and is derived only from the comparison between
* this record and the {@link #original()} record.
*
* @see #original()
* @see #modified(Field)
@ -749,11 +751,13 @@ public interface Record extends Fields, Attachable, Comparable<Record>, Formatta
boolean modified();
/**
* Check if a field's value has been modified since the record was created or
* fetched from the database, using {@link #field(Field)} for lookup.
* Check if a field's value has been modified since the record was created
* or fetched from the database, using {@link #field(Field)} for lookup.
* <p>
* When a record is {@link #modified()}, then it has always been
* {@link #touched()} as well.
* {@link #touched()} as well. Unlike the {@link #touched(Field)} property,
* this property cannot be set and is derived only from the comparison
* between #get(Field) and {@link #original(Field)} values.
*
* @see #modified()
* @see #original(Field)
@ -761,11 +765,13 @@ public interface Record extends Fields, Attachable, Comparable<Record>, Formatta
boolean modified(Field<?> field);
/**
* Check if a field's value has been modified since the record was created or
* fetched from the database, using {@link #field(int)} for lookup.
* Check if a field's value has been modified since the record was created
* or fetched from the database, using {@link #field(int)} for lookup.
* <p>
* When a record is {@link #modified()}, then it has always been
* {@link #touched()} as well.
* {@link #touched()} as well. Unlike the {@link #touched(int)} property,
* this property cannot be set and is derived only from the comparison
* between #get(int) and {@link #original(int)} values.
*
* @param fieldIndex The 0-based field index in this record.
* @see #modified()
@ -774,11 +780,13 @@ public interface Record extends Fields, Attachable, Comparable<Record>, Formatta
boolean modified(int fieldIndex);
/**
* Check if a field's value has been modified since the record was created or
* fetched from the database, using {@link #field(String)} for lookup.
* Check if a field's value has been modified since the record was created
* or fetched from the database, using {@link #field(String)} for lookup.
* <p>
* When a record is {@link #modified()}, then it has always been
* {@link #touched()} as well.
* {@link #touched()} as well. Unlike the {@link #touched(String)} property,
* this property cannot be set and is derived only from the comparison
* between #get(String) and {@link #original(String)} values.
*
* @see #modified()
* @see #original(String)
@ -790,7 +798,9 @@ public interface Record extends Fields, Attachable, Comparable<Record>, Formatta
* fetched from the database, using {@link #field(Name)} for lookup.
* <p>
* When a record is {@link #modified()}, then it has always been
* {@link #touched()} as well.
* {@link #touched()} as well. Unlike the {@link #touched(Name)} property,
* this property cannot be set and is derived only from the comparison
* between #get(Name) and {@link #original(Name)} values.
*
* @see #modified()
* @see #original(Name)

View File

@ -39,6 +39,7 @@ package org.jooq;
import java.util.Collection;
import org.jooq.conf.RecordDirtyTracking;
import org.jooq.conf.Settings;
import org.jooq.exception.DataAccessException;
@ -70,7 +71,9 @@ public interface TableRecord<R extends TableRecord<R>> extends QualifiedRecord<R
* If you want to enforce re-insertion this record's values, regardless if
* the values in this record were touched, you can explicitly set the
* touched flags for all values with {@link #touched(boolean)} or for single
* values with {@link #touched(Field, boolean)}, prior to insertion.
* values with {@link #touched(Field, boolean)}, prior to insertion, if
* {@link Settings#getRecordDirtyTracking()} is set to
* {@link RecordDirtyTracking#TOUCHED}
*
* @return <code>1</code> if the record was stored to the database. <code>0
* </code> if storing was not necessary and

View File

@ -37,6 +37,8 @@
*/
package org.jooq;
import org.jooq.conf.Settings;
import org.jetbrains.annotations.*;
@ -128,9 +130,11 @@ public interface UpdateSetStep<R extends Record> {
* Set a value for a field in the <code>UPDATE</code> statement.
* <p>
* This is the same as calling {@link #set(Map)} with the argument record
* treated as a <code>Map&lt;Field&lt;?&gt;, Object&gt;</code>, except that the
* {@link Record#touched()} flags are taken into consideration in order to
* update only touched values.
* treated as a <code>Map&lt;Field&lt;?&gt;, Object&gt;</code>, except that
* the {@link Record#touched()} flags (or {@link Record#modified()} flags,
* depending on the query's {@link Settings#getRecordDirtyTracking()}
* configuration) are taken into consideration in order to update only
* touched (or modified) values.
*
* @see #set(Map)
*/

View File

@ -0,0 +1,37 @@
package org.jooq.conf;
import jakarta.xml.bind.annotation.XmlEnum;
import jakarta.xml.bind.annotation.XmlType;
/**
* <p>Java class for RecordDirtyTracking.
*
* <p>The following schema fragment specifies the expected content contained within this class.
* <pre>
* &lt;simpleType name="RecordDirtyTracking"&gt;
* &lt;restriction base="{http://www.w3.org/2001/XMLSchema}string"&gt;
* &lt;enumeration value="TOUCHED"/&gt;
* &lt;enumeration value="MODIFIED"/&gt;
* &lt;/restriction&gt;
* &lt;/simpleType&gt;
* </pre>
*
*/
@XmlType(name = "RecordDirtyTracking")
@XmlEnum
public enum RecordDirtyTracking {
TOUCHED,
MODIFIED;
public String value() {
return name();
}
public static RecordDirtyTracking fromValue(String v) {
return valueOf(v);
}
}

View File

@ -378,6 +378,9 @@ public class Settings
@XmlElement(defaultValue = "NEVER")
@XmlSchemaType(name = "string")
protected UpdateUnchangedRecords updateUnchangedRecords = UpdateUnchangedRecords.NEVER;
@XmlElement(defaultValue = "TOUCHED")
@XmlSchemaType(name = "string")
protected RecordDirtyTracking recordDirtyTracking = RecordDirtyTracking.TOUCHED;
@XmlElement(defaultValue = "false")
protected Boolean updatablePrimaryKeys = false;
@XmlElement(defaultValue = "true")
@ -5175,6 +5178,22 @@ public class Settings
this.updateUnchangedRecords = value;
}
/**
* Whether {@link org.jooq.UpdatableRecord#store()} and related calls should be based on {@link org.jooq.Record#touched()} or {@link org.jooq.Record#modified()} semantics. This also affects copying records into explicit statements.
*
*/
public RecordDirtyTracking getRecordDirtyTracking() {
return recordDirtyTracking;
}
/**
* Whether {@link org.jooq.UpdatableRecord#store()} and related calls should be based on {@link org.jooq.Record#touched()} or {@link org.jooq.Record#modified()} semantics. This also affects copying records into explicit statements.
*
*/
public void setRecordDirtyTracking(RecordDirtyTracking value) {
this.recordDirtyTracking = value;
}
/**
* Whether primary key values are deemed to be "updatable" in jOOQ.
* <p>
@ -8865,6 +8884,15 @@ public class Settings
return this;
}
/**
* Whether {@link org.jooq.UpdatableRecord#store()} and related calls should be based on {@link org.jooq.Record#touched()} or {@link org.jooq.Record#modified()} semantics. This also affects copying records into explicit statements.
*
*/
public Settings withRecordDirtyTracking(RecordDirtyTracking value) {
setRecordDirtyTracking(value);
return this;
}
/**
* Whether primary key values are deemed to be "updatable" in jOOQ.
* <p>
@ -9810,6 +9838,7 @@ public class Settings
builder.append("attachRecords", attachRecords);
builder.append("insertUnchangedRecords", insertUnchangedRecords);
builder.append("updateUnchangedRecords", updateUnchangedRecords);
builder.append("recordDirtyTracking", recordDirtyTracking);
builder.append("updatablePrimaryKeys", updatablePrimaryKeys);
builder.append("reflectionCaching", reflectionCaching);
builder.append("cacheRecordMappers", cacheRecordMappers);
@ -11270,6 +11299,15 @@ public class Settings
return false;
}
}
if (recordDirtyTracking == null) {
if (other.recordDirtyTracking!= null) {
return false;
}
} else {
if (!recordDirtyTracking.equals(other.recordDirtyTracking)) {
return false;
}
}
if (updatablePrimaryKeys == null) {
if (other.updatablePrimaryKeys!= null) {
return false;
@ -12148,6 +12186,7 @@ public class Settings
result = ((prime*result)+((attachRecords == null)? 0 :attachRecords.hashCode()));
result = ((prime*result)+((insertUnchangedRecords == null)? 0 :insertUnchangedRecords.hashCode()));
result = ((prime*result)+((updateUnchangedRecords == null)? 0 :updateUnchangedRecords.hashCode()));
result = ((prime*result)+((recordDirtyTracking == null)? 0 :recordDirtyTracking.hashCode()));
result = ((prime*result)+((updatablePrimaryKeys == null)? 0 :updatablePrimaryKeys.hashCode()));
result = ((prime*result)+((reflectionCaching == null)? 0 :reflectionCaching.hashCode()));
result = ((prime*result)+((cacheRecordMappers == null)? 0 :cacheRecordMappers.hashCode()));

View File

@ -47,6 +47,7 @@ import static org.jooq.conf.SettingsTools.renderLocale;
import static org.jooq.impl.DSL.insertInto;
import static org.jooq.impl.DSL.name;
import static org.jooq.impl.DSL.table;
import static org.jooq.impl.Tools.recordDirtyTrackingPredicate;
import static org.jooq.tools.StringUtils.abbreviate;
import static org.jooq.tools.StringUtils.leftPad;
import static org.jooq.tools.StringUtils.rightPad;
@ -163,6 +164,7 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
int size = fields.size();
final int[] decimalPlaces = new int[size];
final int[] widths = new int[size];
ObjIntPredicate<Record> dirty = recordDirtyTrackingPredicate(this);
for (int index = 0; index < size; index++) {
if (Number.class.isAssignableFrom(fields.field(index).getType())) {
@ -173,7 +175,7 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
// Collect all decimal places for the column values
for (R record : buffer)
decimalPlacesList.add(decimalPlaces(format0(record.get(index), record.touched(index), true)));
decimalPlacesList.add(decimalPlaces(format0(record.get(index), dirty.test(record, index), true)));
// Find max
decimalPlaces[index] = Collections.max(decimalPlacesList);
@ -197,7 +199,7 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
// Add column values width
for (R record : buffer) {
String value = format0(record.get(index), record.touched(index), true);
String value = format0(record.get(index), dirty.test(record, index), true);
// Align number values before width is calculated
if (isNumCol)
@ -283,7 +285,7 @@ abstract class AbstractResult<R extends Record> extends AbstractFormattable impl
StringUtils.replace(
StringUtils.replace(
StringUtils.replace(
format0(record.get(index), record.touched(index), true), "\n", "{lf}"
format0(record.get(index), dirty.test(record, index), true), "\n", "{lf}"
), "\r", "{cr}"
), "\t", "{tab}"
);

View File

@ -37,6 +37,8 @@
*/
package org.jooq.impl;
import static org.jooq.impl.Tools.recordDirtyTrackingPredicate;
import java.util.Map;
import org.jooq.Configuration;
@ -72,8 +74,10 @@ implements
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public final void setRecord(R record) {
ObjIntPredicate<Record> dirty = recordDirtyTrackingPredicate(this);
for (int i = 0; i < record.size(); i++)
if (record.touched(i))
if (dirty.test(record, i))
addValue((Field) record.field(i), record.get(i));
}

View File

@ -1239,7 +1239,7 @@ final class InsertImpl<R extends Record, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10
@Override
public final InsertImpl set(Record record) {
return set(Tools.mapOfTouchedValues(record));
return set(Tools.mapOfTouchedValues(this, record));
}
@Override

View File

@ -104,6 +104,7 @@ import static org.jooq.impl.Tools.flattenCollection;
import static org.jooq.impl.Tools.map;
import static org.jooq.impl.Tools.orElse;
import static org.jooq.impl.Tools.qualify;
import static org.jooq.impl.Tools.recordDirtyTrackingPredicate;
import static org.jooq.impl.Tools.unalias;
import static org.jooq.impl.Tools.unqualified;
import static org.jooq.impl.Tools.BooleanDataKey.DATA_CONSTRAINT_REFERENCE;
@ -306,8 +307,10 @@ implements
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public final void setRecordForUpdate(R record) {
ObjIntPredicate<Record> dirty = recordDirtyTrackingPredicate(this);
for (int i = 0; i < record.size(); i++)
if (record.touched(i))
if (dirty.test(record, i))
addValueForUpdate((Field) record.field(i), record.get(i));
}

View File

@ -1020,7 +1020,7 @@ implements
@Override
public final MergeImpl set(Record record) {
return set(Tools.mapOfTouchedValues(record));
return set(Tools.mapOfTouchedValues(this, record));
}
@Override

View File

@ -65,6 +65,7 @@ import static org.jooq.impl.Tools.filter;
import static org.jooq.impl.Tools.indexOrFail;
import static org.jooq.impl.Tools.isEmpty;
import static org.jooq.impl.Tools.let;
import static org.jooq.impl.Tools.recordDirtyTrackingPredicate;
import static org.jooq.impl.Tools.settings;
import static org.jooq.tools.StringUtils.defaultIfNull;
@ -308,9 +309,10 @@ implements
final List<Field<?>> addTouchedValues(Field<?>[] storeFields, StoreQuery<R> query, boolean forUpdate) {
FieldsImpl<Record> f = new FieldsImpl<>(storeFields);
List<Field<?>> result = new ArrayList<>();
ObjIntPredicate<Record> dirty = recordDirtyTrackingPredicate(query);
for (Field<?> field : fields.fields.fields) {
if (touched(field) && f.field(field) != null && writable(field, forUpdate)) {
if (dirty.test(this, indexOf(field)) && f.field(field) != null && writable(field, forUpdate)) {
addValue(query, field, forUpdate);
result.add(field);
}

View File

@ -268,6 +268,7 @@ import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.IntPredicate;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.regex.MatchResult;
@ -354,6 +355,7 @@ import org.jooq.conf.BackslashEscaping;
import org.jooq.conf.NestedCollectionEmulation;
import org.jooq.conf.ParamType;
import org.jooq.conf.ParseNameCase;
import org.jooq.conf.RecordDirtyTracking;
import org.jooq.conf.RenderDefaultNullability;
import org.jooq.conf.RenderMapping;
import org.jooq.conf.RenderQuotedNames;
@ -2790,17 +2792,26 @@ final class Tools {
/**
* Turn a {@link Record} into a {@link Map}
*/
static final Map<Field<?>, Object> mapOfTouchedValues(Record record) {
static final Map<Field<?>, Object> mapOfTouchedValues(Attachable attachable, Record record) {
Map<Field<?>, Object> result = new LinkedHashMap<>();
int size = record.size();
ObjIntPredicate<Record> dirty = recordDirtyTrackingPredicate(attachable);
for (int i = 0; i < size; i++)
if (record.touched(i))
if (dirty.test(record, i))
result.put(record.field(i), record.get(i));
return result;
}
static final ObjIntPredicate<Record> recordDirtyTrackingPredicate(Attachable attachable) {
return RecordDirtyTracking.MODIFIED.equals(configuration(attachable).settings().getRecordDirtyTracking())
? Record::touched
: Record::modified;
}
/**
* Extract the first item from an iterable or <code>null</code>, if there is
* no such item, or if iterable itself is <code>null</code>

View File

@ -60,6 +60,7 @@ import static org.jooq.impl.RecordDelegate.RecordLifecycleType.STORE;
import static org.jooq.impl.RecordDelegate.RecordLifecycleType.UPDATE;
import static org.jooq.impl.Tools.EMPTY_FIELD;
import static org.jooq.impl.Tools.EMPTY_TABLE_FIELD;
import static org.jooq.impl.Tools.recordDirtyTrackingPredicate;
import static org.jooq.impl.Tools.settings;
import java.math.BigInteger;
@ -204,10 +205,11 @@ public class UpdatableRecordImpl<R extends UpdatableRecord<R>> extends TableReco
executeUpdate = fetched;
}
else {
ObjIntPredicate<Record> dirty = recordDirtyTrackingPredicate(this);
for (TableField<R, ?> field : keys) {
// If any primary key value is null or touched
if (touched(field) ||
if (dirty.test(this, indexOf(field)) ||
// [#3237] or if a NOT NULL primary key value is null, then execute an INSERT
(field.getDataType().nullable() == false && get(field) == null)) {

View File

@ -178,7 +178,7 @@ implements
@Override
public final UpdateImpl<R> set(Record record) {
return set(Tools.mapOfTouchedValues(record));
return set(Tools.mapOfTouchedValues(this, record));
}

View File

@ -1328,6 +1328,10 @@ This flag has no effect when "executeWithOptimisticLocking" is turned off.]]></j
<element name="updateUnchangedRecords" type="jooq-runtime:UpdateUnchangedRecords" minOccurs="0" maxOccurs="1" default="NEVER">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Whether {@link org.jooq.UpdatableRecord#update()} calls should be executed if the record is unchanged. This also affects the <code>UPDATE</code> part of {@link org.jooq.UpdatableRecord#store()} and {@link org.jooq.UpdatableRecord#merge()} calls.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="recordDirtyTracking" type="jooq-runtime:RecordDirtyTracking" minOccurs="0" maxOccurs="1" default="TOUCHED">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Whether {@link org.jooq.UpdatableRecord#store()} and related calls should be based on {@link org.jooq.Record#touched()} or {@link org.jooq.Record#modified()} semantics. This also affects copying records into explicit statements.]]></jxb:javadoc></jxb:property></appinfo></annotation>
</element>
<element name="updatablePrimaryKeys" type="boolean" minOccurs="0" maxOccurs="1" default="false">
<annotation><appinfo><jxb:property><jxb:javadoc><![CDATA[Whether primary key values are deemed to be "updatable" in jOOQ.
@ -2398,6 +2402,17 @@ Either &lt;input/&gt; or &lt;inputExpression/&gt; must be provided]]></jxb:javad
</restriction>
</simpleType>
<simpleType name="RecordDirtyTracking">
<restriction base="string">
<!-- Dirty tracking is based on Record.touched() semantics -->
<enumeration value="TOUCHED"/>
<!-- Dirty tracking is based on Record.modified() semantics -->
<enumeration value="MODIFIED"/>
</restriction>
</simpleType>
<simpleType name="Transformation">
<restriction base="string">