This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit a3ce1e77df1bbe72ab340756471fe113d618350d Author: Andrus Adamchik <[email protected]> AuthorDate: Fri Jul 3 14:21:12 2026 -0400 Refactor TranslatedBatch support for deferred values resolution --- .../crypto/batch/CryptoBatchTranslator.java | 17 +------ .../crypto/transformer/BindingsTransformer.java | 9 +++- .../transformer/DefaultBindingsTransformer.java | 46 +++++++++--------- .../transformer/DefaultTransformerFactory.java | 5 +- .../crypto/transformer/TransformerFactory.java | 5 +- .../access/flush/ArcValuesCreationHandler.java | 5 +- .../cayenne/access/flush/ReplacementIdVisitor.java | 10 ++-- .../apache/cayenne/access/jdbc/BatchAction.java | 41 ++++++++++++++-- .../cayenne/access/jdbc/PSBatchParameter.java | 47 +++++++++++++++---- .../translator/batch/BaseBatchTranslator.java | 26 +++++++---- .../access/translator/batch/BatchRowBinder.java | 36 --------------- .../translator/batch/DeleteBatchTranslator.java | 42 ++++++----------- .../translator/batch/InsertBatchTranslator.java | 28 ++++------- .../access/translator/batch/TranslatedBatch.java | 17 ++----- .../translator/batch/UpdateBatchTranslator.java | 33 +++++-------- .../org/apache/cayenne/query/BatchQueryRow.java | 54 +++------------------- .../org/apache/cayenne/query/DeleteBatchQuery.java | 7 +-- .../org/apache/cayenne/query/InsertBatchQuery.java | 4 +- 18 files changed, 185 insertions(+), 247 deletions(-) diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslator.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslator.java index 7615ea191..60d7c490f 100644 --- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslator.java +++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/batch/CryptoBatchTranslator.java @@ -18,8 +18,6 @@ ****************************************************************/ package org.apache.cayenne.crypto.batch; -import org.apache.cayenne.access.jdbc.PSParameter; -import org.apache.cayenne.access.jdbc.PSBatchParameter; import org.apache.cayenne.access.translator.batch.BatchTranslator; import org.apache.cayenne.access.translator.batch.TranslatedBatch; import org.apache.cayenne.crypto.transformer.BindingsTransformer; @@ -27,7 +25,6 @@ import org.apache.cayenne.crypto.transformer.TransformerFactory; import org.apache.cayenne.dba.DbAdapter; import org.apache.cayenne.di.Inject; import org.apache.cayenne.query.BatchQuery; -import org.apache.cayenne.query.BatchQueryRow; /** * @since 5.0 @@ -48,20 +45,10 @@ public class CryptoBatchTranslator<T extends BatchQuery> implements BatchTransla @Override public TranslatedBatch translate(T query, DbAdapter adapter) { TranslatedBatch translated = delegate.translate(query, adapter); - BindingsTransformer encryptor = transformerFactory.encryptor(translated.bindings(), adapter.getExtendedTypes()); + BindingsTransformer encryptor = transformerFactory.encryptor(translated.bindings()); return encryptor != null - ? new TranslatedBatch(translated.sql(), translated.bindings(), (bindings, row) -> transform(bindings, row, translated, encryptor)) + ? new TranslatedBatch(translated.sql(), encryptor.transform(translated.bindings())) : translated; } - - private static PSParameter[] transform( - PSBatchParameter[] template, - BatchQueryRow row, - TranslatedBatch translated, - BindingsTransformer encryptor) { - PSParameter<?>[] updated = translated.binder().bind(template, row); - encryptor.transform(updated); - return updated; - } } diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java index 2ad6a68f0..b9df1f115 100644 --- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java +++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/BindingsTransformer.java @@ -18,12 +18,17 @@ ****************************************************************/ package org.apache.cayenne.crypto.transformer; -import org.apache.cayenne.access.jdbc.PSParameter; +import org.apache.cayenne.access.jdbc.PSBatchParameter; /** * @since 4.0 */ public interface BindingsTransformer { - void transform(PSParameter<?>[] bindings); + /** + * Returns a copy of the given batch parameters with the values of encrypted columns transformed. + * + * @since 5.0 + */ + PSBatchParameter[] transform(PSBatchParameter[] bindings); } diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java index e6a750743..bbbbe7009 100644 --- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java +++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultBindingsTransformer.java @@ -18,9 +18,10 @@ ****************************************************************/ package org.apache.cayenne.crypto.transformer; -import org.apache.cayenne.access.jdbc.PSParameter; -import org.apache.cayenne.access.types.ExtendedType; -import org.apache.cayenne.access.types.ExtendedTypeMap; +import java.util.Arrays; +import java.util.function.Supplier; + +import org.apache.cayenne.access.jdbc.PSBatchParameter; import org.apache.cayenne.crypto.transformer.bytes.BytesEncryptor; import org.apache.cayenne.crypto.transformer.value.ValueEncryptor; @@ -32,35 +33,36 @@ public class DefaultBindingsTransformer implements BindingsTransformer { private final int[] positions; private final ValueEncryptor[] transformers; private final BytesEncryptor encryptor; - private final ExtendedTypeMap extendedTypeMap; - public DefaultBindingsTransformer( - int[] positions, - ValueEncryptor[] transformers, - BytesEncryptor encryptor, - ExtendedTypeMap extendedTypeMap) { + public DefaultBindingsTransformer(int[] positions, ValueEncryptor[] transformers, BytesEncryptor encryptor) { this.positions = positions; this.transformers = transformers; this.encryptor = encryptor; - this.extendedTypeMap = extendedTypeMap; } @Override - public void transform(PSParameter<?>[] bindings) { - - int len = positions.length; + public PSBatchParameter[] transform(PSBatchParameter[] bindings) { + PSBatchParameter[] result = Arrays.copyOf(bindings, bindings.length); - for (int i = 0; i < len; i++) { - PSParameter<?> b = bindings[positions[i]]; - Object transformed = transformers[i].encrypt(encryptor, b.value()); + for (int i = 0; i < positions.length; i++) { + PSBatchParameter b = bindings[positions[i]]; + ValueEncryptor transformer = transformers[i]; - ExtendedType<?> extendedType = transformed != null - ? extendedTypeMap.getRegisteredType(transformed.getClass()) - : extendedTypeMap.getDefaultType(); + Object[] values = b.values(); + Object[] encrypted = new Object[values.length]; + for (int r = 0; r < values.length; r++) { + Object value = values[r]; + if (value instanceof Supplier<?> deferred) { + // a deferred value can only be encrypted after it is resolved at bind time + encrypted[r] = (Supplier<Object>) () -> transformer.encrypt(encryptor, deferred.get()); + } else { + encrypted[r] = transformer.encrypt(encryptor, value); + } + } - bindings[positions[i]] = new PSParameter(transformed, b.psPosition(), b.psType(), b.psScale(), extendedType, b.attribute() - ); + result[positions[i]] = new PSBatchParameter(encrypted, b.psPosition(), b.psType(), b.psScale(), b.attribute()); } - } + return result; + } } diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java index 50d1cac5a..9d5c45564 100644 --- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java +++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/DefaultTransformerFactory.java @@ -20,7 +20,6 @@ package org.apache.cayenne.crypto.transformer; import org.apache.cayenne.access.jdbc.RSColumn; import org.apache.cayenne.access.jdbc.PSBatchParameter; -import org.apache.cayenne.access.types.ExtendedTypeMap; import org.apache.cayenne.crypto.map.ColumnMapper; import org.apache.cayenne.crypto.transformer.bytes.BytesTransformerFactory; import org.apache.cayenne.crypto.transformer.value.ValueDecryptor; @@ -91,7 +90,7 @@ public class DefaultTransformerFactory implements TransformerFactory { } @Override - public BindingsTransformer encryptor(PSBatchParameter[] bindings, ExtendedTypeMap extendedTypeMap) { + public BindingsTransformer encryptor(PSBatchParameter[] bindings) { int len = bindings.length; List<Integer> cryptoColumns = null; @@ -121,7 +120,7 @@ public class DefaultTransformerFactory implements TransformerFactory { transformers[i] = transformerFactory.encryptor(b.attribute()); } - return new DefaultBindingsTransformer(positions, transformers, bytesTransformerFactory.encryptor(), extendedTypeMap); + return new DefaultBindingsTransformer(positions, transformers, bytesTransformerFactory.encryptor()); } return null; diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java index 7ed5cc520..21230a968 100644 --- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java +++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/TransformerFactory.java @@ -20,17 +20,16 @@ package org.apache.cayenne.crypto.transformer; import org.apache.cayenne.access.jdbc.RSColumn; import org.apache.cayenne.access.jdbc.PSBatchParameter; -import org.apache.cayenne.access.types.ExtendedTypeMap; /** * A factory that creates encryption transformers used for processing batch * bindings and decryption transformers - for result rows. - * + * * @since 4.0 */ public interface TransformerFactory { - BindingsTransformer encryptor(PSBatchParameter[] bindings, ExtendedTypeMap extendedTypeMap); + BindingsTransformer encryptor(PSBatchParameter[] bindings); MapTransformer decryptor(RSColumn[] columns, Object sampleRow); } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java index 6da46d468..c529f791c 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/ArcValuesCreationHandler.java @@ -234,8 +234,9 @@ class ArcValuesCreationHandler implements GraphChangeHandler { } } - // propagated master -> child PK - if(id != null && attribute.isPrimaryKey()) { + // propagated master -> child PK; only an arc creation defines the new id value, an arc deletion + // must not stomp it with the old target's value + if(add && id != null && attribute.isPrimaryKey()) { id.getReplacementIdMap().put(attribute.getName(), valueToUse); } if(rowOp != null) { diff --git a/cayenne/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java b/cayenne/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java index 3194e7fff..29043e600 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java @@ -89,11 +89,15 @@ class ReplacementIdVisitor implements DbRowOpVisitor<Void> { return; } Map<String, Object> replacement = id.getReplacementIdMap(); - replacement.forEach((attr, val) -> { - if(val instanceof IdGenerationMarker) { + for (Map.Entry<String, Object> next : replacement.entrySet()) { + if (next.getValue() instanceof IdGenerationMarker) { throw new CayenneRuntimeException("PK for the object %s is not set during insert.", object); } - }); + // resolve lazy suppliers, as by now the DB values they refer to are known + if (next.getValue() instanceof Supplier) { + next.setValue(((Supplier<?>) next.getValue()).get()); + } + } ObjectId replacementId = id.createReplacementId(); if (object.getObjectId() == id && !replacementId.getEntityName().startsWith(ASTDbPath.DB_PREFIX)) { diff --git a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java index eb32da59a..a2b2b4f14 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java @@ -27,6 +27,7 @@ import org.apache.cayenne.access.OperationObserver; import org.apache.cayenne.access.OptimisticLockException; import org.apache.cayenne.access.jdbc.reader.RowReader; import org.apache.cayenne.access.types.ExtendedType; +import org.apache.cayenne.access.types.ExtendedTypeMap; import org.apache.cayenne.access.translator.batch.TranslatedBatch; import org.apache.cayenne.dba.DbAdapter; import org.apache.cayenne.log.JdbcEventLogger; @@ -124,9 +125,9 @@ public class BatchAction extends BaseSQLAction { DbAdapter adapter = dataNode.getAdapter(); try (PreparedStatement statement = prepareStatement(con, sql, adapter, generatesKeys)) { - for (BatchQueryRow row : query.getRows()) { + for (int row = 0; row < query.getRows().size(); row++) { - PSParameter<?>[] bindings = translated.updateBindings(row); + PSParameter<?>[] bindings = rowBindings(translated, row, adapter.getExtendedTypes()); logger.logQueryParameters("batch bind", bindings); for (PSParameter<?> b : bindings) { adapter.bindParameter(statement, b); @@ -185,9 +186,11 @@ public class BatchAction extends BaseSQLAction { DbAdapter adapter = dataNode.getAdapter(); try (PreparedStatement statement = prepareStatement(connection, queryStr, adapter, generatesKeys)) { - for (BatchQueryRow row : query.getRows()) { + List<BatchQueryRow> rows = query.getRows(); + for (int i = 0; i < rows.size(); i++) { + BatchQueryRow row = rows.get(i); - PSParameter<?>[] bindings = translated.updateBindings(row); + PSParameter<?>[] bindings = rowBindings(translated, i, adapter.getExtendedTypes()); logger.logQueryParameters("bind", bindings); for (PSParameter<?> b : bindings) { @@ -211,6 +214,36 @@ public class BatchAction extends BaseSQLAction { } } + /** + * Resolves the given batch row against the translated batch parameters, returning per-row bindings. + */ + protected PSParameter<?>[] rowBindings(TranslatedBatch translated, int row, ExtendedTypeMap extendedTypes) { + PSBatchParameter[] template = translated.bindings(); + PSParameter<?>[] bindings = new PSParameter[template.length]; + for (int j = 0; j < template.length; j++) { + bindings[j] = bind(template[j], row, extendedTypes); + } + return bindings; + } + + @SuppressWarnings("unchecked") + private static <T> PSParameter<T> bind(PSBatchParameter parameter, int row, ExtendedTypeMap extendedTypes) { + Object value = parameter.getValue(row); + + DbAttribute attribute = parameter.attribute(); + if (value == null && attribute.isPrimaryKey()) { + String entity = attribute.getEntity() != null ? attribute.getEntity().getName() : "<null>"; + throw new CayenneRuntimeException("Failed to generate PK: %s.%s", entity, attribute.getName()); + } + + ExtendedType<T> extendedType = value != null + ? extendedTypes.getRegisteredType(value.getClass()) + : extendedTypes.getDefaultType(); + return new PSParameter<>((T) value, parameter.psPosition(), parameter.psType(), parameter.psScale(), + extendedType, parameter.attribute()); + } + + protected PreparedStatement prepareStatement(Connection connection, String queryStr, DbAdapter adapter, boolean generatedKeys) throws SQLException { return (generatedKeys) diff --git a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/PSBatchParameter.java b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/PSBatchParameter.java index 982d678f5..3dec7bd18 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/PSBatchParameter.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/PSBatchParameter.java @@ -19,22 +19,49 @@ package org.apache.cayenne.access.jdbc; -import org.apache.cayenne.access.types.ExtendedType; +import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.map.DbAttribute; +import java.util.function.Supplier; + /** - * An immutable per-batch parameter binding template, carrying the static PreparedStatement position and column - * metadata shared by all rows of a batch. A per-row {@link PSParameter} is produced by - * {@link #bind(Object, ExtendedType)}. + * An immutable batch parameter corresponding to a single PreparedStatement placeholder, carrying the values of all + * batch rows for that placeholder alongside the static position and column metadata. The value of a given row is + * resolved via {@link #getValue(int)}. + * + * <p>A value may be a deferred {@link Supplier}, e.g. when it comes from a generated key of another row in the same + * transaction. Deferred values are resolved when the corresponding row is bound, i.e. after the rows preceding it + * have been executed. * * @since 5.0 */ -public record PSBatchParameter(int psPosition, int psType, int psScale, DbAttribute attribute) { +public record PSBatchParameter(Object[] values, int psPosition, int psType, int psScale, DbAttribute attribute) { + + private static final int MAX_NESTED_SUPPLIER_LEVEL = 1000; + + public Object getValue(int row) { + Object value = values[row]; + + if (!(value instanceof Supplier)) { + return value; + } + + int safeguard = 0; + + // Suppliers can be nested, resolve all the way down + while (value instanceof Supplier<?> s && safeguard < MAX_NESTED_SUPPLIER_LEVEL) { + value = s.get(); + safeguard++; + } + + // guard against recursive Suppliers + if (safeguard == MAX_NESTED_SUPPLIER_LEVEL) { + throw new CayenneRuntimeException( + "Possible recursive supplier chain for the batch value of attribute %s", + attribute.getName()); + } - /** - * Resolves this per-batch template into a per-row {@link PSParameter}. - */ - public <T> PSParameter<T> bind(T value, ExtendedType<T> extendedType) { - return new PSParameter<>(value, psPosition, psType, psScale, extendedType, attribute); + values[row] = value; + return value; } } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java index ad56b7599..0e6b84d07 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java @@ -47,25 +47,33 @@ public abstract class BaseBatchTranslator<T extends BatchQuery> implements Batch String sql = createSql(context); PSBatchParameter[] bindings = createBindings(context); - return new TranslatedBatch(sql, bindings, (template, row) -> updateBindings(context, template, row)); + return new TranslatedBatch(sql, bindings); } protected abstract String createSql(BatchTranslatorContext<T> context); protected PSBatchParameter[] createBindings(BatchTranslatorContext<T> context) { + int[] rowValueIndexes = createRowValueIndexes(context); + List<BatchQueryRow> rows = context.getQuery().getRows(); + PSBatchParameter[] bindings = new PSBatchParameter[context.getBindings().size()]; - int i = 0; - for (PSParameter b : context.getBindings()) { - bindings[i] = new PSBatchParameter(i + 1, b.psType(), b.psScale(), b.attribute()); - i++; + int j = 0; + for (PSParameter<?> b : context.getBindings()) { + Object[] values = new Object[rows.size()]; + for (int r = 0; r < values.length; r++) { + values[r] = rows.get(r).getValue(rowValueIndexes[j]); + } + bindings[j] = new PSBatchParameter(values, j + 1, b.psType(), b.psScale(), b.attribute()); + j++; } return bindings; } - protected abstract PSParameter[] updateBindings( - BatchTranslatorContext<T> context, - PSBatchParameter[] template, - BatchQueryRow row); + /** + * Returns, for each PreparedStatement placeholder in the order {@link #createSql(BatchTranslatorContext)} emits + * them, the index of the {@link BatchQueryRow} value that feeds it. + */ + protected abstract int[] createRowValueIndexes(BatchTranslatorContext<T> context); protected String doTranslate(BatchTranslatorContext<T> context, NodeBuilder nodeBuilder) { Node node = context.getAdapter().getSqlTreeProcessor().process(nodeBuilder.build()); diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BatchRowBinder.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BatchRowBinder.java deleted file mode 100644 index 99ba70d25..000000000 --- a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/BatchRowBinder.java +++ /dev/null @@ -1,36 +0,0 @@ -/***************************************************************** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 - * - * https://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. - ****************************************************************/ - -package org.apache.cayenne.access.translator.batch; - -import org.apache.cayenne.access.jdbc.PSBatchParameter; -import org.apache.cayenne.access.jdbc.PSParameter; -import org.apache.cayenne.query.BatchQueryRow; - -/** - * A stateless strategy that resolves a batch query's binding templates against the values of a single - * {@link BatchQueryRow}, returning a fresh array of per-row {@link PSParameter}s. - * - * @since 5.0 - */ -@FunctionalInterface -public interface BatchRowBinder { - - PSParameter<?>[] bind(PSBatchParameter[] template, BatchQueryRow row); -} diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java index 818a3ebf6..91e5d8bed 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java @@ -19,13 +19,11 @@ package org.apache.cayenne.access.translator.batch; -import org.apache.cayenne.access.jdbc.PSBatchParameter; +import java.util.Arrays; + import org.apache.cayenne.access.sqlbuilder.DeleteBuilder; import org.apache.cayenne.access.sqlbuilder.SQLBuilder; -import org.apache.cayenne.access.jdbc.PSParameter; -import org.apache.cayenne.access.types.ExtendedType; import org.apache.cayenne.map.DbAttribute; -import org.apache.cayenne.query.BatchQueryRow; import org.apache.cayenne.query.DeleteBatchQuery; /** @@ -47,31 +45,19 @@ public class DeleteBatchTranslator extends BaseBatchTranslator<DeleteBatchQuery> } @Override - protected PSParameter[] updateBindings( - BatchTranslatorContext<DeleteBatchQuery> context, - PSBatchParameter[] template, - BatchQueryRow row) { - + protected int[] createRowValueIndexes(BatchTranslatorContext<DeleteBatchQuery> context) { DeleteBatchQuery deleteBatch = context.getQuery(); - PSParameter[] bindings = new PSParameter[template.length]; - for (int i = 0, position = 0; i < deleteBatch.getDbAttributes().size(); i++) { - position = updateBinding(context, template, bindings, row.getValue(i), position); - } - return bindings; - } - - protected int updateBinding( - BatchTranslatorContext<DeleteBatchQuery> context, - PSBatchParameter[] template, - PSParameter[] bindings, - Object value, int position) { - - // skip null attributes... they are translated as "IS NULL" - if (value != null) { - ExtendedType extendedType = context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass()); - bindings[position] = template[position].bind(value, extendedType); - position++; + int[] indexes = new int[deleteBatch.getDbAttributes().size()]; + + int i = 0; + int j = 0; + // attributes with null values render as "IS NULL" and have no placeholder + for (DbAttribute attribute : deleteBatch.getDbAttributes()) { + if (!deleteBatch.isNull(attribute)) { + indexes[j++] = i; + } + i++; } - return position; + return Arrays.copyOf(indexes, j); } } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java index c4df88c23..7aa08b0e8 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java @@ -19,13 +19,11 @@ package org.apache.cayenne.access.translator.batch; -import org.apache.cayenne.access.jdbc.PSBatchParameter; +import java.util.Arrays; + import org.apache.cayenne.access.sqlbuilder.InsertBuilder; import org.apache.cayenne.access.sqlbuilder.SQLBuilder; -import org.apache.cayenne.access.jdbc.PSParameter; -import org.apache.cayenne.access.types.ExtendedType; import org.apache.cayenne.map.DbAttribute; -import org.apache.cayenne.query.BatchQueryRow; import org.apache.cayenne.query.InsertBatchQuery; /** @@ -46,7 +44,7 @@ public class InsertBatchTranslator extends BaseBatchTranslator<InsertBatchQuery> insertBuilder .column(SQLBuilder.column(attribute.getName()).attribute(attribute)) // We can use here any non-null value, to create attribute binding, - // actual value and ExtendedType will be set at updateBindings() call. + // actual values and ExtendedTypes are resolved when binding each row. .value(SQLBuilder.value(1).attribute(attribute)); } @@ -54,26 +52,18 @@ public class InsertBatchTranslator extends BaseBatchTranslator<InsertBatchQuery> } @Override - protected PSParameter[] updateBindings(BatchTranslatorContext<InsertBatchQuery> context, - PSBatchParameter[] template, BatchQueryRow row) { + protected int[] createRowValueIndexes(BatchTranslatorContext<InsertBatchQuery> context) { InsertBatchQuery query = context.getQuery(); - PSParameter[] bindings = new PSParameter[template.length]; + int[] indexes = new int[query.getDbAttributes().size()]; int i = 0; int j = 0; for (DbAttribute attribute : query.getDbAttributes()) { - if (excludeInBatch(context, attribute)) { - i++; - continue; + if (!excludeInBatch(context, attribute)) { + indexes[j++] = i; } - - Object value = row.getValue(i++); - ExtendedType extendedType = value != null - ? context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass()) - : context.getAdapter().getExtendedTypes().getDefaultType(); - bindings[j] = template[j].bind(value, extendedType); - j++; + i++; } - return bindings; + return Arrays.copyOf(indexes, j); } protected boolean excludeInBatch(BatchTranslatorContext<InsertBatchQuery> context, DbAttribute attribute) { diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/TranslatedBatch.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/TranslatedBatch.java index 8bc3fc1b5..02125d30b 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/TranslatedBatch.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/TranslatedBatch.java @@ -20,22 +20,13 @@ package org.apache.cayenne.access.translator.batch; import org.apache.cayenne.access.jdbc.PSBatchParameter; -import org.apache.cayenne.access.jdbc.PSParameter; -import org.apache.cayenne.query.BatchQueryRow; /** - * An immutable result of translating a batch query: the SQL String shared by all rows of the batch, - * the widest possible array of per-batch binding templates, and a stateless {@link BatchRowBinder} that - * resolves a single row's state into per-row bindings. + * An immutable result of translating a batch query: the SQL String shared by all rows of the batch, and one + * {@link PSBatchParameter} per PreparedStatement placeholder, each carrying the values of all batch rows for + * its placeholder. * * @since 5.0 */ -public record TranslatedBatch(String sql, PSBatchParameter[] bindings, BatchRowBinder binder) { - - /** - * Resolves the given row's state against the binding templates, returning a fresh per-row bindings array. - */ - public PSParameter<?>[] updateBindings(BatchQueryRow row) { - return binder.bind(bindings, row); - } +public record TranslatedBatch(String sql, PSBatchParameter[] bindings) { } diff --git a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java index c0b496275..4580e90c5 100644 --- a/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java +++ b/cayenne/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java @@ -19,13 +19,11 @@ package org.apache.cayenne.access.translator.batch; -import org.apache.cayenne.access.jdbc.PSBatchParameter; +import java.util.Arrays; + import org.apache.cayenne.access.sqlbuilder.SQLBuilder; import org.apache.cayenne.access.sqlbuilder.UpdateBuilder; -import org.apache.cayenne.access.jdbc.PSParameter; -import org.apache.cayenne.access.types.ExtendedType; import org.apache.cayenne.map.DbAttribute; -import org.apache.cayenne.query.BatchQueryRow; import org.apache.cayenne.query.UpdateBatchQuery; /** @@ -55,33 +53,24 @@ public class UpdateBatchTranslator extends BaseBatchTranslator<UpdateBatchQuery> } @Override - protected PSParameter[] updateBindings(BatchTranslatorContext<UpdateBatchQuery> context, - PSBatchParameter[] template, BatchQueryRow row) { + protected int[] createRowValueIndexes(BatchTranslatorContext<UpdateBatchQuery> context) { UpdateBatchQuery updateBatch = context.getQuery(); - PSParameter[] bindings = new PSParameter[template.length]; + int updatedCount = updateBatch.getUpdatedAttributes().size(); + int[] indexes = new int[updatedCount + updateBatch.getQualifierAttributes().size()]; int i = 0; int j = 0; - for (; i < updateBatch.getUpdatedAttributes().size(); i++) { - Object value = row.getValue(i); - ExtendedType extendedType = value == null - ? context.getAdapter().getExtendedTypes().getDefaultType() - : context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass()); - bindings[j] = template[j].bind(value, extendedType); - j++; + for (; i < updatedCount; i++) { + indexes[j++] = i; } + // qualifier attributes with null values render as "IS NULL" and have no placeholder for (DbAttribute attribute : updateBatch.getQualifierAttributes()) { - if (updateBatch.isNull(attribute)) { - i++; - continue; + if (!updateBatch.isNull(attribute)) { + indexes[j++] = i; } - Object value = row.getValue(i); - ExtendedType extendedType = context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass()); - bindings[j] = template[j].bind(value, extendedType); - j++; i++; } - return bindings; + return Arrays.copyOf(indexes, j); } } diff --git a/cayenne/src/main/java/org/apache/cayenne/query/BatchQueryRow.java b/cayenne/src/main/java/org/apache/cayenne/query/BatchQueryRow.java index 0bab779c2..084e422ce 100644 --- a/cayenne/src/main/java/org/apache/cayenne/query/BatchQueryRow.java +++ b/cayenne/src/main/java/org/apache/cayenne/query/BatchQueryRow.java @@ -16,24 +16,24 @@ * specific language governing permissions and limitations * under the License. ****************************************************************/ + package org.apache.cayenne.query; import java.util.Map; import java.util.function.Supplier; -import org.apache.cayenne.CayenneRuntimeException; import org.apache.cayenne.ObjectId; import org.apache.cayenne.map.DbAttribute; /** - * Represents a single row of values in a BatchQuery. - * + * Represents a single row of values in a BatchQuery. A value may be a deferred {@link Supplier}, e.g. when it comes + * from a generated key of another row in the same transaction. Deferred values are resolved downstream, when the + * row is bound to a PreparedStatement. + * * @since 4.0 */ public abstract class BatchQueryRow { - private static final int MAX_NESTED_SUPPLIER_LEVEL = 1000; - protected ObjectId objectId; protected Map<String, Object> qualifier; @@ -52,49 +52,7 @@ public abstract class BatchQueryRow { return objectId; } - /** - * Used by subclasses to resolve deferred values on demand. This is useful - * when a certain value comes from a generated key of another master object. - */ protected Object getValue(Map<String, Object> valueMap, DbAttribute attribute) { - - Object value = valueMap.get(attribute.getName()); - boolean isSupplier = false; - int safeguard = 0; - - // Supplier can be nested, resolve all the way down - while(value instanceof Supplier && safeguard < MAX_NESTED_SUPPLIER_LEVEL) { - value = ((Supplier) value).get(); - isSupplier = true; - safeguard++; - } - - // simple guard from recursive Suppliers - if(safeguard == MAX_NESTED_SUPPLIER_LEVEL) { - throw new CayenneRuntimeException("Possible recursive supplier chain for batch row value, object %s, attribute %s" - , objectId, attribute.getName()); - } - - // if a value is a Supplier, resolve it here... - if (isSupplier) { - valueMap.put(attribute.getName(), value); - - // update replacement id - if (attribute.isPrimaryKey()) { - // sanity check - if (value == null) { - String name = attribute.getEntity() != null ? attribute.getEntity().getName() : "<null>"; - throw new CayenneRuntimeException("Failed to generate PK: %s.%s", name, attribute.getName()); - } - - ObjectId id = getObjectId(); - if (id != null) { - // always override with fresh value as this is what's in the DB - id.getReplacementIdMap().put(attribute.getName(), value); - } - } - } - - return value; + return valueMap.get(attribute.getName()); } } diff --git a/cayenne/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java b/cayenne/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java index ca857c6fc..3daea7263 100644 --- a/cayenne/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java +++ b/cayenne/src/main/java/org/apache/cayenne/query/DeleteBatchQuery.java @@ -25,7 +25,6 @@ import org.apache.cayenne.map.DbEntity; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.function.Supplier; /** * Batched delete query. @@ -83,11 +82,7 @@ public class DeleteBatchQuery extends BatchQuery { rows.add(new BatchQueryRow(null, objectId) { @Override public Object getValue(int i) { - Object value = qualifier.get(dbAttributes.get(i).getName()); - if (value instanceof Supplier) { - return ((Supplier) value).get(); - } - return value; + return getValue(qualifier, dbAttributes.get(i)); } }); } diff --git a/cayenne/src/main/java/org/apache/cayenne/query/InsertBatchQuery.java b/cayenne/src/main/java/org/apache/cayenne/query/InsertBatchQuery.java index c004bee95..6a08f0f6d 100644 --- a/cayenne/src/main/java/org/apache/cayenne/query/InsertBatchQuery.java +++ b/cayenne/src/main/java/org/apache/cayenne/query/InsertBatchQuery.java @@ -51,8 +51,8 @@ public class InsertBatchQuery extends BatchQuery { * Adds a snapshot to batch. Optionally stores the object id for the * snapshot. Note that snapshot can hold either the real values or the * instances of java.util.Supplier that will be resolved - * to the actual value on the spot, thus allowing deferred propagated keys - * resolution. + * to the actual value when the row is bound to a PreparedStatement, thus + * allowing deferred propagated keys resolution. * * @since 1.2 */
