mirror of
https://github.com/apache/nifi.git
synced 2025-02-06 01:58:32 +00:00
NIFI-13642 Added Delete Keys Property to PutDatabaseRecord
- Delete Keys property enables targeted deletes for databases that do not support primary keys This closes #9162 Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
parent
abe41ff649
commit
d4344a3140
@ -16,8 +16,43 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.nifi.processors.standard;
|
package org.apache.nifi.processors.standard;
|
||||||
|
|
||||||
|
import static java.lang.String.format;
|
||||||
|
import static org.apache.nifi.expression.ExpressionLanguageScope.ENVIRONMENT;
|
||||||
|
import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
|
||||||
|
import static org.apache.nifi.expression.ExpressionLanguageScope.NONE;
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.cache.Cache;
|
import com.github.benmanes.caffeine.cache.Cache;
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.sql.BatchUpdateException;
|
||||||
|
import java.sql.Clob;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DatabaseMetaData;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.SQLDataException;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.SQLIntegrityConstraintViolationException;
|
||||||
|
import java.sql.SQLTransientException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.HexFormat;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.ServiceLoader;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.nifi.annotation.behavior.InputRequirement;
|
import org.apache.nifi.annotation.behavior.InputRequirement;
|
||||||
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
|
import org.apache.nifi.annotation.behavior.InputRequirement.Requirement;
|
||||||
@ -60,40 +95,6 @@ import org.apache.nifi.serialization.record.RecordSchema;
|
|||||||
import org.apache.nifi.serialization.record.util.DataTypeUtils;
|
import org.apache.nifi.serialization.record.util.DataTypeUtils;
|
||||||
import org.apache.nifi.serialization.record.util.IllegalTypeConversionException;
|
import org.apache.nifi.serialization.record.util.IllegalTypeConversionException;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.sql.BatchUpdateException;
|
|
||||||
import java.sql.Clob;
|
|
||||||
import java.sql.Connection;
|
|
||||||
import java.sql.DatabaseMetaData;
|
|
||||||
import java.sql.PreparedStatement;
|
|
||||||
import java.sql.SQLDataException;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.sql.SQLIntegrityConstraintViolationException;
|
|
||||||
import java.sql.SQLTransientException;
|
|
||||||
import java.sql.Statement;
|
|
||||||
import java.sql.Types;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.HexFormat;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.ServiceLoader;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import static java.lang.String.format;
|
|
||||||
import static org.apache.nifi.expression.ExpressionLanguageScope.ENVIRONMENT;
|
|
||||||
import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
|
|
||||||
import static org.apache.nifi.expression.ExpressionLanguageScope.NONE;
|
|
||||||
|
|
||||||
@InputRequirement(Requirement.INPUT_REQUIRED)
|
@InputRequirement(Requirement.INPUT_REQUIRED)
|
||||||
@Tags({"sql", "record", "jdbc", "put", "database", "update", "insert", "delete"})
|
@Tags({"sql", "record", "jdbc", "put", "database", "update", "insert", "delete"})
|
||||||
@CapabilityDescription("The PutDatabaseRecord processor uses a specified RecordReader to input (possibly multiple) records from an incoming flow file. These records are translated to SQL "
|
@CapabilityDescription("The PutDatabaseRecord processor uses a specified RecordReader to input (possibly multiple) records from an incoming flow file. These records are translated to SQL "
|
||||||
@ -306,6 +307,17 @@ public class PutDatabaseRecord extends AbstractProcessor {
|
|||||||
.dependsOn(STATEMENT_TYPE, UPDATE_TYPE, UPSERT_TYPE, SQL_TYPE, USE_ATTR_TYPE, USE_RECORD_PATH)
|
.dependsOn(STATEMENT_TYPE, UPDATE_TYPE, UPSERT_TYPE, SQL_TYPE, USE_ATTR_TYPE, USE_RECORD_PATH)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
static final PropertyDescriptor DELETE_KEYS = new Builder()
|
||||||
|
.name("Delete Keys")
|
||||||
|
.description("A comma-separated list of column names that uniquely identifies a row in the database for DELETE statements. "
|
||||||
|
+ "If the Statement Type is DELETE and this property is not set, the table's columns are used. "
|
||||||
|
+ "This property is ignored if the Statement Type is not DELETE")
|
||||||
|
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
|
||||||
|
.required(false)
|
||||||
|
.expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
|
||||||
|
.dependsOn(STATEMENT_TYPE, DELETE_TYPE, SQL_TYPE, USE_ATTR_TYPE, USE_RECORD_PATH)
|
||||||
|
.build();
|
||||||
|
|
||||||
static final PropertyDescriptor FIELD_CONTAINING_SQL = new Builder()
|
static final PropertyDescriptor FIELD_CONTAINING_SQL = new Builder()
|
||||||
.name("put-db-record-field-containing-sql")
|
.name("put-db-record-field-containing-sql")
|
||||||
.displayName("Field Containing SQL")
|
.displayName("Field Containing SQL")
|
||||||
@ -427,6 +439,7 @@ public class PutDatabaseRecord extends AbstractProcessor {
|
|||||||
UNMATCHED_FIELD_BEHAVIOR,
|
UNMATCHED_FIELD_BEHAVIOR,
|
||||||
UNMATCHED_COLUMN_BEHAVIOR,
|
UNMATCHED_COLUMN_BEHAVIOR,
|
||||||
UPDATE_KEYS,
|
UPDATE_KEYS,
|
||||||
|
DELETE_KEYS,
|
||||||
FIELD_CONTAINING_SQL,
|
FIELD_CONTAINING_SQL,
|
||||||
ALLOW_MULTIPLE_STATEMENTS,
|
ALLOW_MULTIPLE_STATEMENTS,
|
||||||
QUOTE_IDENTIFIERS,
|
QUOTE_IDENTIFIERS,
|
||||||
@ -730,6 +743,7 @@ public class PutDatabaseRecord extends AbstractProcessor {
|
|||||||
final String schemaName = context.getProperty(SCHEMA_NAME).evaluateAttributeExpressions(flowFile).getValue();
|
final String schemaName = context.getProperty(SCHEMA_NAME).evaluateAttributeExpressions(flowFile).getValue();
|
||||||
final String tableName = context.getProperty(TABLE_NAME).evaluateAttributeExpressions(flowFile).getValue();
|
final String tableName = context.getProperty(TABLE_NAME).evaluateAttributeExpressions(flowFile).getValue();
|
||||||
final String updateKeys = context.getProperty(UPDATE_KEYS).evaluateAttributeExpressions(flowFile).getValue();
|
final String updateKeys = context.getProperty(UPDATE_KEYS).evaluateAttributeExpressions(flowFile).getValue();
|
||||||
|
final String deleteKeys = context.getProperty(DELETE_KEYS).evaluateAttributeExpressions(flowFile).getValue();
|
||||||
final int maxBatchSize = context.getProperty(MAX_BATCH_SIZE).evaluateAttributeExpressions(flowFile).asInteger();
|
final int maxBatchSize = context.getProperty(MAX_BATCH_SIZE).evaluateAttributeExpressions(flowFile).asInteger();
|
||||||
final int timeoutMillis = context.getProperty(QUERY_TIMEOUT).evaluateAttributeExpressions().asTimePeriod(TimeUnit.MILLISECONDS).intValue();
|
final int timeoutMillis = context.getProperty(QUERY_TIMEOUT).evaluateAttributeExpressions().asTimePeriod(TimeUnit.MILLISECONDS).intValue();
|
||||||
|
|
||||||
@ -795,7 +809,7 @@ public class PutDatabaseRecord extends AbstractProcessor {
|
|||||||
} else if (UPDATE_TYPE.equalsIgnoreCase(statementType)) {
|
} else if (UPDATE_TYPE.equalsIgnoreCase(statementType)) {
|
||||||
sqlHolder = generateUpdate(recordSchema, fqTableName, updateKeys, tableSchema, settings);
|
sqlHolder = generateUpdate(recordSchema, fqTableName, updateKeys, tableSchema, settings);
|
||||||
} else if (DELETE_TYPE.equalsIgnoreCase(statementType)) {
|
} else if (DELETE_TYPE.equalsIgnoreCase(statementType)) {
|
||||||
sqlHolder = generateDelete(recordSchema, fqTableName, tableSchema, settings);
|
sqlHolder = generateDelete(recordSchema, fqTableName, deleteKeys, tableSchema, settings);
|
||||||
} else if (UPSERT_TYPE.equalsIgnoreCase(statementType)) {
|
} else if (UPSERT_TYPE.equalsIgnoreCase(statementType)) {
|
||||||
sqlHolder = generateUpsert(recordSchema, fqTableName, updateKeys, tableSchema, settings);
|
sqlHolder = generateUpsert(recordSchema, fqTableName, updateKeys, tableSchema, settings);
|
||||||
} else if (INSERT_IGNORE_TYPE.equalsIgnoreCase(statementType)) {
|
} else if (INSERT_IGNORE_TYPE.equalsIgnoreCase(statementType)) {
|
||||||
@ -1443,7 +1457,7 @@ public class PutDatabaseRecord extends AbstractProcessor {
|
|||||||
return new SqlAndIncludedColumns(sqlBuilder.toString(), includedColumns);
|
return new SqlAndIncludedColumns(sqlBuilder.toString(), includedColumns);
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlAndIncludedColumns generateDelete(final RecordSchema recordSchema, final String tableName, final TableSchema tableSchema, final DMLSettings settings)
|
SqlAndIncludedColumns generateDelete(final RecordSchema recordSchema, final String tableName, String deleteKeys, final TableSchema tableSchema, final DMLSettings settings)
|
||||||
throws IllegalArgumentException, MalformedRecordException, SQLDataException {
|
throws IllegalArgumentException, MalformedRecordException, SQLDataException {
|
||||||
|
|
||||||
final Set<String> normalizedFieldNames = getNormalizedColumnNames(recordSchema, settings.translateFieldNames);
|
final Set<String> normalizedFieldNames = getNormalizedColumnNames(recordSchema, settings.translateFieldNames);
|
||||||
@ -1465,14 +1479,28 @@ public class PutDatabaseRecord extends AbstractProcessor {
|
|||||||
sqlBuilder.append(tableName);
|
sqlBuilder.append(tableName);
|
||||||
|
|
||||||
// iterate over all of the fields in the record, building the SQL statement by adding the column names
|
// iterate over all of the fields in the record, building the SQL statement by adding the column names
|
||||||
List<String> fieldNames = recordSchema.getFieldNames();
|
final List<String> fieldNames = recordSchema.getFieldNames();
|
||||||
final List<Integer> includedColumns = new ArrayList<>();
|
final List<Integer> includedColumns = new ArrayList<>();
|
||||||
if (fieldNames != null) {
|
if (fieldNames != null) {
|
||||||
sqlBuilder.append(" WHERE ");
|
sqlBuilder.append(" WHERE ");
|
||||||
int fieldCount = fieldNames.size();
|
int fieldCount = fieldNames.size();
|
||||||
AtomicInteger fieldsFound = new AtomicInteger(0);
|
AtomicInteger fieldsFound = new AtomicInteger(0);
|
||||||
|
|
||||||
|
// If 'deleteKeys' is not specified by the user, then all columns of the table
|
||||||
|
// should be used in the 'WHERE' clause, in order to keep the original behavior.
|
||||||
|
final Set<String> deleteKeysSet;
|
||||||
|
if (deleteKeys == null) {
|
||||||
|
deleteKeysSet = new HashSet<>(fieldNames);
|
||||||
|
} else {
|
||||||
|
deleteKeysSet = Arrays.stream(deleteKeys.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < fieldCount; i++) {
|
for (int i = 0; i < fieldCount; i++) {
|
||||||
|
if (!deleteKeysSet.contains(fieldNames.get(i))) {
|
||||||
|
continue; // skip this field if it should not be included in 'WHERE'
|
||||||
|
}
|
||||||
|
|
||||||
RecordField field = recordSchema.getField(i);
|
RecordField field = recordSchema.getField(i);
|
||||||
String fieldName = field.getFieldName();
|
String fieldName = field.getFieldName();
|
||||||
|
@ -405,7 +405,9 @@ public class PutDatabaseRecordTest {
|
|||||||
assertEquals("UPDATE PERSONS SET name = ?, code = ? WHERE id = ?",
|
assertEquals("UPDATE PERSONS SET name = ?, code = ? WHERE id = ?",
|
||||||
processor.generateUpdate(schema, "PERSONS", null, tableSchema, settings).getSql());
|
processor.generateUpdate(schema, "PERSONS", null, tableSchema, settings).getSql());
|
||||||
assertEquals("DELETE FROM PERSONS WHERE (id = ?) AND (name = ? OR (name is null AND ? is null)) AND (code = ? OR (code is null AND ? is null))",
|
assertEquals("DELETE FROM PERSONS WHERE (id = ?) AND (name = ? OR (name is null AND ? is null)) AND (code = ? OR (code is null AND ? is null))",
|
||||||
processor.generateDelete(schema, "PERSONS", tableSchema, settings).getSql());
|
processor.generateDelete(schema, "PERSONS", null, tableSchema, settings).getSql());
|
||||||
|
assertEquals("DELETE FROM PERSONS WHERE (id = ?) AND (code = ? OR (code is null AND ? is null))",
|
||||||
|
processor.generateDelete(schema, "PERSONS", "id, code", tableSchema, settings).getSql());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -450,7 +452,7 @@ public class PutDatabaseRecordTest {
|
|||||||
assertEquals("Cannot map field 'non_existing' to any column in the database\nColumns: id,name,code", e.getMessage());
|
assertEquals("Cannot map field 'non_existing' to any column in the database\nColumns: id,name,code", e.getMessage());
|
||||||
|
|
||||||
e = assertThrows(SQLDataException.class,
|
e = assertThrows(SQLDataException.class,
|
||||||
() -> processor.generateDelete(schema, "PERSONS", tableSchema, settings),
|
() -> processor.generateDelete(schema, "PERSONS", null, tableSchema, settings),
|
||||||
"generateDelete should fail with unmatched fields");
|
"generateDelete should fail with unmatched fields");
|
||||||
assertEquals("Cannot map field 'non_existing' to any column in the database\nColumns: id,name,code", e.getMessage());
|
assertEquals("Cannot map field 'non_existing' to any column in the database\nColumns: id,name,code", e.getMessage());
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user