This is an automated email from the ASF dual-hosted git repository.
mthomsen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push:
new 405c393 NIFI-8031: Add UPSERT capability for MySQL in
PutDatabaseRecord
405c393 is described below
commit 405c393cb59407aec89280934e353db7252b4b20
Author: Matthew Burgess <[email protected]>
AuthorDate: Thu Nov 19 15:52:47 2020 -0500
NIFI-8031: Add UPSERT capability for MySQL in PutDatabaseRecord
This closes #4678
Signed-off-by: Mike Thomsen <[email protected]>
---
.../processors/standard/PutDatabaseRecord.java | 12 ++-
.../processors/standard/db/DatabaseAdapter.java | 10 ++
.../standard/db/impl/MySQLDatabaseAdapter.java | 54 ++++++++++
.../standard/db/impl/TestMySQLDatabaseAdapter.java | 109 +++++++++++++++++++++
4 files changed, 184 insertions(+), 1 deletion(-)
diff --git
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/PutDatabaseRecord.java
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/PutDatabaseRecord.java
index d6384d2..0a27ebb 100644
---
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/PutDatabaseRecord.java
+++
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/PutDatabaseRecord.java
@@ -450,7 +450,7 @@ public class PutDatabaseRecord extends
AbstractSessionFactoryProcessor {
}, (fc, inputFlowFile, r, e) -> {
- getLogger().warn("Failed to process {} due to {}", new
Object[]{inputFlowFile, e}, e);
+ getLogger().error("Failed to process {} due to {}", new
Object[]{inputFlowFile, e}, e);
// Check if there was a BatchUpdateException or if multiple SQL
statements were being executed and one failed
final String statementTypeProperty =
context.getProperty(STATEMENT_TYPE).getValue();
@@ -752,6 +752,11 @@ public class PutDatabaseRecord extends
AbstractSessionFactoryProcessor {
if (DELETE_TYPE.equalsIgnoreCase(statementType)) {
ps.setObject(i * 2 + 1, currentValue, sqlType);
ps.setObject(i * 2 + 2, currentValue, sqlType);
+ } else if
(UPSERT_TYPE.equalsIgnoreCase(statementType)) {
+ final int timesToAddObjects =
databaseAdapter.getTimesToAddColumnObjectsForUpsert();
+ for (int j = 0; j < timesToAddObjects; j++) {
+ ps.setObject(i + (fieldIndexes.size() * j)
+ 1, currentValue, sqlType);
+ }
} else {
ps.setObject(i + 1, currentValue, sqlType);
}
@@ -766,6 +771,11 @@ public class PutDatabaseRecord extends
AbstractSessionFactoryProcessor {
if (DELETE_TYPE.equalsIgnoreCase(statementType)) {
ps.setObject(i * 2 + 1, currentValue, sqlType);
ps.setObject(i * 2 + 2, currentValue, sqlType);
+ } else if
(UPSERT_TYPE.equalsIgnoreCase(statementType)) {
+ final int timesToAddObjects =
databaseAdapter.getTimesToAddColumnObjectsForUpsert();
+ for (int j = 0; j < timesToAddObjects; j++) {
+ ps.setObject(i + (fieldIndexes.size() * j)
+ 1, currentValue, sqlType);
+ }
} else {
ps.setObject(i + 1, currentValue, sqlType);
}
diff --git
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/db/DatabaseAdapter.java
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/db/DatabaseAdapter.java
index 40de0b8..1fba3f6 100644
---
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/db/DatabaseAdapter.java
+++
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/db/DatabaseAdapter.java
@@ -68,6 +68,16 @@ public interface DatabaseAdapter {
}
/**
+ * Tells How many times the column values need to be inserted into the
prepared statement. Some DBs (such as MySQL) need the values specified twice in
the statement,
+ * some need only to specify them once.
+ *
+ * @return An integer corresponding to the number of times to insert
column values into the prepared statement for UPSERT, or -1 if upsert is not
supported.
+ */
+ default int getTimesToAddColumnObjectsForUpsert() {
+ return supportsUpsert() ? 1 : -1;
+ }
+
+ /**
* Returns an SQL UPSERT statement - i.e. UPDATE record or INSERT if id
doesn't exist.
* <br /><br />
* There is no standard way of doing this so not all adapters support it -
use together with {@link #supportsUpsert()}!
diff --git
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/db/impl/MySQLDatabaseAdapter.java
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/db/impl/MySQLDatabaseAdapter.java
index fdbc205..cba7768 100644
---
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/db/impl/MySQLDatabaseAdapter.java
+++
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/db/impl/MySQLDatabaseAdapter.java
@@ -16,6 +16,14 @@
*/
package org.apache.nifi.processors.standard.db.impl;
+import com.google.common.base.Preconditions;
+import org.apache.nifi.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
/**
* A generic database adapter that generates MySQL compatible SQL.
*/
@@ -35,4 +43,50 @@ public class MySQLDatabaseAdapter extends
GenericDatabaseAdapter {
// Removes double quotes and back-ticks.
return identifier == null ? null : identifier.replaceAll("[\"`]", "");
}
+
+ @Override
+ public boolean supportsUpsert() {
+ return true;
+ }
+
+ /**
+ * Tells How many times the column values need to be inserted into the
prepared statement. Some DBs (such as MySQL) need the values specified twice in
the statement,
+ * some need only to specify them once.
+ *
+ * @return An integer corresponding to the number of times to insert
column values into the prepared statement for UPSERT, or -1 if upsert is not
supported.
+ */
+ @Override
+ public int getTimesToAddColumnObjectsForUpsert() {
+ return 2;
+ }
+
+ @Override
+ public String getUpsertStatement(String table, List<String> columnNames,
Collection<String> uniqueKeyColumnNames) {
+ Preconditions.checkArgument(!StringUtils.isEmpty(table), "Table name
cannot be null or blank");
+ Preconditions.checkArgument(columnNames != null &&
!columnNames.isEmpty(), "Column names cannot be null or empty");
+ Preconditions.checkArgument(uniqueKeyColumnNames != null &&
!uniqueKeyColumnNames.isEmpty(), "Key column names cannot be null or empty");
+
+ String columns = columnNames.stream()
+ .collect(Collectors.joining(", "));
+
+ String parameterizedInsertValues = columnNames.stream()
+ .map(__ -> "?")
+ .collect(Collectors.joining(", "));
+
+ List<String> updateValues = new ArrayList<>();
+ for (int i = 0; i < columnNames.size(); i++) {
+ updateValues.add(columnNames.get(i) + " = ?");
+ }
+ String parameterizedUpdateValues = String.join(", ", updateValues);
+
+ StringBuilder statementStringBuilder = new StringBuilder("INSERT INTO
")
+ .append(table)
+ .append("(").append(columns).append(")")
+ .append(" VALUES ")
+ .append("(").append(parameterizedInsertValues).append(")")
+ .append(" ON DUPLICATE KEY UPDATE ")
+ .append(parameterizedUpdateValues);
+
+ return statementStringBuilder.toString();
+ }
}
diff --git
a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/db/impl/TestMySQLDatabaseAdapter.java
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/db/impl/TestMySQLDatabaseAdapter.java
new file mode 100644
index 0000000..a5f6eb7
--- /dev/null
+++
b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/db/impl/TestMySQLDatabaseAdapter.java
@@ -0,0 +1,109 @@
+/*
+ * 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
+ *
+ * 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.
+ */
+package org.apache.nifi.processors.standard.db.impl;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class TestMySQLDatabaseAdapter {
+
+ private MySQLDatabaseAdapter testSubject;
+
+ @Before
+ public void setUp() throws Exception {
+ testSubject = new MySQLDatabaseAdapter();
+ }
+
+ @Test
+ public void testSupportsUpsert() throws Exception {
+ assertTrue(testSubject.getClass().getSimpleName() + " should support
upsert", testSubject.supportsUpsert());
+ }
+
+ @Test
+ public void testGetUpsertStatementWithNullTableName() throws Exception {
+ testGetUpsertStatement(null, Collections.singletonList("notEmpty"),
Collections.singletonList("notEmpty"), new IllegalArgumentException("Table name
cannot be null or blank"));
+ }
+
+ @Test
+ public void testGetUpsertStatementWithBlankTableName() throws Exception {
+ testGetUpsertStatement("", Collections.singletonList("notEmpty"),
Collections.singletonList("notEmpty"), new IllegalArgumentException("Table name
cannot be null or blank"));
+ }
+
+ @Test
+ public void testGetUpsertStatementWithNullColumnNames() throws Exception {
+ testGetUpsertStatement("notEmpty", null,
Collections.singletonList("notEmpty"), new IllegalArgumentException("Column
names cannot be null or empty"));
+ }
+
+ @Test
+ public void testGetUpsertStatementWithEmptyColumnNames() throws Exception {
+ testGetUpsertStatement("notEmpty", Collections.emptyList(),
Collections.singletonList("notEmpty"), new IllegalArgumentException("Column
names cannot be null or empty"));
+ }
+
+ @Test
+ public void testGetUpsertStatementWithNullKeyColumnNames() throws
Exception {
+ testGetUpsertStatement("notEmpty",
Collections.singletonList("notEmpty"), null, new IllegalArgumentException("Key
column names cannot be null or empty"));
+ }
+
+ @Test
+ public void testGetUpsertStatementWithEmptyKeyColumnNames() throws
Exception {
+ testGetUpsertStatement("notEmpty",
Collections.singletonList("notEmpty"), Collections.emptyList(), new
IllegalArgumentException("Key column names cannot be null or empty"));
+ }
+
+ @Test
+ public void testGetUpsertStatement() {
+ // GIVEN
+ String tableName = "table";
+ List<String> columnNames = Arrays.asList("column1", "column2",
"column3", "column4");
+ Collection<String> uniqueKeyColumnNames = Arrays.asList("column2",
"column4");
+
+ String expected = "INSERT INTO" +
+ " table(column1, column2, column3, column4) VALUES (?, ?, ?,
?)" +
+ " ON DUPLICATE KEY UPDATE" +
+ " column1 = ?, column2 = ?, column3 = ?, column4 = ?";
+
+ // WHEN
+ // THEN
+ testGetUpsertStatement(tableName, columnNames, uniqueKeyColumnNames,
expected);
+ }
+
+ private void testGetUpsertStatement(String tableName, List<String>
columnNames, Collection<String> uniqueKeyColumnNames, IllegalArgumentException
expected) {
+ try {
+ testGetUpsertStatement(tableName, columnNames,
uniqueKeyColumnNames, (String) null);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals(expected.getMessage(), e.getMessage());
+ }
+ }
+
+ private void testGetUpsertStatement(String tableName, List<String>
columnNames, Collection<String> uniqueKeyColumnNames, String expected) {
+ // WHEN
+ String actual = testSubject.getUpsertStatement(tableName, columnNames,
uniqueKeyColumnNames);
+
+ // THEN
+ assertEquals(expected, actual);
+ }
+
+}
\ No newline at end of file