登录
注册
写文章
发现
工具
通用数据对比工具:智能识别增删改,只比对前端提交字段,零业务耦合
_3t3lfz KEKfID
编辑文章
通用数据对比工具:智能识别增删改,只比对前端提交字段,零业务耦合
asfx站长
2025.11.01 16:48:13
阅读
6
### 背景 实际开发过程中,我们经常会设计一个接口去接收前端提交过来的主子单数据,如果是新增接口那么无脑新增就行,但是如果是修改接口我们得自己判断前端提交过来的数据中子单数据应该是新增?修改?删除?还是不变... 现在我们领导还提了一个要求是:如果前端提交过来的数据中子单没有变更过那后端就不要修改这个子单信息。那么问题来了,难道我们跟数据库里已存在的数据一个字段一个字段比对过去?显示不合适也不应该这么做,这样太业务耦合了,还多出很多不方便阅读和维护的“屎山”代码,又长又恶心。 ### 解决方案 为了解决这个问题,这里提供一个比较通用的数据对比工具,来帮我们解决这种痛点!!代码比较通用,没有耦合业务,可以直接拿来使用。而且自定义程度也比较高,id字段可以自定义,可以排除掉自己不想对比的字段,接收前端提交的子单类和DB数据的子单类不需要同一个类,支持泛型。然后从对比结果中可以清晰得知子单数据是应该新增?还是修改?还是删除?还是不变! 工具类: ``` import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.ruoyi.common.utils.CommonUtil; import lombok.Data; import java.util.*; /** * 通用数据变更比较工具类 * 支持任意类型的主键id自定义;支持自定义排除比较字段; * * @author whm * @date 2025/11/01 */ public class GenericDataComparator { private static final ObjectMapper objectMapper = new ObjectMapper(); /** * 默认排除的字段 */ private static final Set<String> EXCLUDED_FIELDS = Set.of( "createTime", "updateTime", "creator", "updater", "deleteFlag", "delFlag", "tenantId", "createBy", "updateBy", "creatorId", "modifierId", "gmtCreate", "gmtModified" ); static { objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); objectMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); } /** * 对比结果 */ @Data public static class CompareResult<T> { private boolean hasChanged; // 是否有变化 private List<T> toAdd; // 需要新增的 private List<T> toUpdate; // 需要更新的 private List<Object> toDelete; // 需要删除的ID(支持任意类型) private List<T> unchanged; // 未变化的 public CompareResult() { this.toAdd = new ArrayList<>(); this.toUpdate = new ArrayList<>(); this.toDelete = new ArrayList<>(); this.unchanged = new ArrayList<>(); } } public static <E, I> CompareResult<I> compareDetail(List<E> existingList, List<I> incomingList) { return compareDetail(existingList, incomingList, null, null); } /** * 详细对比 - 使用默认ID字段名"id" * 支持不同类型的对象对比(如数据库Entity vs 前端DTO) * * @param existingList 数据库中的现有数据(可以是Entity类型) * @param incomingList 前端提交的数据(可以是DTO类型) * @return 返回的结果中包含的是incomingList的类型 */ public static <E, I> CompareResult<I> compareDetail(List<E> existingList, List<I> incomingList, Set<String> extraExcludedFields) { return compareDetail(existingList, incomingList, "id", extraExcludedFields); } /** * 详细对比 - 使用默认ID字段名"id" * * @param existingList * @param incomingList * @param idFieldName * @param <E> * @param <I> * @return */ public static <E, I> CompareResult<I> compareDetail(List<E> existingList, List<I> incomingList, String idFieldName) { return compareDetail(existingList, incomingList, idFieldName, null); } /** * 详细对比 - 指定ID字段名 * 支持不同类型的对象对比 * * @param existingList 数据库中的现有数据 * @param incomingList 前端提交的数据 * @param idFieldName 主键字段名 */ public static <E, I> CompareResult<I> compareDetail(List<E> existingList, List<I> incomingList, String idFieldName, Set<String> extraExcludedFields) { CompareResult<I> result = new CompareResult<>(); if (existingList == null) existingList = new ArrayList<>(); if (incomingList == null) incomingList = new ArrayList<>(); try { // 1. 建立existing的ID映射 Map<Object, E> existingMap = new HashMap<>(); Map<Object, JsonNode> existingNodeMap = new HashMap<>(); for (E item : existingList) { Object id = extractFieldValue(item, idFieldName); if (id != null) { Object normalizedId = normalizeId(id); existingMap.put(normalizedId, item); existingNodeMap.put(normalizedId, objectMapper.valueToTree(item)); } } // 2. 遍历incoming,判断是新增还是修改 Set<Object> processedIds = new HashSet<>(); for (I incomingItem : incomingList) { Object id = extractFieldValue(incomingItem, idFieldName); Object normalizedId = normalizeId(id); if (id == null || !existingMap.containsKey(normalizedId)) { // 新增 result.getToAdd().add(incomingItem); result.setHasChanged(true); } else { // 可能是修改,需要详细对比 processedIds.add(normalizedId); JsonNode existingNode = existingNodeMap.get(normalizedId); JsonNode incomingNode = objectMapper.valueToTree(incomingItem); // 只对比incoming中存在的字段 if (isItemChanged(existingNode, incomingNode, idFieldName, extraExcludedFields)) { result.getToUpdate().add(incomingItem); result.setHasChanged(true); } else { result.getUnchanged().add(incomingItem); } } } // 3. 找出被删除的 for (Object existingId : existingMap.keySet()) { if (!processedIds.contains(existingId)) { result.getToDelete().add(existingId); result.setHasChanged(true); } } } catch (Exception e) { throw new RuntimeException("Data comparison failed: " + e.getMessage(), e); } return result; } /** * 对比单个对象是否变化(只对比incoming中存在的字段) * * @param existingNode * @param incomingNode * @param idFieldName * @return */ private static boolean isItemChanged(JsonNode existingNode, JsonNode incomingNode, String idFieldName) { return isItemChanged(existingNode, incomingNode, idFieldName, null); } /** * 对比单个对象是否变化(只对比incoming中存在的字段) */ private static boolean isItemChanged(JsonNode existingNode, JsonNode incomingNode, String idFieldName, Set<String> extraExcludedFields) { Iterator<Map.Entry<String, JsonNode>> fields = incomingNode.fields(); while (fields.hasNext()) { Map.Entry<String, JsonNode> entry = fields.next(); String fieldName = entry.getKey(); // 跳过系统字段和ID字段 if (EXCLUDED_FIELDS.contains(fieldName) || idFieldName.equals(fieldName)) { continue; } // 跳过额外排除的字段 if (CommonUtil.isNotEmpty(extraExcludedFields) && extraExcludedFields.contains(fieldName)) { continue; } JsonNode incomingValue = entry.getValue(); JsonNode existingValue = existingNode.get(fieldName); // 对比值 if (!Objects.equals(incomingValue, existingValue)) { return true; } } return false; } /** * 提取对象的指定字段值(支持任意类型) */ private static <T> Object extractFieldValue(T item, String fieldName) { try { JsonNode node = objectMapper.valueToTree(item); if (node.has(fieldName) && !node.get(fieldName).isNull()) { JsonNode fieldNode = node.get(fieldName); // 根据类型返回对应的值 if (fieldNode.isNumber()) { if (fieldNode.isLong() || fieldNode.isInt()) { return fieldNode.asLong(); } else { return fieldNode.asDouble(); } } else if (fieldNode.isTextual()) { return fieldNode.asText(); } else if (fieldNode.isBoolean()) { return fieldNode.asBoolean(); } else { return fieldNode.toString(); } } return null; } catch (Exception e) { throw new RuntimeException("Failed to extract field '" + fieldName + "': " + e.getMessage(), e); } } /** * 标准化ID值,确保不同类型但值相同的ID能正确匹配 */ private static Object normalizeId(Object id) { if (id == null) { return null; } // 数字类型统一转为字符串进行比较 if (id instanceof Number) { return String.valueOf(((Number) id).longValue()); } // 字符串类型去除空格 if (id instanceof String) { return ((String) id).trim(); } return id; } /** * 简单判断是否有变化(不返回详细信息) */ public static <E, I> boolean hasChanged(List<E> existingList, List<I> incomingList) { return hasChanged(existingList, incomingList, "id"); } /** * 简单判断是否有变化 - 指定ID字段名 */ public static <E, I> boolean hasChanged(List<E> existingList, List<I> incomingList, String idFieldName) { CompareResult<I> result = compareDetail(existingList, incomingList, idFieldName); return result.isHasChanged(); } } ``` 测试类: ``` import lombok.Data; import java.math.BigDecimal; import java.util.Arrays; import java.util.List; public class GenericDataComparatorTest { public static void main(String[] args) { System.out.println("========== 开始测试 GenericDataComparator ==========\n"); // 测试场景1: 数据无变化 testCase1_EntityVsDTO_NoChange(); // 测试场景2: 修改单个字段 testCase2_EntityVsDTO_QuantityChange(); // 测试场景3: 新增记录 testCase3_EntityVsDTO_AddNewItem(); // 测试场景4: 删除记录 testCase4_EntityVsDTO_DeleteItem(); // 测试场景5: 复杂混合操作 testCase5_EntityVsDTO_ComplexScenario(); // 测试场景6: DTO字段少于Entity testCase6_EntityVsDTO_PartialFields(); // 测试场景7: 自定义主键字段名 testCase7_CustomIdField(); // 测试场景8: 主键类型不一致 testCase8_DifferentIdTypes(); System.out.println("\n========== 所有测试完成 =========="); } /** * 测试1: Entity vs DTO - 无变化 */ private static void testCase1_EntityVsDTO_NoChange() { System.out.println("【测试1】Entity vs DTO - 子单无变化"); // 数据库实体(包含完整字段) List<OrderItem> existing = Arrays.asList( createOrderItem(1L, 100L, "商品A", 2, new BigDecimal("99.99"), "备注1", "2024-01-01"), createOrderItem(2L, 200L, "商品B", 5, new BigDecimal("199.99"), "备注2", "2024-01-01") ); // 前端DTO(只包含部分字段) List<OrderItemDTO> incoming = Arrays.asList( createOrderItemDTO(1L, 100L, "商品A", 2, new BigDecimal("99.99")), createOrderItemDTO(2L, 200L, "商品B", 5, new BigDecimal("199.99")) ); GenericDataComparator.CompareResult<OrderItemDTO> result = GenericDataComparator.compareDetail(existing, incoming); printResult(result, "应该无变化"); assert !result.isHasChanged() : "测试失败:应该无变化"; assert result.getUnchanged().size() == 2 : "测试失败:应该有2条未变化"; System.out.println("✅ 测试通过\n"); } /** * 测试2: Entity vs DTO - 修改数量 */ private static void testCase2_EntityVsDTO_QuantityChange() { System.out.println("【测试2】Entity vs DTO - 修改其中一条的数量"); List<OrderItem> existing = Arrays.asList( createOrderItem(1L, 100L, "商品A", 2, new BigDecimal("99.99"), "备注1", "2024-01-01"), createOrderItem(2L, 200L, "商品B", 5, new BigDecimal("199.99"), "备注2", "2024-01-01") ); List<OrderItemDTO> incoming = Arrays.asList( createOrderItemDTO(1L, 100L, "商品A", 3, new BigDecimal("99.99")), // 数量从2改为3 createOrderItemDTO(2L, 200L, "商品B", 5, new BigDecimal("199.99")) ); GenericDataComparator.CompareResult<OrderItemDTO> result = GenericDataComparator.compareDetail(existing, incoming); printResult(result, "应该有1条更新"); assert result.isHasChanged() : "测试失败:应该有变化"; assert result.getToUpdate().size() == 1 : "测试失败:应该有1条更新"; assert result.getToUpdate().get(0).getId() == 1L : "测试失败:应该更新ID为1的"; assert result.getUnchanged().size() == 1 : "测试失败:应该有1条未变化"; System.out.println("✅ 测试通过\n"); } /** * 测试3: Entity vs DTO - 新增 */ private static void testCase3_EntityVsDTO_AddNewItem() { System.out.println("【测试3】Entity vs DTO - 新增一条子单"); List<OrderItem> existing = Arrays.asList( createOrderItem(1L, 100L, "商品A", 2, new BigDecimal("99.99"), "备注1", "2024-01-01") ); List<OrderItemDTO> incoming = Arrays.asList( createOrderItemDTO(1L, 100L, "商品A", 2, new BigDecimal("99.99")), createOrderItemDTO(null, 200L, "商品B", 5, new BigDecimal("199.99")) // 新增 ); GenericDataComparator.CompareResult<OrderItemDTO> result = GenericDataComparator.compareDetail(existing, incoming); printResult(result, "应该有1条新增"); assert result.isHasChanged() : "测试失败:应该有变化"; assert result.getToAdd().size() == 1 : "测试失败:应该有1条新增"; assert result.getUnchanged().size() == 1 : "测试失败:应该有1条未变化"; System.out.println("✅ 测试通过\n"); } /** * 测试4: Entity vs DTO - 删除 */ private static void testCase4_EntityVsDTO_DeleteItem() { System.out.println("【测试4】Entity vs DTO - 删除一条子单"); List<OrderItem> existing = Arrays.asList( createOrderItem(1L, 100L, "商品A", 2, new BigDecimal("99.99"), "备注1", "2024-01-01"), createOrderItem(2L, 200L, "商品B", 5, new BigDecimal("199.99"), "备注2", "2024-01-01") ); List<OrderItemDTO> incoming = Arrays.asList( createOrderItemDTO(1L, 100L, "商品A", 2, new BigDecimal("99.99")) // ID为2的被删除了 ); GenericDataComparator.CompareResult<OrderItemDTO> result = GenericDataComparator.compareDetail(existing, incoming); printResult(result, "应该有1条删除"); assert result.isHasChanged() : "测试失败:应该有变化"; assert result.getToDelete().size() == 1 : "测试失败:应该有1条删除"; assert result.getToDelete().get(0).equals("2") : "测试失败:应该删除ID为2的"; assert result.getUnchanged().size() == 1 : "测试失败:应该有1条未变化"; System.out.println("✅ 测试通过\n"); } /** * 测试5: Entity vs DTO - 复杂场景 */ private static void testCase5_EntityVsDTO_ComplexScenario() { System.out.println("【测试5】Entity vs DTO - 复杂场景(增删改混合)"); List<OrderItem> existing = Arrays.asList( createOrderItem(1L, 100L, "商品A", 2, new BigDecimal("99.99"), "备注1", "2024-01-01"), createOrderItem(2L, 200L, "商品B", 5, new BigDecimal("199.99"), "备注2", "2024-01-01"), createOrderItem(3L, 300L, "商品C", 8, new BigDecimal("299.99"), "备注3", "2024-01-01") ); List<OrderItemDTO> incoming = Arrays.asList( createOrderItemDTO(1L, 100L, "商品A", 3, new BigDecimal("99.99")), // 修改 // ID为2的被删除 createOrderItemDTO(3L, 300L, "商品C", 8, new BigDecimal("299.99")), // 不变 createOrderItemDTO(null, 400L, "商品D", 10, new BigDecimal("399.99")) // 新增 ); GenericDataComparator.CompareResult<OrderItemDTO> result = GenericDataComparator.compareDetail(existing, incoming); printResult(result, "应该有1新增、1修改、1删除、1不变"); assert result.isHasChanged() : "测试失败:应该有变化"; assert result.getToAdd().size() == 1 : "测试失败:应该有1条新增"; assert result.getToUpdate().size() == 1 : "测试失败:应该有1条修改"; assert result.getToDelete().size() == 1 : "测试失败:应该有1条删除"; assert result.getUnchanged().size() == 1 : "测试失败:应该有1条不变"; System.out.println("✅ 测试通过\n"); } /** * 测试6: Entity vs DTO - 前端只提交部分字段 */ private static void testCase6_EntityVsDTO_PartialFields() { System.out.println("【测试6】Entity vs DTO - 前端只提交部分字段(DTO字段少于Entity)"); // Entity包含: id, productId, productName, quantity, price, remark, createTime List<OrderItem> existing = Arrays.asList( createOrderItem(1L, 100L, "商品A", 2, new BigDecimal("99.99"), "这是备注", "2024-01-01") ); // DTO只包含: id, productId, quantity, price (不包含productName和remark) OrderItemDTO dto = new OrderItemDTO(); dto.setId(1L); dto.setProductId(100L); dto.setQuantity(2); dto.setPrice(new BigDecimal("99.99")); // productName为null, remark为null List<OrderItemDTO> incoming = Arrays.asList(dto); GenericDataComparator.CompareResult<OrderItemDTO> result = GenericDataComparator.compareDetail(existing, incoming); printResult(result, "应该无变化(只对比DTO中存在的字段)"); assert !result.isHasChanged() : "测试失败:应该无变化"; assert result.getUnchanged().size() == 1 : "测试失败:应该有1条未变化"; System.out.println("✅ 测试通过(成功忽略了Entity中DTO没有的字段)\n"); } /** * 测试7: 自定义ID字段名 */ private static void testCase7_CustomIdField() { System.out.println("【测试7】自定义ID字段名 - 使用'orderId'"); List<CustomIdEntity> existing = Arrays.asList( createCustomIdEntity(1001L, "商品A", 2, "备注1"), createCustomIdEntity(1002L, "商品B", 5, "备注2") ); List<CustomIdDTO> incoming = Arrays.asList( createCustomIdDTO(1001L, "商品A", 3), // 修改数量 createCustomIdDTO(1003L, "商品C", 10) // 新增 ); // 指定ID字段名为"orderId" GenericDataComparator.CompareResult<CustomIdDTO> result = GenericDataComparator.compareDetail(existing, incoming, "orderId"); printResult(result, "应该有1条修改、1条新增、1条删除"); assert result.isHasChanged() : "测试失败"; assert result.getToUpdate().size() == 1 : "测试失败"; assert result.getToAdd().size() == 1 : "测试失败"; assert result.getToDelete().size() == 1 : "测试失败"; System.out.println("✅ 测试通过\n"); } /** * 测试8: 不同ID类型(Long vs Integer) */ private static void testCase8_DifferentIdTypes() { System.out.println("【测试8】不同ID类型 - Entity用Long, DTO用Integer"); // Entity使用Long类型的ID List<OrderItem> existing = Arrays.asList( createOrderItem(1L, 100L, "商品A", 2, new BigDecimal("99.99"), "备注", "2024-01-01") ); // DTO使用Integer类型的ID List<IntegerIdDTO> incoming = Arrays.asList( createIntegerIdDTO(1, 100L, "商品A", 2, new BigDecimal("99.99")) ); GenericDataComparator.CompareResult<IntegerIdDTO> result = GenericDataComparator.compareDetail(existing, incoming); printResult(result, "应该无变化(Long(1)和Integer(1)应该匹配)"); assert !result.isHasChanged() : "测试失败:不同数字类型的相同值应该能匹配"; System.out.println("✅ 测试通过(成功处理了Long和Integer的类型差异)\n"); } // ============ 辅助方法 ============ private static void printResult(GenericDataComparator.CompareResult<?> result, String expected) { System.out.println("期望: " + expected); System.out.println("结果: hasChanged=" + result.isHasChanged() + ", 新增=" + result.getToAdd().size() + ", 修改=" + result.getToUpdate().size() + ", 删除=" + result.getToDelete().size() + ", 不变=" + result.getUnchanged().size()); if (!result.getToAdd().isEmpty()) { System.out.println(" - 新增明细: " + result.getToAdd()); } if (!result.getToUpdate().isEmpty()) { System.out.println(" - 修改明细: " + result.getToUpdate()); } if (!result.getToDelete().isEmpty()) { System.out.println(" - 删除ID: " + result.getToDelete()); } } // ============ Entity类(数据库实体,字段完整) ============ @Data static class OrderItem { private Long id; private Long productId; private String productName; private Integer quantity; private BigDecimal price; private String remark; // DTO中没有这个字段 private String createTime; // 系统字段 private String updateTime; // 系统字段 } @Data static class CustomIdEntity { private Long orderId; // 自定义ID字段名 private String productName; private Integer quantity; private String remark; private String createTime; } // ============ DTO类(前端传递,字段较少) ============ @Data static class OrderItemDTO { private Long id; private Long productId; private String productName; private Integer quantity; private BigDecimal price; // 注意:没有remark, createTime, updateTime字段 } @Data static class CustomIdDTO { private Long orderId; private String productName; private Integer quantity; // 没有remark字段 } @Data static class IntegerIdDTO { private Integer id; // 注意:这里是Integer类型 private Long productId; private String productName; private Integer quantity; private BigDecimal price; } // ============ 工厂方法 ============ private static OrderItem createOrderItem(Long id, Long productId, String name, Integer qty, BigDecimal price, String remark, String createTime) { OrderItem item = new OrderItem(); item.setId(id); item.setProductId(productId); item.setProductName(name); item.setQuantity(qty); item.setPrice(price); item.setRemark(remark); item.setCreateTime(createTime); item.setUpdateTime("2024-01-01 12:00:00"); return item; } private static OrderItemDTO createOrderItemDTO(Long id, Long productId, String name, Integer qty, BigDecimal price) { OrderItemDTO dto = new OrderItemDTO(); dto.setId(id); dto.setProductId(productId); dto.setProductName(name); dto.setQuantity(qty); dto.setPrice(price); return dto; } private static CustomIdEntity createCustomIdEntity(Long orderId, String name, Integer qty, String remark) { CustomIdEntity entity = new CustomIdEntity(); entity.setOrderId(orderId); entity.setProductName(name); entity.setQuantity(qty); entity.setRemark(remark); entity.setCreateTime("2024-01-01"); return entity; } private static CustomIdDTO createCustomIdDTO(Long orderId, String name, Integer qty) { CustomIdDTO dto = new CustomIdDTO(); dto.setOrderId(orderId); dto.setProductName(name); dto.setQuantity(qty); return dto; } private static IntegerIdDTO createIntegerIdDTO(Integer id, Long productId, String name, Integer qty, BigDecimal price) { IntegerIdDTO dto = new IntegerIdDTO(); dto.setId(id); dto.setProductId(productId); dto.setProductName(name); dto.setQuantity(qty); dto.setPrice(price); return dto; } } ```
我的主页
退出