mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
CE-1107: added ability to use replaceAction specifying that null key values be treated as equal
This commit is contained in:
@ -47,6 +47,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
import org.apache.commons.lang.BooleanUtils;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -82,6 +83,8 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
|
|||||||
QTableMetaData table = input.getTable();
|
QTableMetaData table = input.getTable();
|
||||||
UniqueKey uniqueKey = input.getKey();
|
UniqueKey uniqueKey = input.getKey();
|
||||||
String primaryKeyField = table.getPrimaryKeyField();
|
String primaryKeyField = table.getPrimaryKeyField();
|
||||||
|
boolean allowNullKeyValuesToEqual = BooleanUtils.isTrue(input.getAllowNullKeyValuesToEqual());
|
||||||
|
|
||||||
if(transaction == null)
|
if(transaction == null)
|
||||||
{
|
{
|
||||||
transaction = QBackendTransaction.openFor(new InsertInput(input.getTableName()));
|
transaction = QBackendTransaction.openFor(new InsertInput(input.getTableName()));
|
||||||
@ -98,10 +101,11 @@ public class ReplaceAction extends AbstractQActionFunction<ReplaceInput, Replace
|
|||||||
// originally it was thought that we'd need to pass the filter in here //
|
// originally it was thought that we'd need to pass the filter in here //
|
||||||
// but, it's been decided not to. the filter only applies to what we can delete //
|
// but, it's been decided not to. the filter only applies to what we can delete //
|
||||||
///////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
Map<List<Serializable>, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey);
|
Map<List<Serializable>, Serializable> existingKeys = UniqueKeyHelper.getExistingKeys(transaction, table, page, uniqueKey, allowNullKeyValuesToEqual);
|
||||||
|
|
||||||
for(QRecord record : page)
|
for(QRecord record : page)
|
||||||
{
|
{
|
||||||
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record);
|
Optional<List<Serializable>> keyValues = UniqueKeyHelper.getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual);
|
||||||
if(keyValues.isPresent())
|
if(keyValues.isPresent())
|
||||||
{
|
{
|
||||||
if(existingKeys.containsKey(keyValues.get()))
|
if(existingKeys.containsKey(keyValues.get()))
|
||||||
|
@ -54,7 +54,7 @@ public class UniqueKeyHelper
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static Map<List<Serializable>, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey) throws QException
|
public static Map<List<Serializable>, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey, boolean allowNullKeyValuesToEqual) throws QException
|
||||||
{
|
{
|
||||||
List<String> ukFieldNames = uniqueKey.getFieldNames();
|
List<String> ukFieldNames = uniqueKey.getFieldNames();
|
||||||
Map<List<Serializable>, Serializable> existingRecords = new HashMap<>();
|
Map<List<Serializable>, Serializable> existingRecords = new HashMap<>();
|
||||||
@ -112,7 +112,7 @@ public class UniqueKeyHelper
|
|||||||
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
QueryOutput queryOutput = new QueryAction().execute(queryInput);
|
||||||
for(QRecord record : queryOutput.getRecords())
|
for(QRecord record : queryOutput.getRecords())
|
||||||
{
|
{
|
||||||
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record);
|
Optional<List<Serializable>> keyValues = getKeyValues(table, uniqueKey, record, allowNullKeyValuesToEqual);
|
||||||
if(keyValues.isPresent())
|
if(keyValues.isPresent())
|
||||||
{
|
{
|
||||||
existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField()));
|
existingRecords.put(keyValues.get(), record.getValue(table.getPrimaryKeyField()));
|
||||||
@ -128,7 +128,17 @@ public class UniqueKeyHelper
|
|||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record)
|
public static Map<List<Serializable>, Serializable> getExistingKeys(QBackendTransaction transaction, QTableMetaData table, List<QRecord> recordList, UniqueKey uniqueKey) throws QException
|
||||||
|
{
|
||||||
|
return (getExistingKeys(transaction, table, recordList, uniqueKey, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record, boolean allowNullKeyValuesToEqual)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -138,7 +148,19 @@ public class UniqueKeyHelper
|
|||||||
QFieldMetaData field = table.getField(fieldName);
|
QFieldMetaData field = table.getField(fieldName);
|
||||||
Serializable value = record.getValue(fieldName);
|
Serializable value = record.getValue(fieldName);
|
||||||
Serializable typedValue = ValueUtils.getValueAsFieldType(field.getType(), value);
|
Serializable typedValue = ValueUtils.getValueAsFieldType(field.getType(), value);
|
||||||
keyValues.add(typedValue == null ? new NullUniqueKeyValue() : typedValue);
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// if null value, look at flag to determine if a null should be used (which will //
|
||||||
|
// allow keys to match), or a NullUniqueKeyValue, (which will never match) //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////
|
||||||
|
if(typedValue == null)
|
||||||
|
{
|
||||||
|
keyValues.add(allowNullKeyValuesToEqual ? null : new NullUniqueKeyValue());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
keyValues.add(typedValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (Optional.of(keyValues));
|
return (Optional.of(keyValues));
|
||||||
}
|
}
|
||||||
@ -150,6 +172,16 @@ public class UniqueKeyHelper
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static Optional<List<Serializable>> getKeyValues(QTableMetaData table, UniqueKey uniqueKey, QRecord record)
|
||||||
|
{
|
||||||
|
return (getKeyValues(table, uniqueKey, record, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** To make a list of unique key values here behave like they do in an RDBMS
|
** To make a list of unique key values here behave like they do in an RDBMS
|
||||||
** (which is what we're trying to mimic - which is - 2 null values in a field
|
** (which is what we're trying to mimic - which is - 2 null values in a field
|
||||||
|
@ -40,6 +40,7 @@ public class ReplaceInput extends AbstractTableActionInput
|
|||||||
private List<QRecord> records;
|
private List<QRecord> records;
|
||||||
private QQueryFilter filter;
|
private QQueryFilter filter;
|
||||||
private boolean performDeletes = true;
|
private boolean performDeletes = true;
|
||||||
|
private boolean allowNullKeyValuesToEqual = false;
|
||||||
|
|
||||||
private boolean omitDmlAudit = false;
|
private boolean omitDmlAudit = false;
|
||||||
|
|
||||||
@ -239,4 +240,35 @@ public class ReplaceInput extends AbstractTableActionInput
|
|||||||
return (this);
|
return (this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Getter for allowNullKeyValuesToEqual
|
||||||
|
*******************************************************************************/
|
||||||
|
public boolean getAllowNullKeyValuesToEqual()
|
||||||
|
{
|
||||||
|
return (this.allowNullKeyValuesToEqual);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Setter for allowNullKeyValuesToEqual
|
||||||
|
*******************************************************************************/
|
||||||
|
public void setAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual)
|
||||||
|
{
|
||||||
|
this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Fluent setter for allowNullKeyValuesToEqual
|
||||||
|
*******************************************************************************/
|
||||||
|
public ReplaceInput withAllowNullKeyValuesToEqual(boolean allowNullKeyValuesToEqual)
|
||||||
|
{
|
||||||
|
this.allowNullKeyValuesToEqual = allowNullKeyValuesToEqual;
|
||||||
|
return (this);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -157,6 +157,134 @@ class ReplaceActionTest extends BaseTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testTwoKeysWithNullsNotMatchingAllowingDelete() throws QException
|
||||||
|
{
|
||||||
|
String tableName = TestUtils.TABLE_NAME_TWO_KEYS;
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// start with these 2 records //
|
||||||
|
////////////////////////////////
|
||||||
|
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(
|
||||||
|
new QRecord().withValue("key1", 1).withValue("key2", 2),
|
||||||
|
new QRecord().withValue("key1", 3)
|
||||||
|
)));
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
// now do a replace action that just updates them //
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
List<QRecord> newThings = List.of(
|
||||||
|
new QRecord().withValue("key1", 1).withValue("key2", 2),
|
||||||
|
new QRecord().withValue("key1", 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// replace allowing deletes //
|
||||||
|
//////////////////////////////
|
||||||
|
ReplaceInput replaceInput = new ReplaceInput();
|
||||||
|
replaceInput.setTableName(tableName);
|
||||||
|
replaceInput.setKey(new UniqueKey("key1", "key2"));
|
||||||
|
replaceInput.setOmitDmlAudit(true);
|
||||||
|
replaceInput.setRecords(newThings);
|
||||||
|
replaceInput.setFilter(null);
|
||||||
|
ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput);
|
||||||
|
|
||||||
|
assertEquals(1, replaceOutput.getInsertOutput().getRecords().size());
|
||||||
|
assertEquals(1, replaceOutput.getUpdateOutput().getRecords().size());
|
||||||
|
assertEquals(1, replaceOutput.getDeleteOutput().getDeletedRecordCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testTwoKeysWithNullsNotMatchingNotAllowingDelete() throws QException
|
||||||
|
{
|
||||||
|
String tableName = TestUtils.TABLE_NAME_TWO_KEYS;
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// start with these 2 records //
|
||||||
|
////////////////////////////////
|
||||||
|
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(
|
||||||
|
new QRecord().withValue("key1", 1).withValue("key2", 2),
|
||||||
|
new QRecord().withValue("key1", 3)
|
||||||
|
)));
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
// now do a replace action that just updates them //
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
List<QRecord> newThings = List.of(
|
||||||
|
new QRecord().withValue("key1", 1).withValue("key2", 2),
|
||||||
|
new QRecord().withValue("key1", 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
/////////////////////////////////
|
||||||
|
// replace disallowing deletes //
|
||||||
|
/////////////////////////////////
|
||||||
|
ReplaceInput replaceInput = new ReplaceInput();
|
||||||
|
replaceInput.setTableName(tableName);
|
||||||
|
replaceInput.setKey(new UniqueKey("key1", "key2"));
|
||||||
|
replaceInput.setOmitDmlAudit(true);
|
||||||
|
replaceInput.setRecords(newThings);
|
||||||
|
replaceInput.setFilter(null);
|
||||||
|
replaceInput.setPerformDeletes(false);
|
||||||
|
ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput);
|
||||||
|
|
||||||
|
assertEquals(1, replaceOutput.getInsertOutput().getRecords().size());
|
||||||
|
assertEquals(1, replaceOutput.getUpdateOutput().getRecords().size());
|
||||||
|
assertNull(replaceOutput.getDeleteOutput());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testTwoKeysWithNullMatching() throws QException
|
||||||
|
{
|
||||||
|
String tableName = TestUtils.TABLE_NAME_TWO_KEYS;
|
||||||
|
|
||||||
|
////////////////////////////////
|
||||||
|
// start with these 2 records //
|
||||||
|
////////////////////////////////
|
||||||
|
new InsertAction().execute(new InsertInput(tableName).withRecords(List.of(
|
||||||
|
new QRecord().withValue("key1", 1).withValue("key2", 2),
|
||||||
|
new QRecord().withValue("key1", 3)
|
||||||
|
)));
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
// now do a replace action that just updates them //
|
||||||
|
////////////////////////////////////////////////////
|
||||||
|
List<QRecord> newThings = List.of(
|
||||||
|
new QRecord().withValue("key1", 1).withValue("key2", 2),
|
||||||
|
new QRecord().withValue("key1", 3)
|
||||||
|
);
|
||||||
|
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
// replace treating null key values as equal //
|
||||||
|
///////////////////////////////////////////////
|
||||||
|
ReplaceInput replaceInput = new ReplaceInput();
|
||||||
|
replaceInput.setTableName(tableName);
|
||||||
|
replaceInput.setKey(new UniqueKey("key1", "key2"));
|
||||||
|
replaceInput.setOmitDmlAudit(true);
|
||||||
|
replaceInput.setRecords(newThings);
|
||||||
|
replaceInput.setFilter(null);
|
||||||
|
replaceInput.setAllowNullKeyValuesToEqual(true);
|
||||||
|
ReplaceOutput replaceOutput = new ReplaceAction().execute(replaceInput);
|
||||||
|
|
||||||
|
assertEquals(0, replaceOutput.getInsertOutput().getRecords().size());
|
||||||
|
assertEquals(2, replaceOutput.getUpdateOutput().getRecords().size());
|
||||||
|
assertEquals(0, replaceOutput.getDeleteOutput().getDeletedRecordCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -138,6 +138,7 @@ public class TestUtils
|
|||||||
public static final String APP_NAME_PEOPLE = "peopleApp";
|
public static final String APP_NAME_PEOPLE = "peopleApp";
|
||||||
public static final String APP_NAME_MISCELLANEOUS = "miscellaneous";
|
public static final String APP_NAME_MISCELLANEOUS = "miscellaneous";
|
||||||
|
|
||||||
|
public static final String TABLE_NAME_TWO_KEYS = "twoKeys";
|
||||||
public static final String TABLE_NAME_PERSON = "person";
|
public static final String TABLE_NAME_PERSON = "person";
|
||||||
public static final String TABLE_NAME_SHAPE = "shape";
|
public static final String TABLE_NAME_SHAPE = "shape";
|
||||||
public static final String TABLE_NAME_SHAPE_CACHE = "shapeCache";
|
public static final String TABLE_NAME_SHAPE_CACHE = "shapeCache";
|
||||||
@ -196,6 +197,7 @@ public class TestUtils
|
|||||||
qInstance.addBackend(defineMemoryBackend());
|
qInstance.addBackend(defineMemoryBackend());
|
||||||
|
|
||||||
qInstance.addTable(defineTablePerson());
|
qInstance.addTable(defineTablePerson());
|
||||||
|
qInstance.addTable(defineTableTwoKeys());
|
||||||
qInstance.addTable(definePersonFileTable());
|
qInstance.addTable(definePersonFileTable());
|
||||||
qInstance.addTable(definePersonMemoryTable());
|
qInstance.addTable(definePersonMemoryTable());
|
||||||
qInstance.addTable(definePersonMemoryCacheTable());
|
qInstance.addTable(definePersonMemoryCacheTable());
|
||||||
@ -545,6 +547,24 @@ public class TestUtils
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Define the 'two key' table used in standard tests.
|
||||||
|
*******************************************************************************/
|
||||||
|
public static QTableMetaData defineTableTwoKeys()
|
||||||
|
{
|
||||||
|
return new QTableMetaData()
|
||||||
|
.withName(TABLE_NAME_TWO_KEYS)
|
||||||
|
.withLabel("Two Keys")
|
||||||
|
.withBackendName(MEMORY_BACKEND_NAME)
|
||||||
|
.withPrimaryKeyField("id")
|
||||||
|
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||||
|
.withUniqueKey(new UniqueKey("key1", "key2"))
|
||||||
|
.withField(new QFieldMetaData("key1", QFieldType.INTEGER))
|
||||||
|
.withField(new QFieldMetaData("key2", QFieldType.INTEGER));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Define the 'person' table used in standard tests.
|
** Define the 'person' table used in standard tests.
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -791,6 +811,26 @@ public class TestUtils
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
** Define a table with unique key where one is nullable
|
||||||
|
*******************************************************************************/
|
||||||
|
public static QTableMetaData defineTwoKeyTable()
|
||||||
|
{
|
||||||
|
return (new QTableMetaData()
|
||||||
|
.withName(TABLE_NAME_BASEPULL)
|
||||||
|
.withLabel("Basepull Test")
|
||||||
|
.withPrimaryKeyField("id")
|
||||||
|
.withBackendName(MEMORY_BACKEND_NAME)
|
||||||
|
.withFields(TestUtils.defineTablePerson().getFields()))
|
||||||
|
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||||
|
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withBackendName("create_date").withIsEditable(false))
|
||||||
|
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withBackendName("modify_date").withIsEditable(false))
|
||||||
|
.withField(new QFieldMetaData(BASEPULL_KEY_FIELD_NAME, QFieldType.STRING).withBackendName("process_name").withIsRequired(true))
|
||||||
|
.withField(new QFieldMetaData(BASEPULL_LAST_RUN_TIME_FIELD_NAME, QFieldType.DATE_TIME).withBackendName("last_run_time").withIsRequired(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
** Define a basepullTable
|
** Define a basepullTable
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
Reference in New Issue
Block a user