From b131440beff40ddbdc3d4127f7c27e6eb495b19c Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Mon, 6 Apr 2020 13:30:35 +0200 Subject: [PATCH] [jOOQ/jOOQ#2961] Add UpdatableRecord.merge() --- jOOQ/src/main/java/org/jooq/InsertQuery.java | 8 +- .../main/java/org/jooq/RecordListener.java | 20 +++++ .../main/java/org/jooq/UpdatableRecord.java | 75 +++++++++++++++++++ .../org/jooq/impl/DefaultRecordListener.java | 6 ++ .../org/jooq/impl/DefaultRenderContext.java | 1 - .../java/org/jooq/impl/RecordDelegate.java | 3 + .../java/org/jooq/impl/TableRecordImpl.java | 29 +++---- jOOQ/src/main/java/org/jooq/impl/Tools.java | 2 - .../org/jooq/impl/UpdatableRecordImpl.java | 74 +++++++++++++----- 9 files changed, 184 insertions(+), 34 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/InsertQuery.java b/jOOQ/src/main/java/org/jooq/InsertQuery.java index 1676679609..4d5086b924 100644 --- a/jOOQ/src/main/java/org/jooq/InsertQuery.java +++ b/jOOQ/src/main/java/org/jooq/InsertQuery.java @@ -79,7 +79,7 @@ import java.util.Map; * @param The record type of the table being inserted into * @author Lukas Eder */ -public interface InsertQuery extends StoreQuery, Insert { +public interface InsertQuery extends StoreQuery, Insert, ConditionProvider { /** * Adds a new Record to the insert statement for multi-record inserts @@ -260,6 +260,7 @@ public interface InsertQuery extends StoreQuery, Insert * * @param condition The condition */ + @Override @Support({ CUBRID, DERBY, H2, MARIADB, POSTGRES, SQLITE }) void addConditions(Condition condition); @@ -272,6 +273,7 @@ public interface InsertQuery extends StoreQuery, Insert * * @param conditions The condition */ + @Override @Support({ CUBRID, DERBY, H2, MARIADB, POSTGRES, SQLITE }) void addConditions(Condition... conditions); @@ -284,6 +286,7 @@ public interface InsertQuery extends StoreQuery, Insert * * @param conditions The condition */ + @Override @Support({ CUBRID, DERBY, H2, MARIADB, POSTGRES, SQLITE }) void addConditions(Collection conditions); @@ -296,6 +299,7 @@ public interface InsertQuery extends StoreQuery, Insert * * @param condition The condition */ + @Override @Support({ CUBRID, DERBY, H2, MARIADB, POSTGRES, SQLITE }) void addConditions(Operator operator, Condition condition); @@ -308,6 +312,7 @@ public interface InsertQuery extends StoreQuery, Insert * * @param conditions The condition */ + @Override @Support({ CUBRID, DERBY, H2, MARIADB, POSTGRES, SQLITE }) void addConditions(Operator operator, Condition... conditions); @@ -320,6 +325,7 @@ public interface InsertQuery extends StoreQuery, Insert * * @param conditions The condition */ + @Override @Support({ CUBRID, DERBY, H2, MARIADB, POSTGRES, SQLITE }) void addConditions(Operator operator, Collection conditions); diff --git a/jOOQ/src/main/java/org/jooq/RecordListener.java b/jOOQ/src/main/java/org/jooq/RecordListener.java index 1e33f39371..def224f941 100644 --- a/jOOQ/src/main/java/org/jooq/RecordListener.java +++ b/jOOQ/src/main/java/org/jooq/RecordListener.java @@ -128,6 +128,26 @@ public interface RecordListener extends EventListener { */ void updateEnd(RecordContext ctx); + /** + * Called before merging an UpdatableRecord. + *

+ * Implementations are allowed to modify {@link RecordContext#record()} + * prior to merging. + * + * @see UpdatableRecord#merge() + */ + void mergeStart(RecordContext ctx); + + /** + * Called after merging an UpdatableRecord. + *

+ * Implementations are allowed to modify {@link RecordContext#record()} + * after merging. + * + * @see UpdatableRecord#merge() + */ + void mergeEnd(RecordContext ctx); + /** * Called before deleting an UpdatableRecord. *

diff --git a/jOOQ/src/main/java/org/jooq/UpdatableRecord.java b/jOOQ/src/main/java/org/jooq/UpdatableRecord.java index 0f4d7c16df..3f18a1bd2f 100644 --- a/jOOQ/src/main/java/org/jooq/UpdatableRecord.java +++ b/jOOQ/src/main/java/org/jooq/UpdatableRecord.java @@ -37,6 +37,25 @@ */ package org.jooq; +// ... +// ... +// ... +import static org.jooq.SQLDialect.CUBRID; +// ... +import static org.jooq.SQLDialect.DERBY; +import static org.jooq.SQLDialect.H2; +import static org.jooq.SQLDialect.HSQLDB; +// ... +import static org.jooq.SQLDialect.MARIADB; +// ... +import static org.jooq.SQLDialect.MYSQL; +// ... +import static org.jooq.SQLDialect.POSTGRES; +import static org.jooq.SQLDialect.SQLITE; +// ... +// ... +// ... + import java.sql.Statement; import java.util.Collection; @@ -350,6 +369,62 @@ public interface UpdatableRecord> extends TableReco @Support int update(Collection> fields) throws DataAccessException, DataChangedException; + /** + * Store this record back to the database using a MERGE + * statement. + *

+ * Unlike {@link #store()}, the statement produced by this operation does + * not depend on whether the record has been previously fetched from the + * database or created afresh. It implements the semantics of an + * INSERT .. ON DUPLICATE KEY UPDATE statement, which will + * update the row regardless of which (unique) key value is already present. + * See {@link InsertOnDuplicateStep#onDuplicateKeyUpdate()}. + *

+ * Optimistic locking only works if the underlying dialect supports + * {@link InsertOnConflictWhereStep#where(Condition)}. Otherwise, the + * UPDATE path of the statement will not be able to update the + * row conditionally. + *

+ * If you want to enforce statement execution, regardless if the values in + * this record were changed, you can explicitly set the changed flags for + * all values with {@link #changed(boolean)} or for single values with + * {@link #changed(Field, boolean)}, prior to insertion. + *

+ * This is the same as calling record.merge(record.fields()) + * + * @return 1 if the record was merged to the database. 0 + * if merging was not necessary. + * @throws DataAccessException if something went wrong executing the query + * @see #store() + * @see InsertOnDuplicateStep#onDuplicateKeyUpdate() + */ + @Support({ CUBRID, DERBY, H2, HSQLDB, MARIADB, MYSQL, POSTGRES, SQLITE }) + int merge() throws DataAccessException; + + /** + * Store parts of this record to the database using a MERGE + * statement. + * + * @return 1 if the record was merged to the database. 0 + * if merging was not necessary. + * @throws DataAccessException if something went wrong executing the query + * @see #merge() + */ + @Support({ CUBRID, DERBY, H2, HSQLDB, MARIADB, MYSQL, POSTGRES, SQLITE }) + int merge(Field... fields) throws DataAccessException; + + /** + * Store parts of this record to the database using a MERGE + * statement. + * + * @return 1 if the record was merged to the database. 0 + * if merging was not necessary. + * @throws DataAccessException if something went wrong executing the query + * @see #merge() + */ + @Support({ CUBRID, DERBY, H2, HSQLDB, MARIADB, MYSQL, POSTGRES, SQLITE }) + int merge(Collection> fields) throws DataAccessException; + /** * 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/impl/DefaultRecordListener.java b/jOOQ/src/main/java/org/jooq/impl/DefaultRecordListener.java index e3279791fe..15e805a548 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultRecordListener.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultRecordListener.java @@ -68,6 +68,12 @@ public class DefaultRecordListener implements RecordListener { @Override public void updateEnd(RecordContext ctx) {} + @Override + public void mergeStart(RecordContext ctx) {} + + @Override + public void mergeEnd(RecordContext ctx) {} + @Override public void deleteStart(RecordContext ctx) {} diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java b/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java index 1b6e2b8645..c2848ec2e8 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultRenderContext.java @@ -603,7 +603,6 @@ class DefaultRenderContext extends AbstractContext implements Ren return visit(part); } - @SuppressWarnings("deprecation") @Override protected final void visit0(QueryPartInternal internal) { int before = bindValues.size(); diff --git a/jOOQ/src/main/java/org/jooq/impl/RecordDelegate.java b/jOOQ/src/main/java/org/jooq/impl/RecordDelegate.java index 6c0079dd88..3aa9434cdc 100644 --- a/jOOQ/src/main/java/org/jooq/impl/RecordDelegate.java +++ b/jOOQ/src/main/java/org/jooq/impl/RecordDelegate.java @@ -114,6 +114,7 @@ final class RecordDelegate { case STORE: listener.storeStart(ctx); break; case INSERT: listener.insertStart(ctx); break; case UPDATE: listener.updateStart(ctx); break; + case MERGE: listener.mergeStart(ctx); break; case DELETE: listener.deleteStart(ctx); break; default: throw new IllegalStateException("Type not supported: " + type); @@ -158,6 +159,7 @@ final class RecordDelegate { case STORE: listener.storeEnd(ctx); break; case INSERT: listener.insertEnd(ctx); break; case UPDATE: listener.updateEnd(ctx); break; + case MERGE: listener.mergeEnd(ctx); break; case DELETE: listener.deleteEnd(ctx); break; default: throw new IllegalStateException("Type not supported: " + type); @@ -181,6 +183,7 @@ final class RecordDelegate { STORE, INSERT, UPDATE, + MERGE, DELETE } } diff --git a/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java b/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java index 5a25ec5a45..4930c0ea6b 100644 --- a/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/TableRecordImpl.java @@ -185,7 +185,7 @@ public class TableRecordImpl> extends AbstractRecord im final int storeInsert0(Field[] storeFields) { DSLContext create = create(); InsertQuery insert = create.insertQuery(getTable()); - addChangedValues(storeFields, insert); + addChangedValues(storeFields, insert, false); // Don't store records if no value was set by client code if (!insert.isExecutable()) { @@ -196,8 +196,8 @@ public class TableRecordImpl> extends AbstractRecord im } // [#1596] Set timestamp and/or version columns to appropriate values - BigInteger version = addRecordVersion(insert); - Timestamp timestamp = addRecordTimestamp(insert); + BigInteger version = addRecordVersion(insert, false); + Timestamp timestamp = addRecordTimestamp(insert, false); // [#814] Refresh identity and/or main unique key values // [#1002] Consider also identity columns of non-updatable records @@ -292,34 +292,37 @@ public class TableRecordImpl> extends AbstractRecord im } /** - * Set all changed values of this record to a store query + * Set all changed values of this record to a store query. */ - final void addChangedValues(Field[] storeFields, StoreQuery query) { + final void addChangedValues(Field[] storeFields, StoreQuery query, boolean forUpdate) { Fields f = new Fields<>(storeFields); for (Field field : fields.fields.fields) if (changed(field) && f.field(field) != null) - addValue(query, field); + addValue(query, field, forUpdate); } /** * Extracted method to ensure generic type safety. */ - final void addValue(StoreQuery store, Field field, Object value) { + final void addValue(StoreQuery store, Field field, Object value, boolean forUpdate) { store.addValue(field, Tools.field(value, field)); + + if (forUpdate) + ((InsertQuery) store).addValueForUpdate(field, Tools.field(value, field)); } /** * Extracted method to ensure generic type safety. */ - final void addValue(StoreQuery store, Field field) { - addValue(store, field, get(field)); + final void addValue(StoreQuery store, Field field, boolean forUpdate) { + addValue(store, field, get(field), forUpdate); } /** * Set an updated timestamp value to a store query */ - final Timestamp addRecordTimestamp(StoreQuery store) { + final Timestamp addRecordTimestamp(StoreQuery store, boolean forUpdate) { Timestamp result = null; TableField timestamp = getTable().getRecordTimestamp(); @@ -334,7 +337,7 @@ public class TableRecordImpl> extends AbstractRecord im // [#9933] Truncate timestamp to column precision, if needed - addValue(store, timestamp, result = truncate(result, timestamp.getDataType())); + addValue(store, timestamp, result = truncate(result, timestamp.getDataType()), forUpdate); } return result; @@ -354,7 +357,7 @@ public class TableRecordImpl> extends AbstractRecord im /** * Set an updated version value to a store query */ - final BigInteger addRecordVersion(StoreQuery store) { + final BigInteger addRecordVersion(StoreQuery store, boolean forUpdate) { BigInteger result = null; TableField version = getTable().getRecordVersion(); @@ -367,7 +370,7 @@ public class TableRecordImpl> extends AbstractRecord im else result = new BigInteger(value.toString()).add(BigInteger.ONE); - addValue(store, version, result); + addValue(store, version, result, forUpdate); } return result; diff --git a/jOOQ/src/main/java/org/jooq/impl/Tools.java b/jOOQ/src/main/java/org/jooq/impl/Tools.java index 4c002481fb..f09dae000e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Tools.java +++ b/jOOQ/src/main/java/org/jooq/impl/Tools.java @@ -3144,7 +3144,6 @@ final class Tools { /** * Add primary key conditions to a query */ - @SuppressWarnings("deprecation") static final void addConditions(org.jooq.ConditionProvider query, Record record, Field... keys) { for (Field field : keys) addCondition(query, record, field); @@ -3153,7 +3152,6 @@ final class Tools { /** * Add a field condition to a query */ - @SuppressWarnings("deprecation") static final void addCondition(org.jooq.ConditionProvider provider, Record record, Field field) { // [#2764] If primary keys are allowed to be changed, the diff --git a/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java b/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java index 2476ad0adb..8ce251da33 100644 --- a/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java +++ b/jOOQ/src/main/java/org/jooq/impl/UpdatableRecordImpl.java @@ -43,6 +43,7 @@ import static org.jooq.SQLDialect.SQLITE; import static org.jooq.conf.SettingsTools.updatablePrimaryKeys; import static org.jooq.impl.RecordDelegate.delegate; import static org.jooq.impl.RecordDelegate.RecordLifecycleType.DELETE; +import static org.jooq.impl.RecordDelegate.RecordLifecycleType.MERGE; import static org.jooq.impl.RecordDelegate.RecordLifecycleType.REFRESH; import static org.jooq.impl.RecordDelegate.RecordLifecycleType.STORE; import static org.jooq.impl.RecordDelegate.RecordLifecycleType.UPDATE; @@ -59,16 +60,17 @@ import org.jooq.Configuration; import org.jooq.DeleteQuery; import org.jooq.Field; import org.jooq.ForeignKey; +import org.jooq.InsertQuery; import org.jooq.Record; import org.jooq.Result; import org.jooq.SQLDialect; import org.jooq.SelectQuery; +import org.jooq.StoreQuery; import org.jooq.Table; import org.jooq.TableField; import org.jooq.TableRecord; import org.jooq.UniqueKey; import org.jooq.UpdatableRecord; -import org.jooq.UpdateQuery; import org.jooq.exception.DataChangedException; import org.jooq.exception.NoDataFoundException; import org.jooq.tools.JooqLogger; @@ -160,6 +162,21 @@ public class UpdatableRecordImpl> extends TableReco return update(storeFields.toArray(EMPTY_FIELD)); } + @Override + public final int merge() { + return merge(fields.fields.fields); + } + + @Override + public int merge(Field... storeFields) { + return storeMerge(storeFields, getPrimaryKey().getFieldsArray()); + } + + @Override + public final int merge(Collection> storeFields) { + return merge(storeFields.toArray(EMPTY_FIELD)); + } + private final int store0(Field[] storeFields) { TableField[] keys = getPrimaryKey().getFieldsArray(); boolean executeUpdate = false; @@ -188,12 +205,10 @@ public class UpdatableRecordImpl> extends TableReco int result = 0; - if (executeUpdate) { + if (executeUpdate) result = storeUpdate(storeFields, keys); - } - else { + else result = storeInsert(storeFields); - } return result; } @@ -212,32 +227,57 @@ public class UpdatableRecordImpl> extends TableReco }); return result[0]; - } private final int storeUpdate0(Field[] storeFields, TableField[] keys) { - UpdateQuery update = create().updateQuery(getTable()); - addChangedValues(storeFields, update); - Tools.addConditions(update, this, keys); + return storeMergeOrUpdate0(storeFields, keys, create().updateQuery(getTable()), false); + } + + private final int storeMerge(final Field[] storeFields, final TableField[] keys) { + final int[] result = new int[1]; + + delegate(configuration(), (Record) this, MERGE) + .operate(new RecordOperation() { + + @Override + public Record operate(Record record) throws RuntimeException { + result[0] = storeMerge0(storeFields, keys); + return record; + } + }); + + // MySQL returns 0 when nothing was updated, 1 when something was inserted, and 2 if something was updated + return Math.min(result[0], 1); + } + + private final int storeMerge0(Field[] storeFields, TableField[] keys) { + InsertQuery merge = create().insertQuery(getTable()); + merge.onDuplicateKeyUpdate(true); + return storeMergeOrUpdate0(storeFields, keys, merge, true); + } + + private final & org.jooq.ConditionProvider> int storeMergeOrUpdate0(Field[] storeFields, TableField[] keys, Q query, boolean merge) { + addChangedValues(storeFields, query, merge); + Tools.addConditions(query, this, keys); // Don't store records if no value was set by client code - if (!update.isExecutable()) { + if (!query.isExecutable()) { if (log.isDebugEnabled()) - log.debug("Query is not executable", update); + log.debug("Query is not executable", query); return 0; } // [#1596] Set timestamp and/or version columns to appropriate values // [#8924] Allow for overriding this using a setting - BigInteger version = addRecordVersion(update); - Timestamp timestamp = addRecordTimestamp(update); + BigInteger version = addRecordVersion(query, merge); + Timestamp timestamp = addRecordTimestamp(query, merge); if (isExecuteWithOptimisticLocking()) // [#1596] Add additional conditions for version and/or timestamp columns if (isTimestampOrVersionAvailable()) - addConditionForVersionAndTimestamp(update); + addConditionForVersionAndTimestamp(query); // [#1547] Try fetching the Record again first, and compare this // Record's original values with the ones in the database @@ -247,8 +287,8 @@ public class UpdatableRecordImpl> extends TableReco // [#1596] Check if the record was really changed in the database // [#1859] Specify the returning clause if needed - Collection> key = setReturningIfNeeded(update); - int result = update.execute(); + Collection> key = setReturningIfNeeded(query); + int result = query.execute(); checkIfChanged(result, version, timestamp); if (result > 0) { @@ -256,7 +296,7 @@ public class UpdatableRecordImpl> extends TableReco changed(storeField, false); // [#1859] If an update was successful try fetching the generated - getReturningIfNeeded(update, key); + getReturningIfNeeded(query, key); } return result;