From 8b6eb6325345757f14bf27604e5bdb20c0a2eb62 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 31 Oct 2023 08:20:48 -0500 Subject: [PATCH] CE-604 Rewrite copy constructor to try to not use SerializationUtils, which was seen as a runtime dominator during profiling bulk loads --- .../qqq/backend/core/model/data/QRecord.java | 60 ++++--- .../backend/core/model/data/QRecordTest.java | 155 ++++++++++++++++++ 2 files changed, 195 insertions(+), 20 deletions(-) create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java index 6cf505c0..49ae05ce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecord.java @@ -27,18 +27,21 @@ import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; import java.time.LocalTime; +import java.time.temporal.Temporal; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage; import com.kingsrook.qqq.backend.core.model.statusmessages.QWarningMessage; import com.kingsrook.qqq.backend.core.utils.ValueUtils; -import org.apache.commons.lang.SerializationUtils; +import org.apache.commons.lang3.SerializationUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -61,6 +64,8 @@ import org.apache.commons.lang.SerializationUtils; *******************************************************************************/ public class QRecord implements Serializable { + private static final QLogger LOG = QLogger.getLogger(QRecord.class); + private String tableName; private String recordLabel; @@ -110,12 +115,14 @@ public class QRecord implements Serializable this.tableName = record.tableName; this.recordLabel = record.recordLabel; - this.values = doDeepCopy(record.values); - this.displayValues = doDeepCopy(record.displayValues); - this.backendDetails = doDeepCopy(record.backendDetails); - this.errors = doDeepCopy(record.errors); - this.warnings = doDeepCopy(record.warnings); - this.associatedRecords = doDeepCopy(record.associatedRecords); + this.values = deepCopySimpleMap(record.values); + this.displayValues = deepCopySimpleMap(record.displayValues); + this.backendDetails = deepCopySimpleMap(record.backendDetails); + + this.associatedRecords = deepCopyAssociatedRecords(record.associatedRecords); + + this.errors = record.errors == null ? null : new ArrayList<>(record.errors); + this.warnings = record.warnings == null ? null : new ArrayList<>(record.warnings); } @@ -135,40 +142,53 @@ public class QRecord implements Serializable ** todo - move to a cloning utils maybe? *******************************************************************************/ @SuppressWarnings({ "unchecked" }) - private Map doDeepCopy(Map map) + private Map deepCopySimpleMap(Map map) { if(map == null) { return (null); } - if(map instanceof Serializable serializableMap) + Map clone = new LinkedHashMap<>(); + for(Map.Entry entry : map.entrySet()) { - return (Map) SerializationUtils.clone(serializableMap); + V value = entry.getValue(); + if(value == null || value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Temporal) + { + clone.put(entry.getKey(), entry.getValue()); + } + else if(entry.getValue() instanceof Serializable serializableValue) + { + LOG.info("Non-primitive serializable value in QRecord - calling SerializationUtils.clone...", logPair("key", entry.getKey()), logPair("type", value.getClass())); + clone.put(entry.getKey(), (V) SerializationUtils.clone(serializableValue)); + } + else + { + LOG.warn("Non-serializable value in QRecord...", logPair("key", entry.getKey()), logPair("type", value.getClass())); + clone.put(entry.getKey(), entry.getValue()); + } } - - return (new LinkedHashMap<>(map)); + return (clone); } /******************************************************************************* - ** todo - move to a cloning utils maybe? + ** *******************************************************************************/ - @SuppressWarnings({ "unchecked" }) - private List doDeepCopy(List list) + private Map> deepCopyAssociatedRecords(Map> input) { - if(list == null) + if(input == null) { return (null); } - if(list instanceof Serializable serializableList) + Map> clone = new HashMap<>(); + for(Map.Entry> entry : input.entrySet()) { - return (List) SerializationUtils.clone(serializableList); + clone.put(entry.getKey(), new ArrayList<>(entry.getValue())); } - - return (new ArrayList<>(list)); + return (clone); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordTest.java new file mode 100644 index 00000000..fffd70b9 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordTest.java @@ -0,0 +1,155 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.data; + + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import org.junit.jupiter.api.Test; +import static com.kingsrook.qqq.backend.core.model.data.QRecord.BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS; +import static com.kingsrook.qqq.backend.core.model.data.QRecord.BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + + +/******************************************************************************* + ** Unit test for QRecord + *******************************************************************************/ +class QRecordTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCopyConstructor() + { + String jsonValue = """ + {"key": [1,2]} + """; + Map fieldLengths = MapBuilder.of("a", 1, "b", 2); + + QRecord original = new QRecord() + .withTableName("myTable") + .withRecordLabel("My Record") + .withValue("one", 1) + .withValue("two", "two") + .withValue("three", new BigDecimal("3")) + .withValue("false", false) + .withValue("empty", null) + .withDisplayValue("three", "3.00") + .withBackendDetail(BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT, jsonValue) + .withBackendDetail(BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS, new HashMap<>(fieldLengths)) + .withError(new BadInputStatusMessage("Bad Input")) + .withAssociatedRecord("child", new QRecord().withValue("id", "child1")) + .withAssociatedRecord("child", new QRecord().withValue("id", "child2")) + .withAssociatedRecord("nephew", new QRecord().withValue("id", "nephew1")); + + QRecord clone = new QRecord(original); + + ////////////////////////////////////////////////////////////// + // assert equality on all the members values in the records // + ////////////////////////////////////////////////////////////// + assertEquals("myTable", clone.getTableName()); + assertEquals("My Record", clone.getRecordLabel()); + assertEquals(1, clone.getValue("one")); + assertEquals("two", clone.getValue("two")); + assertEquals(new BigDecimal("3"), clone.getValue("three")); + assertEquals(false, clone.getValue("false")); + assertNull(clone.getValue("empty")); + assertEquals("3.00", clone.getDisplayValue("three")); + assertEquals(jsonValue, clone.getBackendDetail(BACKEND_DETAILS_TYPE_JSON_SOURCE_OBJECT)); + assertEquals(fieldLengths, clone.getBackendDetail(BACKEND_DETAILS_TYPE_HEAVY_FIELD_LENGTHS)); + assertEquals(1, clone.getErrors().size()); + assertEquals(BadInputStatusMessage.class, clone.getErrors().get(0).getClass()); + assertEquals("Bad Input", clone.getErrors().get(0).getMessage()); + assertEquals(0, clone.getWarnings().size()); + assertEquals(2, clone.getAssociatedRecords().size()); + assertEquals(2, clone.getAssociatedRecords().get("child").size()); + assertEquals("child1", clone.getAssociatedRecords().get("child").get(0).getValue("id")); + assertEquals("child2", clone.getAssociatedRecords().get("child").get(1).getValue("id")); + assertEquals(1, clone.getAssociatedRecords().get("nephew").size()); + assertEquals("nephew1", clone.getAssociatedRecords().get("nephew").get(0).getValue("id")); + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure the associated record data structures are not the same (e.g., not the same map & lists) // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + assertNotSame(clone.getAssociatedRecords(), original.getAssociatedRecords()); + assertNotSame(clone.getAssociatedRecords().get("child"), original.getAssociatedRecords().get("child")); + + ///////////////////////////////////////////////////////////////////////////////////// + // but we'll be okay with the same records inside the associated records structure // + ///////////////////////////////////////////////////////////////////////////////////// + assertSame(clone.getAssociatedRecords().get("child").get(0), original.getAssociatedRecords().get("child").get(0)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testCopyConstructorEdgeCases() + { + QRecord nullValuesRecord = new QRecord(); + nullValuesRecord.setValues(null); + assertNull(new QRecord(nullValuesRecord).getValues()); + + QRecord nullDisplayValuesRecord = new QRecord(); + nullDisplayValuesRecord.setDisplayValues(null); + assertNull(new QRecord(nullDisplayValuesRecord).getDisplayValues()); + + QRecord nullBackendDetailsRecord = new QRecord(); + nullBackendDetailsRecord.setBackendDetails(null); + assertNull(new QRecord(nullBackendDetailsRecord).getBackendDetails()); + + QRecord nullAssociations = new QRecord(); + nullAssociations.setAssociatedRecords(null); + assertNull(new QRecord(nullAssociations).getAssociatedRecords()); + + QRecord nullErrors = new QRecord(); + nullErrors.setErrors(null); + assertNull(new QRecord(nullErrors).getErrors()); + + QRecord nullWarnings = new QRecord(); + nullWarnings.setWarnings(null); + assertNull(new QRecord(nullWarnings).getWarnings()); + + QRecord emptyRecord = new QRecord(); + QRecord emptyClone = new QRecord(emptyRecord); + assertNull(emptyClone.getTableName()); + assertNull(emptyClone.getRecordLabel()); + assertEquals(0, emptyClone.getValues().size()); + assertEquals(0, emptyClone.getDisplayValues().size()); + assertEquals(0, emptyClone.getBackendDetails().size()); + assertEquals(0, emptyClone.getErrors().size()); + assertEquals(0, emptyClone.getWarnings().size()); + assertEquals(0, emptyClone.getAssociatedRecords().size()); + } + +} \ No newline at end of file