跳转至

第五章:数据更新、删除

5.0 引言

在企业应用开发中,数据的安全修改是一个核心需求,也是最容易引发问题的环节。随着业务复杂度的增加和并发访问量的提升,如何保证数据修改的安全性、一致性和高效性,成为了系统设计的关键挑战。Jimmer作为一款现代化的ORM框架,在数据修改领域提供了独特的解决方案,本章将深入探讨Jimmer对数据修改的创新设计。

5.0.1 数据修改操作的核心价值

数据修改是应用系统的核心功能,其重要性体现在以下几个方面:

  • 业务价值载体

每一次数据修改都承载着业务价值的转换。以电商系统为例,当用户下单购买商品时,系统需要创建订单记录、更新商品库存、记录支付信息等一系列数据修改操作。这些操作直接反映了业务状态的变化,是业务流程的数字化表达。

// 购买流程中的数据修改
Order order = OrderDraft.$.produce(draft -> {
    draft.setId(orderId);
    draft.setCustomerId(customerId);
    draft.setStatus(OrderStatus.CREATED);
    draft.setTotalAmount(totalAmount);
    draft.setCreatedTime(LocalDateTime.now());

    // 设置订单项
    draft.setItems(items.stream()
        .map(item -> OrderItemDraft.$.produce(d -> {
            d.setProductId(item.getProductId());
            d.setQuantity(item.getQuantity());
            d.setPrice(item.getPrice());
        }))
        .collect(Collectors.toList())
    );
});

// 保存订单并更新关联数据
sqlClient.getEntities().save(order);
  • 数据一致性保障

在分布式系统和高并发环境下,保证数据修改的一致性是一个巨大挑战。数据修改操作需要在各种复杂场景下都能保持数据的一致性,包括:

  • 事务一致性:确保一组相关操作要么全部成功,要么全部失败
  • 并发一致性:多用户并发修改同一数据时避免数据覆盖和冲突
  • 关联一致性:维护实体之间的关联关系,确保外键约束和业务规则

  • 系统安全基石

数据修改操作的安全性直接关系到系统的整体安全。不安全的数据修改可能导致:

  • 数据泄露:敏感字段的错误暴露
  • 权限绕过:未经授权的数据修改
  • 注入攻击:通过数据修改操作注入恶意代码
  • 数据损坏:不完整或错误的修改导致数据不一致

  • 性能关键点

数据修改操作通常是系统性能的关键瓶颈之一。高效的数据修改实现对系统整体性能有着决定性影响:

  • 减少不必要的数据库访问
  • 优化批量操作效率
  • 控制事务范围和锁竞争
  • 智能检测和更新变化的字段

5.0.2 传统ORM框架数据修改的局限性

传统ORM框架如Hibernate、MyBatis等在数据修改方面存在一些固有的局限性,这些问题在复杂业务场景和高并发环境中尤为明显:

  • 可变对象的线程安全问题

传统ORM框架基于可变对象模型,实体对象可以通过setter方法随时被修改。这种设计在多线程环境下容易引发并发问题:

// 传统ORM的线程安全问题
public void processOrder(Long productId) {
    // 两个线程同时获取同一个商品
    Product product = productRepository.findById(productId).get();

    // 线程A读取价格进行计算
    BigDecimal price = product.getPrice();
    // ...计算过程

    // 同时线程B修改了同一个对象的价格
    product.setPrice(product.getPrice().multiply(new BigDecimal("0.9")));
    productRepository.save(product);

    // 线程A继续使用已经失效的价格数据进行后续处理
    // 这将导致计算错误和数据不一致
}

我们在测试中模拟了这种情况:

@Test
@DisplayName("模拟传统ORM可变对象的并发问题")
public void testTraditionalOrmMutableObjectConcurrencyIssue() throws InterruptedException {
    // 1. 模拟传统ORM的可变产品类
    MutableProduct mutableProduct = new MutableProduct(
        "test-id",
        "可变商品",
        new BigDecimal("100.00"),
        100,
        true,
        LocalDateTime.now()
    );

    // 2. 并发环境模拟
    int threadCount = 10;
    ExecutorService executor = Executors.newFixedThreadPool(threadCount);
    CountDownLatch latch = new CountDownLatch(threadCount * 2);

    // 线程组1: 修改可变对象的价格
    for (int i = 0; i < threadCount; i++) {
        final int discount = i + 1;
        executor.submit(() -> {
            BigDecimal discountRate = new BigDecimal("0." + discount);
            // 直接修改共享对象
            BigDecimal originalPrice = mutableProduct.getPrice();
            mutableProduct.setPrice(originalPrice.multiply(discountRate));
            latch.countDown();
        });
    }

    // 线程组2: 读取可变对象的价格(期望是100.00)
    for (int i = 0; i < threadCount; i++) {
        executor.submit(() -> {
            // 读取共享对象的价格 - 在并发环境下可能被其他线程修改
            BigDecimal price = mutableProduct.getPrice();
            latch.countDown();
        });
    }

    // 等待所有线程完成
    latch.await();
    executor.shutdown();

    // 此时价格已被修改为某个折扣值,不再是原始的100.00
}

这种可变性导致对象状态难以预测,在复杂业务逻辑和多线程环境中容易引发各种并发问题。

  • 全字段更新与更新效率

传统ORM框架在更新实体时通常采用全字段更新策略,或者要求开发者手动指定需要更新的字段,这带来两个明显问题:

  1. 性能开销:更新一个拥有几十个字段的实体,即使只修改了一个字段,也会生成更新所有字段的SQL
  2. 开发负担:要求开发者手动指定更新字段,增加了开发复杂度和出错可能
// 传统ORM的字段更新问题
public void updateProductPrice(Long productId, BigDecimal newPrice) {
    // 方法1: 全字段更新 - 不必要地更新了所有字段
    Product product = productRepository.findById(productId).get();
    product.setPrice(newPrice);
    productRepository.save(product);

    // 方法2: 手动指定更新字段 - 开发负担重,容易出错
    productRepository.updatePrice(productId, newPrice);
    // 需要为不同的更新场景编写不同的自定义方法
}
  • 复杂关联处理的挑战

处理复杂的实体关联关系是传统ORM框架的一大挑战:

  1. N+1问题:加载实体关联时的性能问题
  2. 级联处理复杂:关联实体的级联创建、更新和删除配置繁琐且容易出错
  3. 双向关联同步:需要手动维护双向关联的一致性
// 传统ORM的关联处理
public void addItemToOrder(Long orderId, OrderItem newItem) {
    Order order = orderRepository.findById(orderId).get();
    List<OrderItem> items = order.getItems(); // 可能触发额外查询

    // 需要手动维护双向关联
    newItem.setOrder(order);
    items.add(newItem);

    // 保存时可能出现意外行为
    orderRepository.save(order); // 是否会级联保存newItem?
}
  • 悲观锁与乐观锁的使用困境

传统ORM框架中的并发控制策略使用存在一些问题:

  1. 悲观锁:增加数据库锁争用,降低并发性能
  2. 乐观锁:处理冲突的方式不够灵活,错误恢复机制有限
  3. 手动实现:往往需要手动编写额外代码处理并发控制

5.0.3 Jimmer数据修改的设计理念

Jimmer针对传统ORM框架的局限性,提出了全新的数据修改设计理念:

  • 不可变实体与可变Draft的双模态设计

Jimmer的核心创新在于将实体对象设计为不可变(Immutable)对象,同时提供Draft对象作为可变视图用于修改操作:

// Jimmer的不可变实体与Draft结合的设计
@Test
public void testBasicEntitySave() {
    // 1. 使用Draft创建不可变实体
    Category category = CategoryDraft.$.produce(draft -> {
        draft.setId(categoryId);
        draft.setName("测试类别");
        draft.setDescription("用于测试的类别");
        draft.setCreatedTime(LocalDateTime.now());
    });

    // 2. 保存实体
    sqlClient.getEntities().save(category);

    // 3. 基于已有实体创建新实体
    Product product = ProductDraft.$.produce(draft -> {
        draft.setId(productId);
        draft.setName("测试商品");
        draft.setPrice(new BigDecimal("99.99"));
        draft.setStock(100);
        draft.setActive(true);
        draft.setCategoryId(category.id()); // 设置关联
        draft.setCreatedTime(LocalDateTime.now());
    });

    sqlClient.getEntities().save(product);
}

这种设计带来多重优势: - 实体对象本身不可变,提供了天然的线程安全性 - Draft对象仅在本地线程中可变,避免了共享状态的并发问题 - 实体与Draft之间的转换由框架自动处理,对开发者透明

  • 多线程环境下的安全保障

不可变对象在多线程环境中的优势尤为明显,测试结果证明了这一点:

@Test
public void testImmutableObjectThreadSafety() throws InterruptedException {
    // 1. 创建不可变商品
    Product product = sqlClient.getEntities().findById(Product.class, productId);

    // 2. 并发环境模拟
    ExecutorService executor = Executors.newFixedThreadPool(10);
    CountDownLatch latch = new CountDownLatch(20);
    AtomicReference<Exception> error = new AtomicReference<>();

    // 线程组1: 创建新对象(修改商品价格)
    for (int i = 0; i < 10; i++) {
        final int discount = i + 1;
        executor.submit(() -> {
            try {
                // 基于原对象创建新的折扣商品对象
                BigDecimal discountRate = new BigDecimal("0." + discount);
                Product discountedProduct = ProductDraft.$.produce(product, draft -> {
                    draft.setPrice(product.price().multiply(discountRate));
                });

                // 原对象不变,新对象反映折扣价格
                assertEquals(0, new BigDecimal("100.00").compareTo(product.price()));
                assertTrue(discountedProduct.price().compareTo(product.price()) < 0);
            } catch (Exception e) {
                error.set(e);
            } finally {
                latch.countDown();
            }
        });
    }

    // 线程组2: 直接读取原对象
    for (int i = 0; i < 10; i++) {
        executor.submit(() -> {
            try {
                // 验证原对象的值始终不变
                assertEquals(0, new BigDecimal("100.00").compareTo(product.price()));
                assertEquals("测试商品", product.name());
                assertEquals(100, product.stock());
            } catch (Exception e) {
                error.set(e);
            } finally {
                latch.countDown();
            }
        });
    }

    // 等待所有线程完成,验证没有并发错误
    latch.await();
    executor.shutdown();
    assertNull(error.get());
}

测试结果证明,即使在多线程并发环境下,Jimmer的不可变对象仍然保持完美的线程安全性。

  • 智能变更检测与高效更新

Jimmer自动检测对象变更,只更新实际发生变化的字段:

// 智能变更检测 - 只更新修改的字段
Product updatedProduct = ProductDraft.$.produce(existingProduct, draft -> {
    // 只修改价格,其他属性保持不变
    draft.setPrice(existingProduct.price().multiply(new BigDecimal("0.9")));
});
sqlClient.getEntities().save(updatedProduct);
// 生成的SQL只会更新price字段,不会更新其他字段

这种设计消除了传统ORM的全字段更新问题,同时避免了手动指定更新字段的开发负担。

  • 透明的关联处理与级联操作

Jimmer使关联处理和级联操作变得简单透明:

// 透明的关联处理与级联保存
Order order = OrderDraft.$.produce(draft -> {
    draft.setId(orderId);
    draft.setCustomerId(customerId);
    draft.setStatus(OrderStatus.CREATED);

    // 设置订单项 - 级联创建
    draft.setItems(Arrays.asList(
        OrderItemDraft.$.produce(d -> {
            d.setProductId(product1Id);
            d.setQuantity(2);
            d.setPrice(new BigDecimal("50.00"));
        }),
        OrderItemDraft.$.produce(d -> {
            d.setProductId(product2Id);
            d.setQuantity(1);
            d.setPrice(new BigDecimal("100.00"));
        })
    ));
});

// 一次调用完成订单和订单项的保存
sqlClient.getEntities().save(order);

框架自动处理关联关系的级联保存、更新和删除,无需开发者手动维护关联一致性。

  • 内置乐观锁与冲突处理

Jimmer内置支持乐观锁和版本控制,简化并发环境下的数据一致性保障:

// 内置乐观锁支持
@Entity
public interface Product {
    @Id
    long id();

    String name();

    BigDecimal price();

    @Version
    long version(); // 版本字段
}

// 使用乐观锁更新
try {
    Product product = sqlClient.getEntities().findById(Product.class, id);
    Product updatedProduct = ProductDraft.$.produce(product, draft -> {
        draft.setPrice(new BigDecimal("199.99"));
    });
    sqlClient.getEntities().save(updatedProduct);
} catch (OptimisticLockException e) {
    // 处理并发冲突
    logger.warn("发生并发冲突,请重试");
}

5.0.4 本章内容导航

本章将全面探讨Jimmer数据修改的各个方面,为读者提供深入理解和实践指导:

  1. 不可变对象的修改模式(5.1节)
  2. 深入解析Draft对象的设计与生命周期
  3. 不可变性与数据修改的辩证关系
  4. 实体状态切换的内部机制

  5. 实体的创建与保存(5.2节)

  6. Draft对象创建实体的完整过程
  7. 保存操作的各种模式与选项
  8. 批量保存与性能优化策略

  9. 实体的更新与变更检测(5.3节)

  10. 智能变更检测的工作原理
  11. 动态更新与静态更新的使用场景
  12. 乐观锁机制与并发控制

  13. 级联保存与关联处理(5.4节)

  14. 各种关联关系的级联保存处理
  15. 复杂实体图的保存策略
  16. 性能优化与实践建议

  17. 实体的删除操作(5.5节)

  18. 基本删除与级联删除
  19. 逻辑删除的实现与应用
  20. 删除操作的约束处理

  21. 变更审计与历史追踪(5.6节)

  22. 审计字段的自动处理
  23. 历史版本与变更日志
  24. 大规模系统的审计策略

通过本章的学习,我们可以掌握Jimmer数据修改的核心理念和实践技巧,能够设计出兼具安全性、一致性和高性能的数据修改方案。在接下来的5.1节中,我们将深入探讨不可变对象的修改模式,揭示Jimmer如何通过Draft对象解决不可变性与数据修改的表面矛盾。

5.1 不可变对象的修改模式

不可变对象是Jimmer框架的核心设计理念之一,也是区别于传统ORM框架的关键特性。本节将深入探讨不可变对象在数据修改场景中的应用模式,以及Jimmer如何通过巧妙的设计解决不可变性与数据修改之间看似矛盾的问题。

5.1.1 不可变性与数据修改的矛盾

  • 不可变性的价值

不可变性(Immutability)是函数式编程中的核心概念,指对象一旦创建后,其状态就不能被改变。在ORM领域中引入不可变性具有许多优势:

  1. 线程安全:不可变对象天然线程安全,无需加锁即可在多线程环境中安全共享
  2. 防御性拷贝:避免对象状态被意外修改,提高代码的可预测性
  3. 缓存友好:不会变化的对象更适合缓存,无需担心缓存失效问题
  4. 状态追踪:每次"修改"都会创建新对象,便于实现变更历史追踪

然而,在数据持久化场景中,数据修改是不可避免的需求,这与不可变性原则形成了明显的矛盾。传统ORM框架如Hibernate采用可变对象模型,通过直接修改实体对象的状态来实现数据更新。这种方式虽然直观,但在并发环境中容易引发问题。

  • 传统可变对象模型的问题

以电商系统中的商品库存管理为例,传统ORM的可变对象模型可能导致以下问题:

// 传统ORM中的可变对象模式
Product product = productRepository.findById(1L);

// 线程A读取库存进行计算
int currentStock = product.getStock();
// 计算逻辑...

// 同时线程B修改了同一对象的库存
product.setStock(product.getStock() - 10);
productRepository.save(product);

// 线程A基于已读取的库存继续计算,但此时库存已被修改
// 导致计算结果错误

上述代码中,由于product对象是可变的,线程B的修改直接影响了线程A正在操作的对象状态,导致线程A的计算基于过时数据,产生了不一致问题。在实际业务中,这类问题可能导致库存计算错误、数据不一致等严重后果。

  • Jimmer的解决方案

Jimmer通过"不可变实体"+"可变Draft"的双模态设计,巧妙地解决了这一矛盾:

  1. 实体对象(Entity):完全不可变,一旦创建,状态不可改变
  2. 草稿对象(Draft):可变视图,用于修改操作,仅在受控边界内可用
  3. 转换机制:提供Entity与Draft之间安全、高效的转换

这种设计将"读取"和"修改"两种操作清晰分离,既保留了不可变性的优势,又满足了数据修改的需求。

5.1.2 Draft对象的核心设计

Draft对象是Jimmer框架中实现不可变对象修改的核心机制,提供了一种受控的可变视图,用于创建和修改实体对象。

  • Draft接口与实体接口的关系

在Jimmer中,每个实体接口都有一个对应的Draft接口,两者形成紧密的配对关系:

// 实体接口 - 不可变
public interface Product {
    String id();
    String name();
    BigDecimal price();
    int stock();
    // 其他属性...
}

// Draft接口 - 可变视图
public interface ProductDraft extends Product, Draft {
    void setId(String id);
    void setName(String name);
    void setPrice(BigDecimal price);
    void setStock(int stock);
    // 其他setter方法...
}

核心特点:

  1. 类型安全:Draft接口继承自实体接口,保持类型一致性
  2. 访问模式区分:实体接口只有getter方法,Draft接口同时拥有getter和setter方法
  3. 自动生成:Jimmer编译时代码生成器自动处理,开发者无需手动编写
  4. 实现绑定:Draft对象内部持有对应不可变对象的引用,保证数据一致性

  5. Draft对象的生命周期

Draft对象拥有严格定义的生命周期,确保可变状态被合理控制:

  1. 创建阶段:通过XxxDraft.$.produce()方法创建
  2. 修改阶段:在lambda表达式内部进行属性修改
  3. 转换阶段:lambda执行完毕,Draft对象被转换为不可变实体并返回
  4. 销毁阶段:转换完成后,Draft对象不再可访问
// Draft对象生命周期示例
Product newProduct = ProductDraft.$.produce(draft -> {
    // 修改阶段 - draft是可变的Draft对象
    draft.setName("新商品");
    draft.setPrice(new BigDecimal("99.99"));
    draft.setStock(100);
    // 离开lambda后,draft对象将被转换并销毁
});
// newProduct是不可变的实体对象

这种设计确保了可变状态被限制在一个明确的边界内,一旦离开这个边界,对象就回到不可变状态,保证了系统的整体安全性。

  • 类型安全的修改操作

Draft机制提供了类型安全的修改操作,无论是创建新对象还是修改现有对象:

// 创建新对象
Product newProduct = ProductDraft.$.produce(draft -> {
    draft.setId(UUID.randomUUID().toString());
    draft.setName("商品A");
    draft.setPrice(new BigDecimal("199.00"));
    draft.setStock(50);
});

// 基于现有对象创建新对象
Product updatedProduct = ProductDraft.$.produce(existingProduct, draft -> {
    draft.setPrice(new BigDecimal("180.00"));
    draft.setStock(draft.stock() + 20);
});

实际案例中,Draft对象的类型安全特性能有效避免许多常见错误:

ProductDraft.$.produce(draft -> {
    // 编译时类型检查,错误的属性名会导致编译失败
    // draft.setprice(new BigDecimal("100")); // 编译错误:找不到方法

    // 类型不匹配也会在编译时发现
    // draft.setPrice("100"); // 编译错误:类型不兼容
});

这种严格的类型检查机制在大型项目中尤为重要,能够在开发阶段就发现并解决潜在问题。

5.1.3 实体对象的保存机制

  • 基于Draft的保存流程

Jimmer中的实体保存操作建立在不可变对象和Draft机制之上,形成了一套独特的流程:

  1. 创建/获取不可变对象:新建实体或从数据库加载现有实体
  2. 创建Draft并修改:基于不可变对象创建Draft并进行修改
  3. 生成新的不可变对象:Draft修改完成后生成新的不可变实体
  4. 保存到数据库:使用SqlClient将不可变实体保存到数据库

完整流程示例:

// 1. 从数据库加载商品
Product product = sqlClient.getEntities().findById(Product.class, productId);

// 2. 创建Draft并修改
Product modifiedProduct = ProductDraft.$.produce(product, draft -> {
    draft.setPrice(new BigDecimal("120.00"));
    draft.setStock(80);
    draft.setModifiedTime(LocalDateTime.now());
});

// 3. 保存修改后的不可变实体
sqlClient.getEntities().save(modifiedProduct);

这种流程保持了整个系统的不可变性特征,即使在数据修改场景中。

  • 全量保存与局部保存

Jimmer支持两种保存模式:

  1. 全量保存:保存实体的所有属性,适用于新建或完全替换场景
  2. 局部保存:只保存实际变化的属性,适用于部分更新场景

而实现两种模式的关键在于,Jimmer能够通过比较操作前后的不可变对象,智能检测哪些属性发生了变化:

// 全量保存 - 新建实体
Product newProduct = ProductDraft.$.produce(draft -> {
    draft.setId(productId);
    draft.setName("新商品");
    draft.setPrice(new BigDecimal("99.99"));
    draft.setStock(100);
    draft.setActive(true);
    draft.setCreatedTime(LocalDateTime.now());
});
sqlClient.getEntities().save(newProduct); // 插入所有字段

// 局部保存 - 只修改价格
Product priceUpdated = ProductDraft.$.produce(existingProduct, draft -> {
    // 只修改价格
    draft.setPrice(new BigDecimal("89.99"));
    draft.setModifiedTime(LocalDateTime.now());
});
sqlClient.getEntities().save(priceUpdated); // 只更新price和modifiedTime字段

这种设计不仅简化了开发者的工作,也提高了系统性能,避免了不必要的数据库操作。

  • 智能变更检测

Jimmer的智能变更检测机制是局部保存的基础,其工作原理是:

  1. 保存前,比较新旧不可变对象的每个属性
  2. 识别出发生变化的属性集合
  3. 动态生成只包含变化属性的SQL更新语句

这种机制带来的优势:

  • 减少数据库负载:只更新必要的字段,减少数据库IO
  • 避免乐观锁冲突:不修改未变更的字段,降低并发冲突概率
  • 保留历史数据:如果某个字段由其他流程更新,不会被覆盖

在实际测试中,这种智能变更检测机制能显著提升系统性能,特别是在大型实体和高并发场景下。

5.1.4 深入理解immutable与mutable状态切换

Jimmer实体在生命周期中会经历不可变(immutable)与可变(mutable)两种状态的切换,这一过程是理解Jimmer对象模型的关键。

  • 状态切换的核心流程
不可变实体(Entity) --[produce]--> 可变Draft --[修改]--> 可变Draft --[完成]--> 新的不可变实体(Entity)

状态切换具体发生在以下场景:

  1. Immutable → Mutable:当调用XxxDraft.$.produce()方法时,Jimmer创建一个可变的Draft对象
  2. Mutable → Immutable:当lambda表达式执行完成时,Draft对象转换回不可变的实体对象
// 状态切换示例
Product product = getExistingProduct(); // 不可变状态
assertFalse(product instanceof Draft);

// immutable → mutable
Product modifiedProduct = ProductDraft.$.produce(product, draft -> {
    // 在lambda内部,draft是可变的
    assertTrue(draft instanceof Draft);
    draft.setPrice(new BigDecimal("150.00"));

    // 此时无法访问修改后的product,因为它尚未创建
});

// mutable → immutable
// modifiedProduct是新创建的不可变对象
assertFalse(modifiedProduct instanceof Draft);

// 原对象保持不变
assertEquals(originalPrice, product.price());
// 新对象反映了变更
assertEquals(new BigDecimal("150.00"), modifiedProduct.price());
  • 状态切换的技术实现

Jimmer通过以下技术实现状态切换:

  1. 实体接口代理:使用ByteBuddy生成的动态代理实现实体接口
  2. 内部状态复制:从不可变对象到Draft,再到新的不可变对象的属性复制
  3. 结构共享优化:新旧对象共享未修改的部分,降低内存占用
  4. 转换时机控制:仅在Draft生命周期结束时才执行状态转换

状态切换过程对应的源码简化模型:

// 简化的produce实现逻辑
public static <T> T produce(T base, Consumer<T> mutator) {
    // 1. 创建可变Draft
    T draft = createDraft(base);

    try {
        // 2. 执行修改操作
        mutator.accept(draft);

        // 3. 将Draft转换为不可变实体
        return finalizeDraft(draft);
    } finally {
        // 4. 清理Draft资源
        releaseDraft(draft);
    }
}

这种设计确保了可变状态严格限制在lambda表达式内部,外部世界只能看到和操作不可变对象,从而保证了系统的整体安全性和一致性。

5.1.5 不可变对象在数据修改中的优势

通过前面的分析,我们已经了解了Jimmer如何实现不可变对象的修改。现在,让我们深入探讨这种设计在实际业务场景中带来的优势。

  • 并发场景中的数据一致性

在高并发电商系统中,商品信息经常需要同时进行多种操作,如价格计算和库存管理。不可变对象模型能够确保这些并发操作的安全性:

// 加载商品信息
Product product = sqlClient.getEntities().findById(Product.class, productId);

// 并发场景:多个线程同时处理同一个商品
ExecutorService executor = Executors.newFixedThreadPool(10);

// 价格计算线程 - 计算促销价格
executor.submit(() -> {
    // 基于原对象计算折扣价
    BigDecimal discountPrice = calculateDiscount(product.price());

    // 创建新的折扣商品对象
    Product discountProduct = ProductDraft.$.produce(product, draft -> {
        draft.setPrice(discountPrice);
        draft.setName(product.name() + " (促销)");
    });

    // 处理折扣商品...
    // 原商品对象不受影响
});

// 库存管理线程 - 调整库存
executor.submit(() -> {
    // 基于原对象调整库存
    int newStock = calculateNewStock(product.stock());

    // 创建库存已调整的新商品对象
    Product stockAdjusted = ProductDraft.$.produce(product, draft -> {
        draft.setStock(newStock);
    });

    // 处理库存调整...
    // 原商品对象不受影响
});

// 无论多少线程同时操作,原始product对象都保持不变
// 每个线程都基于相同的原始状态进行计算,不会互相干扰

这种模式下,各个线程可以安全地并行处理业务逻辑,不需要复杂的锁机制,大大提高了系统的并发性能和可靠性。

  • 事务边界的清晰定义

不可变对象模型使得事务边界更加清晰:

@Transactional
public Product applyDiscount(String productId, BigDecimal discountRate) {
    // 1. 读取当前状态
    Product product = repository.findById(productId);

    // 2. 在事务内创建新状态
    Product discounted = ProductDraft.$.produce(product, draft -> {
        draft.setPrice(product.price().multiply(discountRate));
        draft.setModifiedTime(LocalDateTime.now());
    });

    // 3. 保存新状态
    return repository.save(discounted);
}

在这个模式中: - 事务开始时读取的是一个不可变快照 - 所有修改都基于这个快照创建新对象 - 事务提交时保存新对象状态 - 整个过程中状态变更边界明确,不会有中间状态泄露

  • 业务逻辑验证与决策模式

不可变对象特别适合实现验证后再决定是否保存的业务模式:

// 加载商品
Product product = repository.findById(productId);

// 创建调价后的商品
Product priceUpdated = ProductDraft.$.produce(product, draft -> {
    draft.setPrice(newPrice);
    draft.setModifiedTime(LocalDateTime.now());
});

// 业务验证 - 基于新状态判断是否应该保存
if (isPriceChangeValid(product.price(), priceUpdated.price())) {
    // 验证通过,保存新状态
    repository.save(priceUpdated);
    notifyPriceChange(productId, product.price(), priceUpdated.price());
} else {
    // 验证失败,简单丢弃新对象,无需回滚任何状态
    logRejectedPriceChange(productId, product.price(), newPrice);
}

这种模式的优势在于: - 可以先创建新状态,再决定是否应用这个变更 - 验证失败时无需特殊处理,简单丢弃新对象即可 - 原对象状态始终不变,便于在失败时进行重试

  • 代码可读性与可维护性提升

不可变对象模型使得数据修改逻辑更加清晰:

// 传统可变对象模型
product.setName("新名称");
product.setPrice(new BigDecimal("150.00"));
repository.save(product);

// Jimmer不可变对象模型
Product updatedProduct = ProductDraft.$.produce(product, draft -> {
    draft.setName("新名称");
    draft.setPrice(new BigDecimal("150.00"));
});
repository.save(updatedProduct);

虽然Jimmer模式看起来代码量略多,但它具有以下优势: - 明确表明原对象与新对象的关系 - 清晰展示哪些属性被修改 - 修改操作被封装在一个原子单元中 - 局部变量作用域更加明确

在大型项目中,这种清晰的代码结构极大地提升了可维护性,减少了由状态修改引起的Bug。

小结

本节深入探讨了Jimmer框架的不可变对象修改模式,从不可变性与数据修改的矛盾入手,详细分析了Draft对象的核心设计、实体保存机制、状态切换原理以及这种模式在实际业务中的优势。

通过"不可变实体+可变Draft"的双模态设计,Jimmer成功地将函数式编程的不可变性理念引入到ORM领域,在保证线程安全和数据一致性的同时,提供了类型安全、直观易用的数据修改API。这种设计不仅适用于简单的CRUD操作,在复杂的业务场景和高并发环境中更能发挥其独特优势。

在下一节中,我们将更深入地探讨实体的创建与保存操作,包括如何高效创建新实体、处理不同类型的保存场景,以及优化保存性能等方面。

5.2 实体的创建与保存

在上一节中,我们深入了解了Jimmer中不可变对象的修改模式及其优势。本节将进一步探讨如何在Jimmer框架中创建新实体对象并将其保存到数据库中。我们将详细介绍Draft创建对象的方式、单个与批量保存机制,以及保存结果的处理方法。

5.2.1 使用Draft创建新实体

在Jimmer中,创建新的实体对象需要通过Draft接口。Draft接口为不可变对象提供了一个可变的视图,允许我们修改对象属性。Draft的核心机制是produce方法,它提供了一个临时可变上下文,用于构建或修改对象。

  • Draft创建对象的基本语法

创建新实体的基本语法如下:

EntityType entity = EntityDraft.$.produce(draft -> {
    // 在这里设置实体的属性
    draft.setXxx(...);
    draft.setYyy(...);
    // ...
});

其中,EntityDraft.$是由Jimmer自动生成的静态访问点,produce方法接收一个lambda表达式,在该表达式内部,我们可以通过draft参数设置实体的属性。

  • 创建简单实体

让我们看一个从实际测试用例中提取的例子,创建一个简单的标签实体:

// 创建简单实体 - 标签
String tagId = TEST_PREFIX + "tag-" + UUID.randomUUID().toString();
Tag tag = TagDraft.$.produce(draft -> {
    draft.setId(tagId);
    draft.setName("新标签");
    draft.setDescription("使用Draft创建的标签");
    draft.setCreatedTime(LocalDateTime.now());
});

这段代码创建了一个新的Tag实体,并设置了其ID、名称、描述和创建时间属性。注意,我们使用UUID生成唯一标识符,这在实际应用中是常见的做法。创建完成后,我们得到的tag对象是一个不可变对象,但它包含了我们在Draft中设置的所有属性值。

  • 创建带关联的实体

Jimmer的一大优势是可以轻松创建和管理带有关联关系的实体。下面的例子展示了如何创建一个带有关联的实体:

// 创建类别
String categoryId = TEST_PREFIX + "category-" + UUID.randomUUID().toString();
Category category = CategoryDraft.$.produce(draft -> {
    draft.setId(categoryId);
    draft.setName("新类别");
    draft.setCreatedTime(LocalDateTime.now());
});

// 创建关联到类别的商品
String productId = TEST_PREFIX + "product-" + UUID.randomUUID().toString();
Product product = ProductDraft.$.produce(draft -> {
    draft.setId(productId);
    draft.setName("新商品");
    draft.setPrice(new BigDecimal("199.00"));
    draft.setStock(50);
    draft.setActive(true);
    draft.setDescription("使用Draft创建的商品");
    // 设置关联
    draft.setCategoryId(categoryId);
    // 设置多对多关联
    draft.setTags(Collections.singletonList(
        TagDraft.$.produce(d -> d.setId(tagId))
    ));
    draft.setCreatedTime(LocalDateTime.now());
});

在这个例子中,我们首先创建了一个Category实体,然后创建了一个Product实体,并将其关联到此类别。值得注意的是,我们使用了setCategoryId方法设置类别ID,这是Jimmer自动为外键关联生成的便捷方法。同时,我们还通过setTags方法设置了多对多关联,直接传入了一个标签列表。

  • Draft对象的生命周期

Draft对象只在produce方法的lambda表达式内部有效。一旦produce方法执行完毕,就会生成不可变的实体对象,而Draft对象则会被销毁。这种设计确保了实体对象在创建后的不可变性,同时又提供了灵活的创建机制。

*# 工作原理剖析

当我们调用produce方法时,Jimmer会:

  1. 创建一个临时的Draft对象
  2. 执行lambda表达式,让我们设置Draft对象的属性
  3. 根据Draft对象的状态创建一个新的不可变实体对象
  4. 返回这个不可变实体对象,同时销毁Draft对象

这个过程确保了实体对象的不可变性,同时又提供了类似于Builder模式的便捷创建方式。

5.2.2 保存单个实体对象

创建实体对象后,我们需要将其保存到数据库中。Jimmer提供了简单而强大的API来执行这一操作。

  • 基本保存语法

保存单个实体的基本语法如下:

var saveResult = sqlClient.getEntities().save(entity);

这个操作会将实体数据持久化到数据库中,并返回一个SaveResult对象,该对象包含了保存操作的结果信息。

  • 实际示例

下面是一个完整的创建并保存实体的例子:

// 创建标签
String tagId = TEST_PREFIX + "tag-" + UUID.randomUUID().toString();
Tag tag = TagDraft.$.produce(draft -> {
    draft.setId(tagId);
    draft.setName("新标签");
    draft.setDescription("使用Draft创建的标签");
    draft.setCreatedTime(LocalDateTime.now());
});

// 保存标签
sqlClient.getEntities().save(tag);

// 验证保存结果
Tag savedTag = sqlClient.getEntities().findById(Tag.class, tagId);
assertNotNull(savedTag);
assertEquals("新标签", savedTag.name());

在这个例子中,我们首先创建了一个标签对象,然后使用sqlClient.getEntities().save方法将其保存到数据库,最后通过查询确认保存是否成功。

  • 保存模式

Jimmer提供了多种保存模式,用于控制保存操作的行为。保存模式通过SaveMode枚举定义,包括:

  • UPSERT:更新或插入,如果实体存在则更新,不存在则插入(默认模式)
  • INSERT_ONLY:仅插入,如果实体已存在则抛出异常
  • UPDATE_ONLY:仅更新,如果实体不存在则不执行任何操作
  • INSERT_IF_ABSENT:仅在实体不存在时插入,已存在则不执行任何操作

使用保存模式的语法如下:

var saveResult = sqlClient.getEntities().save(entity, SaveMode.XXX);
  • 保存模式示例

以下是使用不同保存模式的例子,摘自我们的测试用例:

// 使用INSERT_ONLY模式保存
try {
    var result = sqlClient.getEntities().save(modifiedTag1, SaveMode.INSERT_ONLY);
    fail("应该抛出异常,因为使用 INSERT_ONLY 模式插入已存在的记录");
} catch (Exception e) {
    // 预期会抛出异常,因为记录已存在
    System.out.println("预期的异常: " + e.getMessage());
}

// 使用UPDATE_ONLY模式保存
var updateResult = sqlClient.getEntities().save(modifiedTag1, SaveMode.UPDATE_ONLY);
int updateCount = updateResult.getTotalAffectedRowCount();

// 验证数据已被更新
fetchedTag1 = sqlClient.getEntities().findById(Tag.class, tagId1);
assertEquals("修改后的标签1", fetchedTag1.name(), "UPDATE_ONLY 模式下,已存在记录应被更新");
assertEquals(1, updateCount, "应该有1条记录被更新");

在这个例子中,我们首先尝试使用INSERT_ONLY模式保存一个已存在的标签,预期会抛出异常。然后,我们使用UPDATE_ONLY模式保存同一个标签,并验证更新是否成功。

5.2.3 批量保存与性能优化

在处理大量数据时,批量保存是一种高效的方式。Jimmer提供了批量保存API,可以同时保存多个实体对象。

  • 批量保存语法

批量保存的基本语法如下:

List<Entity> entities = ...;
var batchResult = sqlClient.getEntities().saveEntities(entities);

或者使用指定的保存模式:

var batchResult = sqlClient.getEntities().saveEntities(entities, SaveMode.XXX);
  • 批量保存示例

下面是一个批量保存标签的例子:

// 准备测试数据 - 创建多个标签
String tagId1 = TEST_PREFIX + "batch-tag-1-" + UUID.randomUUID().toString();
String tagId2 = TEST_PREFIX + "batch-tag-2-" + UUID.randomUUID().toString();
String tagId3 = TEST_PREFIX + "batch-tag-3-" + UUID.randomUUID().toString();

List<Tag> tags = Arrays.asList(
    createTag(tagId1, "标签1"),
    createTag(tagId2, "标签2"),
    createTag(tagId3, "标签3")
);

// 使用 INSERT_ONLY 保存模式批量保存标签
var batchResult = sqlClient.getEntities().saveEntities(tags, SaveMode.INSERT_ONLY);
int savedCount = batchResult.getTotalAffectedRowCount();

// 验证保存结果
assertEquals(3, savedCount, "应该保存3个标签");

这个示例中,我们创建了三个标签对象,并使用saveEntities方法一次性将它们保存到数据库中。通过getTotalAffectedRowCount方法,我们可以知道有多少条记录被成功保存。

  • 批量保存的性能优势

批量保存相比于多次单个保存有以下性能优势:

  1. 减少数据库连接开销:批量操作只需要一次数据库连接,而单个保存可能需要多次连接
  2. 减少网络往返:批量操作可以在一次网络通信中完成,减少了网络延迟
  3. 数据库批处理优化:数据库可以对批量操作进行优化,如批量插入

在Jimmer中,批量保存操作实际上使用了JDBC的批处理功能,这能显著提高大量数据保存的性能。对于需要保存大量数据的场景,建议始终使用批量保存而不是循环单个保存。

  • 混合操作示例

Jimmer的批量保存API还支持混合操作,即在一个批处理中同时执行插入和更新操作:

// 修改已存在记录
Tag modifiedTag2 = TagDraft.$.produce(draft -> {
    draft.setId(tagId2);
    draft.setName("修改后的标签2");
    draft.setCreatedTime(LocalDateTime.now());
});

// 创建新记录
String tagId5 = TEST_PREFIX + "batch-tag-5-" + UUID.randomUUID().toString();
Tag newTag5 = createTag(tagId5, "标签5");

// 使用 UPSERT 模式批量保存
var upsertResult = sqlClient.getEntities().saveEntities(
    Arrays.asList(modifiedTag2, newTag5), 
    SaveMode.UPSERT
);
int upsertCount = upsertResult.getTotalAffectedRowCount();

// 验证结果 - 应该有2条记录被保存或更新
assertEquals(2, upsertCount, "UPSERT 模式下,应该有2条记录被保存或更新");

在这个例子中,我们同时保存了一个已存在的标签(需要更新)和一个新标签(需要插入)。通过使用UPSERT模式,Jimmer能够智能地处理这两种情况,而不需要我们分别执行更新和插入操作。

5.2.4 保存结果的处理

Jimmer的保存操作返回一个SaveResult对象,该对象包含了丰富的保存结果信息,有助于我们进行后续处理。

  • SaveResult的主要方法

SaveResult接口提供了以下主要方法:

  • getTotalAffectedRowCount():获取受影响的总行数
  • getOriginalEntity():获取原始实体对象
  • getModifiedEntity():获取修改后的实体对象

  • 处理保存结果示例

下面是一个展示如何处理保存结果的例子:

// 保存实体并获取结果
var saveResult = sqlClient.getEntities().save(tag);

// 1. 获取影响的行数
int affectedRowCount = saveResult.getTotalAffectedRowCount();
assertEquals(1, affectedRowCount, "应该有1行被影响");

// 2. 获取原始实体
Tag originalEntity = saveResult.getOriginalEntity();
assertNotNull(originalEntity, "原始实体不应为空");
assertEquals(tagId, originalEntity.id(), "原始实体ID应该正确");

// 3. 获取修改后的实体
Tag modifiedEntity = saveResult.getModifiedEntity();

这个例子展示了如何从SaveResult对象中获取各种信息,包括影响的行数、原始实体对象和修改后的实体对象。

  • 批量保存结果处理

批量保存也会返回一个保存结果对象,但它包含的是整个批次的汇总信息:

var batchResult = sqlClient.getEntities().saveEntities(tags);
int totalAffected = batchResult.getTotalAffectedRowCount();
assertEquals(2, totalAffected, "批量保存应该影响2行");

5.2.5 实体创建的最佳实践

在使用Jimmer进行实体创建和保存时,以下最佳实践可以帮助您编写更健壮、高效的代码:

    1. 合理使用ID生成策略
  • 对于测试环境,手动指定有意义的ID有助于追踪和调试

  • 在生产环境中,考虑使用自动生成的ID(UUID、自增ID等)
  • 始终确保ID的唯一性,避免违反唯一约束
// 测试环境中使用有意义的前缀
String testId = "TEST-" + UUID.randomUUID().toString();
Entity entity = EntityDraft.$.produce(draft -> {
    draft.setId(testId);
    // 其他属性设置
});
    1. 选择合适的保存模式
  • 使用INSERT_ONLY进行新增操作,可以避免意外更新

  • 使用UPDATE_ONLY进行更新操作,可以避免意外插入
  • 当需要插入或更新时,使用UPSERT模式简化逻辑
  • 使用INSERT_IF_ABSENT可以实现不重复插入的幂等操作

    1. 使用批处理提高性能
  • 对于大量数据,始终使用批量保存而不是循环单个保存

  • 控制批量大小,避免过大的批量导致内存压力
  • 考虑在事务中进行批量操作,确保原子性
// 控制批量大小的例子
List<Entity> allEntities = ... // 假设有大量实体
int batchSize = 100;
for (int i = 0; i < allEntities.size(); i += batchSize) {
    int end = Math.min(i + batchSize, allEntities.size());
    List<Entity> batch = allEntities.subList(i, end);

    // 保存当前批次
    sqlClient.getEntities().saveEntities(batch);
}
    1. 合理处理关联关系
  • 创建关联实体时,确保关联双方都设置了正确的关联字段

  • 对于复杂的关联图,考虑先保存主实体,再处理关联
  • 利用Jimmer的级联保存功能(在下一节详细介绍)简化关联处理

    1. 妥善处理异常

对保存操作的可能异常进行适当处理,常见的异常包括:

  • 唯一约束违反异常
  • 外键约束违反异常
  • 数据库连接异常
  • 保存模式冲突异常
try {
    sqlClient.getEntities().save(entity, SaveMode.INSERT_ONLY);
} catch (Exception e) {
    // 处理异常
    if (e instanceof SqlConstraintViolationException) {
        System.out.println("违反约束: " + e.getMessage());
    } else {
        System.out.println("保存失败: " + e.getMessage());
        // 可能需要日志记录、重试或其他处理
    }
}

小结

本节我们详细探讨了Jimmer中实体的创建与保存机制。通过Draft接口,Jimmer提供了一种富有表现力且类型安全的方式来创建和修改不可变实体。我们学习了单个实体和批量实体的保存方法,以及不同保存模式的应用场景。同时,我们还了解了如何正确处理保存结果,包括影响行数、原始实体等信息。

Jimmer的实体创建和保存API设计简洁而强大,既保持了代码的简洁性,又提供了丰富的功能和灵活性。合理使用这些API,可以编写出高效、健壮的数据访问代码。

在下一节中,我们将深入探讨实体的更新与变更检测机制,了解Jimmer如何智能地检测实体变更并生成高效的更新语句。

5.3 实体的更新与变更检测

在上一节中,我们详细探讨了如何使用Jimmer创建和保存实体。本节将继续深入,重点关注实体的更新操作以及Jimmer强大的变更检测机制。我们将分析Jimmer如何高效地识别和处理实体的变化,以及动态更新与静态更新API的使用场景和优势。

5.3.1 通过Draft更新实体

Jimmer中更新实体是通过Draft接口进行的,这与创建新实体的方式非常一致。这种设计为开发者提供了统一的编程体验,无论是创建还是更新实体。

  • 基于已有实体创建Draft

当我们需要更新一个已存在的实体时,通常的流程是:

  1. 从数据库查询获取实体
  2. 基于已有实体创建Draft对象
  3. 修改Draft中的属性
  4. 将修改后的实体保存回数据库

以下是一个完整的实体更新示例:

// 1. 先查询获取实体
Product savedProduct = sqlClient.getEntities().findById(Product.class, productId);
assertNotNull(savedProduct);
assertEquals("原始商品", savedProduct.name());
assertEquals(0, new BigDecimal("100.00").compareTo(savedProduct.price()));

// 2. 通过Draft更新实体
Product updatedProduct = ProductDraft.$.produce(savedProduct, draft -> {
    draft.setName("更新后的商品");
    draft.setPrice(new BigDecimal("120.00"));
    draft.setDescription("通过Draft更新的商品");
    draft.setModifiedTime(LocalDateTime.now());
});

// 3. 保存更新
sqlClient.getEntities().save(updatedProduct);

// 4. 查询并验证更新成功
Product result = sqlClient.getEntities().findById(Product.class, productId);
assertNotNull(result);
assertEquals("更新后的商品", result.name());
assertEquals(0, new BigDecimal("120.00").compareTo(result.price()));
assertEquals("通过Draft更新的商品", result.description());
assertNotNull(result.modifiedTime());

// 验证原对象不变(不可变性)
assertEquals("原始商品", savedProduct.name());

在这个例子中,我们首先查询得到一个商品实体,然后使用ProductDraft.$.produce方法创建一个基于该实体的Draft,在Draft中修改属性,最后生成一个新的实体对象并保存到数据库。

  • 实体更新的核心机制

实体更新的核心机制可以分解为以下几个关键步骤:

  1. 创建可变视图produce方法接受一个已存在的实体,创建其可变视图(Draft)
  2. 修改属性:在Draft上调用setter方法修改所需属性
  3. 创建新的不可变对象:Draft完成后,生成一个新的包含所有变更的不可变实体对象
  4. 持久化变更:通过save方法将变更保存到数据库

下图展示了实体更新的完整流程:

┌─────────────┐     ┌──────────────┐     ┌─────────────────┐     ┌───────────┐
│ 不可变实体A │────>│ 可变Draft A' │────>│ 新的不可变实体A"│────>│ 数据库    │
└─────────────┘     └──────────────┘     └─────────────────┘     └───────────┘
                    │                │
                    │  修改属性...   │
                    └────────────────┘
  • 局部修改与整体修改

Jimmer支持两种更新方式:局部修改和整体修改。

局部修改是指只修改实体的部分属性,保持其他属性不变。这是最常见的更新方式:

Product updatedProduct = ProductDraft.$.produce(savedProduct, draft -> {
    // 只修改价格,其他属性保持不变
    draft.setPrice(new BigDecimal("120.00"));
    draft.setModifiedTime(LocalDateTime.now());
});

整体修改是指创建一个全新的实体对象,设置所有需要的属性,包括ID,然后保存。这更像是"覆盖"而不是"更新":

Product fullUpdated = ProductDraft.$.produce(draft -> {
    draft.setId(productId); // 设置ID以便更新已有记录
    draft.setName("全新商品名称");
    draft.setPrice(new BigDecimal("220.00"));
    draft.setStock(200);
    draft.setActive(true);
    draft.setDescription("全量更新的商品");
    draft.setCreatedTime(savedProduct.createdTime()); // 保持创建时间不变
    draft.setModifiedTime(LocalDateTime.now());
});

// 使用UPDATE_ONLY模式确保只进行更新而非插入
sqlClient.getEntities().save(fullUpdated, SaveMode.UPDATE_ONLY);

整体修改通常用于以下场景: - 从外部系统导入数据时 - 用户编辑表单提交整个实体时 - 需要替换实体的大部分或全部属性时

  • 处理关联更新

在更新实体时,Jimmer也支持关联对象的更新。例如,我们可以同时更新商品及其关联的类别:

Product productWithCategory = ProductDraft.$.produce(savedProduct, draft -> {
    draft.setName("新商品名称");
    // 更新关联的类别
    draft.setCategoryId(newCategoryId);
});

sqlClient.getEntities().save(productWithCategory);

对于集合类型的关联,例如一对多或多对多关系,我们也可以更新整个集合:

Product productWithTags = ProductDraft.$.produce(savedProduct, draft -> {
    // 更新商品标签(多对多关联)
    draft.setTags(Arrays.asList(
        TagDraft.$.produce(d -> d.setId("TAG1")),
        TagDraft.$.produce(d -> d.setId("TAG2"))
    ));
});

sqlClient.getEntities().save(productWithTags);

级联保存的详细内容将在5.4节中深入讨论。

5.3.2 智能变更检测机制

Jimmer的一个重要特性是智能变更检测机制,它能够自动识别实体的哪些属性发生了变化,并且只更新这些变化的属性,而不是盲目地更新所有字段。这大大提高了更新操作的效率,尤其是在处理大型实体时。

  • 变更检测的工作原理

当我们使用Draft修改实体并生成新的不可变对象时,Jimmer会跟踪哪些属性被修改了。在保存实体时,Jimmer会:

  1. 比较原始实体和新实体的差异
  2. 生成只包含已变更字段的SQL更新语句
  3. 执行这个精简的SQL更新语句

这个过程是完全自动的,不需要开发者做任何额外工作。

  • 实例演示

下面的例子展示了Jimmer如何只更新变化的字段:

// 1. 创建测试数据
String tagId = TEST_PREFIX + "tag-" + UUID.randomUUID().toString();
Tag tag = createTestTag(tagId, "原始标签", "原始描述");

// 2. 保存标签
sqlClient.getEntities().save(tag);

// 3. 查询标签
Tag savedTag = sqlClient.getEntities().findById(Tag.class, tagId);
assertNotNull(savedTag);

// 4. 仅修改描述
Tag updatedTag = TagDraft.$.produce(savedTag, draft -> {
    // 只修改描述,名称保持不变
    draft.setDescription("更新后的描述");
    draft.setModifiedTime(LocalDateTime.now());
});

// 5. 保存修改
sqlClient.getEntities().save(updatedTag);

// 6. 查询并验证 - 只有描述被更新,名称保持不变
Tag result = sqlClient.getEntities().findById(Tag.class, tagId);
assertNotNull(result);
assertEquals("原始标签", result.name()); // 名称未变
assertEquals("更新后的描述", result.description()); // 描述已更新
assertNotNull(result.modifiedTime());

在这个例子中,我们只修改了标签的描述字段,Jimmer会自动生成一个只更新descriptionmodified_time字段的SQL语句,而不会更新其他字段。

  • 检测变更的API

Jimmer提供了ImmutableObjects.isLoaded方法来检查实体对象的哪些属性被加载或修改:

// 检查哪些属性被加载/修改
assertTrue(ImmutableObjects.isLoaded(updatedTag, "description")); // 描述被修改,所以被加载

需要注意的是,isLoaded并不完全等同于"被修改",而是表示该属性在对象中是否有值。在实际应用中,Jimmer会更智能地判断实际变更的字段,并只将这些字段包含在更新语句中。

  • 变更检测的性能优势

智能变更检测带来了显著的性能优势:

  1. 减少数据库负载:只更新必要的字段,减少了数据库的I/O操作
  2. 减少并发冲突:只更新变化的字段,降低了并发更新冲突的可能性
  3. 提高吞吐量:简化的SQL语句执行更快,允许系统处理更多请求

在高并发系统中,这些优势尤为重要,能够显著提高系统的整体性能。

5.3.3 动态更新与静态更新

Jimmer提供了两种更新实体的方式:动态更新和静态更新。它们各有优势,适用于不同的场景。

  • 动态更新API

动态更新是指通过Draft对象修改实体属性,然后保存的方式。这是我们前面演示的更新方式:

// 动态更新示例
Category dynamicUpdated = CategoryDraft.$.produce(category, draft -> {
    draft.setName("动态更新类别");
    draft.setModifiedTime(LocalDateTime.now());
});

// 保存动态更新
sqlClient.getEntities().save(dynamicUpdated);

动态更新的特点:

  1. 类型安全:编译时检查属性名和类型
  2. 对象导向:使用对象而非SQL语句
  3. 智能变更检测:自动检测并只更新变化的字段
  4. 支持复杂对象图:可以处理嵌套对象和关联关系

  5. 静态更新API

静态更新是指直接构建SQL更新语句,而不需要先查询实体。Jimmer提供了类似JPA的静态更新API:

// 静态更新示例
CategoryTable table = CategoryTable.$;
int affectedRows = sqlClient.createUpdate(table)
    .set(table.name(), "静态更新类别")
    .set(table.modifiedTime(), LocalDateTime.now())
    .where(table.id().eq(categoryId))
    .execute();

// 验证静态更新结果
assertEquals(1, affectedRows);

静态更新的特点:

  1. 无需先查询:直接构建更新语句
  2. 批量更新:可以一次更新多条记录
  3. 性能优势:减少数据库往返
  4. 灵活的条件:支持复杂的WHERE条件

  5. 条件更新与批量更新

静态更新API的一个重要特性是支持条件更新。例如,我们可以只更新满足特定条件的记录:

// 条件更新示例 - 只有当条件满足时才会更新
int noRowsAffected = sqlClient.createUpdate(table)
    .set(table.name(), "不会更新的名称")
    .where(table.id().eq(categoryId))
    .where(table.name().eq("不存在的名称")) // 这个条件不满足
    .execute();

// 验证没有行被更新
assertEquals(0, noRowsAffected);

静态更新还可以用于批量更新多条记录:

// 批量更新示例 - 更新所有活跃商品的价格
ProductTable pt = ProductTable.$;
int updatedCount = sqlClient.createUpdate(pt)
    .set(pt.price(), pt.price().mul(new BigDecimal("0.9"))) // 所有价格打9折
    .where(pt.active().eq(true))
    .execute();
  • 两种方式的对比与选择
特性 动态更新 静态更新
类型安全
智能变更检测
无需先查询
批量更新能力
复杂对象图
事务控制
性能 更好

选择哪种更新方式取决于具体场景:

  • 动态更新适合
  • 需要先读取实体,修改部分属性后保存
  • 需要处理复杂对象图和关联关系
  • 需要智能变更检测

  • 静态更新适合

  • 批量更新多条记录
  • 基于条件更新记录
  • 性能是首要考虑因素
  • 不需要先查询实体

5.3.4 更新冲突与乐观锁

在多用户并发环境中,实体更新可能面临冲突问题。当多个用户同时修改同一条记录时,如果没有适当的并发控制机制,后一个更新可能会无意中覆盖前一个更新的内容。

  • 并发更新问题

考虑以下场景:

// 两个用户同时读取商品
Product product1 = sqlClient.getEntities().findById(Product.class, productId);
Product product2 = sqlClient.getEntities().findById(Product.class, productId);

// 第一个用户修改价格
Product updated1 = ProductDraft.$.produce(product1, draft -> {
    draft.setPrice(new BigDecimal("160.00"));
    draft.setModifiedTime(LocalDateTime.now());
});

// 第二个用户修改价格
Product updated2 = ProductDraft.$.produce(product2, draft -> {
    draft.setPrice(new BigDecimal("165.00"));
    draft.setModifiedTime(LocalDateTime.now());
});

// 第一个用户先保存
sqlClient.getEntities().save(updated1);

// 第二个用户后保存 - 这会覆盖第一个用户的修改!
sqlClient.getEntities().save(updated2);

在这个例子中,第二个用户的更新会覆盖第一个用户的修改,这可能不是我们期望的行为。

  • 乐观锁实现机制

解决并发更新问题的常用方法是使用乐观锁。Jimmer支持通过@Version注解实现乐观锁:

@Entity
public interface Product {
    @Id
    String id();

    String name();

    BigDecimal price();

    // 版本字段,用于乐观锁控制
    @Version
    int version();

    // 其他属性...
}

添加@Version注解后,当实体被保存时,Jimmer会:

  1. 检查数据库中的版本号是否与当前内存中的实体版本号一致
  2. 如果一致,更新成功,并将版本号+1
  3. 如果不一致,表示该记录已被其他用户修改,抛出OptimisticLockException

  4. 冲突检测与处理策略

应用程序需要适当处理乐观锁异常:

try {
    // 尝试保存更新
    sqlClient.getEntities().save(updatedProduct);
} catch (OptimisticLockException e) {
    // 处理并发冲突
    log.warn("并发更新冲突: {}", e.getMessage());

    // 可能的处理策略:
    // 1. 通知用户冲突,要求用户重新尝试
    // 2. 重新加载最新的实体,合并变更后再次尝试保存
    // 3. 显示差异让用户决定如何处理
}

常见的冲突处理策略包括:

  1. 重新加载并重试:获取最新版本的实体,重新应用修改,然后再次尝试保存
  2. 通知用户:告知用户数据已被他人修改,请求用户刷新并重新编辑
  3. 合并更改:自动合并不冲突的字段,只有冲突的字段需要用户决策
  4. 后保存者胜出:简单地覆盖先前的更改(不推荐在大多数业务场景中使用)

  5. 实际应用示例

以电子商务系统为例,当多个客服同时处理同一个订单时:

try {
    // 客服A尝试更新订单状态
    Order updatedOrder = OrderDraft.$.produce(order, draft -> {
        draft.setStatus(OrderStatus.PROCESSING);
        draft.setOperator("客服A");
        draft.setNote("开始处理这个订单");
    });

    orderService.save(updatedOrder);
} catch (OptimisticLockException e) {
    // 通知客服A该订单已被其他客服修改
    notificationService.alert("订单 #" + order.id() + " 已被其他客服修改,请刷新后重试");

    // 记录冲突日志
    auditLogService.log("订单更新冲突", order.id(), "客服A");
}

5.3.5 实体更新的最佳实践

基于Jimmer的特性和实践经验,以下是更新实体时的一些最佳实践建议:

    1. 选择合适的更新方式
  • 对于单条记录更新,优先使用动态更新API(Draft方式)

  • 对于批量更新或条件更新,使用静态更新API
  • 需要精细控制字段更新时,使用动态更新配合智能变更检测

    1. 合理使用乐观锁
  • 在并发更新频繁的实体上添加@Version字段

  • 谨慎处理乐观锁异常,提供友好的用户体验
  • 考虑业务场景选择合适的冲突解决策略

    1. 批量操作优化
  • 使用静态更新API进行批量更新

  • 考虑使用批处理来减少数据库连接开销
  • 对于大批量操作,考虑分批处理以避免长事务
// 分批处理示例
List<String> allProductIds = ... // 大量商品ID
int batchSize = 100;

for (int i = 0; i < allProductIds.size(); i += batchSize) {
    List<String> batchIds = allProductIds.subList(
        i, Math.min(i + batchSize, allProductIds.size())
    );

    // 批量更新一组商品
    ProductTable pt = ProductTable.$;
    sqlClient.createUpdate(pt)
        .set(pt.active(), true)
        .where(pt.id().in(batchIds))
        .execute();
}
    1. 更新关联数据
  • 对于简单的外键更新,直接设置外键ID

  • 对于复杂的关联更新,使用级联保存功能
  • 谨慎处理多对多关系的更新,确保中间表正确更新
// 简单外键更新
Product updatedProduct = ProductDraft.$.produce(product, draft -> {
    draft.setCategoryId(newCategoryId); // 直接设置外键ID
});

// 复杂关联更新
Product productWithRelations = ProductDraft.$.produce(product, draft -> {
    // 更新类别(一对多关系)
    draft.setCategory(CategoryDraft.$.produce(d -> {
        d.setId(categoryId);
        d.setName("更新的类别名");
    }));

    // 更新标签(多对多关系)
    draft.setTags(Arrays.asList(
        TagDraft.$.produce(d -> d.setId(tagId1)),
        TagDraft.$.produce(d -> d.setId(tagId2))
    ));
});

// 保存带关联的产品
sqlClient.getEntities().save(productWithRelations);
    1. 事务控制
  • 在复杂更新中使用事务确保数据一致性

  • 控制事务边界,避免过长事务
  • 考虑使用Spring的声明式事务管理
@Transactional
public void updateProductAndInventory(String productId, BigDecimal newPrice, int stockChange) {
    // 更新商品价格
    Product product = sqlClient.getEntities().findById(Product.class, productId);
    Product updatedProduct = ProductDraft.$.produce(product, draft -> {
        draft.setPrice(newPrice);
        draft.setModifiedTime(LocalDateTime.now());
    });
    sqlClient.getEntities().save(updatedProduct);

    // 更新库存记录
    Inventory inventory = inventoryRepository.findByProductId(productId);
    Inventory updatedInventory = InventoryDraft.$.produce(inventory, draft -> {
        draft.setStock(inventory.stock() + stockChange);
        draft.setLastUpdated(LocalDateTime.now());
    });
    sqlClient.getEntities().save(updatedInventory);

    // 记录历史
    historyService.recordPriceChange(productId, product.price(), newPrice);
}
    1. 安全考虑
  • 更新前验证用户权限

  • 防止过度暴露内部属性
  • 使用验证器确保数据有效性
public void updateProduct(String productId, ProductDto dto, User currentUser) {
    // 检查权限
    if (!securityService.canEditProduct(currentUser, productId)) {
        throw new AccessDeniedException("无权限修改此商品");
    }

    // 获取实体
    Product product = sqlClient.getEntities().findById(Product.class, productId);
    if (product == null) {
        throw new NotFoundException("商品不存在");
    }

    // 更新实体,只允许特定字段
    Product updatedProduct = ProductDraft.$.produce(product, draft -> {
        // 只允许更新特定字段
        draft.setName(dto.getName());
        draft.setPrice(dto.getPrice());
        draft.setDescription(dto.getDescription());

        // 添加审计信息
        draft.setModifiedBy(currentUser.id());
        draft.setModifiedTime(LocalDateTime.now());
    });

    // 验证实体
    validator.validate(updatedProduct);

    // 保存实体
    sqlClient.getEntities().save(updatedProduct);
}

小结

在本节中,我们深入探讨了Jimmer中实体更新的各个方面,包括通过Draft更新实体、智能变更检测机制、动态更新与静态更新API的对比,以及处理并发更新冲突的策略。

Jimmer的智能变更检测机制是其核心优势之一,它能自动识别实体的变化并生成高效的更新语句,这在处理大型实体和高并发场景时尤为重要。同时,Jimmer提供了多种更新方式,使开发者能够根据不同场景选择最合适的更新策略。

实体更新是数据库应用中最常见的操作之一,正确、高效地处理更新操作对系统性能和数据一致性至关重要。通过遵循本节介绍的最佳实践,我们可以充分利用Jimmer的强大功能,构建高性能、可靠的数据访问层。

5.4 级联保存与关联处理

5.4.1 级联保存的基本概念

在实际业务场景中,实体之间经常存在各种关联关系。例如,一个订单包含多个订单项,一个商品属于一个类别,一个商品可以有多个标签。在进行数据操作时,很多情况下我们需要同时保存或修改多个相互关联的实体。传统的做法可能是逐个保存实体,然后再处理它们之间的关联关系,这种方式不仅代码冗长,而且容易出错。

Jimmer提供了级联保存机制,可以在一次操作中完成整个实体图的保存,包括所有关联实体和它们之间的关系。这种机制具有以下优点:

  1. 简化代码:不需要编写复杂的逻辑来处理实体间的依赖关系
  2. 保证一致性:整个实体图在一个事务中保存,确保数据一致性
  3. 提高效率:自动批处理和优化SQL执行,减少数据库交互
  4. 类型安全:借助Draft API提供完全类型安全的级联操作

  5. 级联类型与配置

Jimmer中的级联保存可以应用于多种关联类型:

  • 一对一关联:例如商品与其详情信息
  • 一对多关联:例如类别与其包含的商品
  • 多对多关联:例如商品与标签的关系

对于这些关联类型,Jimmer提供了灵活的配置选项,允许开发者控制级联行为:

  • 关联深度:控制级联操作的层级深度
  • 保存模式:包括插入、更新、合并、删除等不同操作类型
  • 关联处理:决定如何处理已存在和新增的关联关系

  • 级联保存的触发条件

在Jimmer中,级联保存是通过Draft对象系统自动触发的。当我们创建或修改一个实体,并在其中设置关联实体时,这些关联实体会被自动包含在保存操作中。具体来说,当我们调用sqlClient.getEntities().save(entity)方法时,Jimmer会分析实体的关联关系,并执行必要的级联操作。

5.4.2 一对一关联的级联保存

一对一关联是实体关系中最简单的形式,例如商品与其所属类别之间的关系。在Jimmer中,我们可以轻松实现一对一关联的级联保存。

  • 实体定义

首先,让我们看看实体是如何定义的:

@Entity
@Table(name = "t_product")
public interface Product {
    @Id
    String id();

    String name();

    BigDecimal price();

    // 其他属性...

    @ManyToOne
    @JoinColumn(name = "category_id")
    @Nullable
    Category category();

    @IdView("category")
    @Nullable
    String categoryId();

    // 其他关联...
}

@Entity
@Table(name = "t_category")
public interface Category {
    @Id
    String id();

    String name();

    // 其他属性...
}

在这个定义中,产品(Product)与类别(Category)之间是一对一(从产品角度看是多对一)的关系。

  • 代码示例

下面是一个完整的示例,展示如何通过级联保存一个商品及其关联的类别:

@Test
@DisplayName("一对一关联的级联保存")
public void testOneToOneCascadeSave() {
    // 1. 创建测试数据
    String productId = TEST_PREFIX + "product-" + UUID.randomUUID().toString();
    String categoryId = TEST_PREFIX + "category-" + UUID.randomUUID().toString();

    // 创建包含关联类别的商品
    Product product = ProductDraft.$.produce(draft -> {
        draft.setId(productId);
        draft.setName("智能音箱");
        draft.setPrice(new BigDecimal("299.00"));
        draft.setStock(200);
        draft.setActive(true);
        draft.setCreatedTime(LocalDateTime.now());

        // 添加关联的类别(一对一关联)
        draft.setCategory(CategoryDraft.$.produce(categoryDraft -> {
            categoryDraft.setId(categoryId);
            categoryDraft.setName("智能家居");
            categoryDraft.setDescription("智能家居产品类别");
            categoryDraft.setCreatedTime(LocalDateTime.now());
        }));
    });

    // 2. 执行级联保存
    sqlClient.getEntities().save(product);

    // 3. 验证数据是否正确保存到数据库
    // 验证商品
    Product savedProduct = sqlClient.getEntities().findById(Product.class, productId);
    assertNotNull(savedProduct);
    assertEquals("智能音箱", savedProduct.name());
    assertEquals(categoryId, savedProduct.categoryId());

    // 验证类别
    Category savedCategory = sqlClient.getEntities().findById(Category.class, categoryId);
    assertNotNull(savedCategory);
    assertEquals("智能家居", savedCategory.name());
}

在这个例子中,我们首先创建了一个商品对象,并为其设置了关联的类别。当我们调用save方法时,Jimmer自动保存了商品对象及其关联的类别对象,无需手动分别保存。

  • 工作原理

Jimmer在处理一对一关联保存时会:

  1. 首先保存关联实体(本例中是类别)
  2. 然后保存主实体(本例中是商品),并设置外键关系
  3. 整个过程在单个事务中完成,确保数据一致性

这种机制自动处理了实体之间的依赖关系,简化了开发工作,同时保证了数据的完整性。

5.4.3 一对多关联的级联保存

一对多关联在业务场景中非常常见,例如一个类别包含多个商品。在这种关系中,我们不仅需要保存父实体,还需要处理子实体集合的创建、更新和删除。Jimmer的级联保存机制可以自动处理这些复杂操作。

  • 代码示例

下面的例子展示了如何保存一个类别及其包含的多个商品:

@Test
@DisplayName("一对多关联的级联保存")
public void testOneToManyCascadeSave() {
    // 1. 创建一个类别以及关联的多个商品
    String categoryId = TEST_PREFIX + "category-" + UUID.randomUUID().toString();
    String productId1 = TEST_PREFIX + "product-1-" + UUID.randomUUID().toString();
    String productId2 = TEST_PREFIX + "product-2-" + UUID.randomUUID().toString();

    // 创建包含商品集合的类别
    Category category = CategoryDraft.$.produce(draft -> {
        draft.setId(categoryId);
        draft.setName("电子产品");
        draft.setDescription("各种电子产品");
        draft.setCreatedTime(LocalDateTime.now());

        // 添加关联的商品
        draft.setProducts(Arrays.asList(
            ProductDraft.$.produce(productDraft -> {
                productDraft.setId(productId1);
                productDraft.setName("笔记本电脑");
                productDraft.setPrice(new BigDecimal("5999.00"));
                productDraft.setStock(100);
                productDraft.setActive(true);
                productDraft.setCreatedTime(LocalDateTime.now());
            }),
            ProductDraft.$.produce(productDraft -> {
                productDraft.setId(productId2);
                productDraft.setName("智能手机");
                productDraft.setPrice(new BigDecimal("3999.00"));
                productDraft.setStock(200);
                productDraft.setActive(true);
                productDraft.setCreatedTime(LocalDateTime.now());
            })
        ));
    });

    // 2. 执行级联保存
    sqlClient.getEntities().save(category);

    // 3. 验证数据是否正确保存到数据库
    Category savedCategory = sqlClient.getEntities().findById(Category.class, categoryId);
    assertNotNull(savedCategory);
    assertEquals("电子产品", savedCategory.name());

    Product savedProduct1 = sqlClient.getEntities().findById(Product.class, productId1);
    assertNotNull(savedProduct1);
    assertEquals("笔记本电脑", savedProduct1.name());
    assertEquals(categoryId, savedProduct1.categoryId());

    Product savedProduct2 = sqlClient.getEntities().findById(Product.class, productId2);
    assertNotNull(savedProduct2);
    assertEquals("智能手机", savedProduct2.name());
    assertEquals(categoryId, savedProduct2.categoryId());
}
  • 集合关联的差异检测

Jimmer在处理一对多关联时,会自动执行集合差异检测,识别出需要新增、更新或删除的子实体。这种智能的差异检测机制使得开发者可以专注于业务逻辑,而不是底层的CRUD操作。

例如,在更新一个已存在的类别及其商品时,Jimmer会:

  1. 识别出新增的商品,并执行插入操作
  2. 识别出已存在但被修改的商品,并执行更新操作
  3. 识别出不再存在的商品,根据配置决定是删除还是解除关联

这种机制大大简化了集合类型关联的处理,避免了手动编写复杂的差异检测代码。

  • 性能优化策略

在处理大型一对多关联时,Jimmer采用了多种优化策略:

  1. 批量操作:使用批量SQL语句减少数据库交互
  2. 智能排序:优化SQL执行顺序,减少约束冲突
  3. 部分更新:只更新发生变化的字段,减少不必要的数据传输
  4. 缓存协调:保证缓存与数据库的一致性

这些优化策略确保了即使在处理大型的一对多关联时,也能保持良好的性能。

5.4.4 多对多关联的级联保存

多对多关联是最复杂的关联类型,如商品与标签之间的关系。这种关系通常通过中间表实现,在传统ORM框架中处理起来比较繁琐。Jimmer提供了简化的API,使得多对多关联的处理变得直观而高效。

  • 实体定义

以下是多对多关联的实体定义:

@Entity
@Table(name = "t_product")
public interface Product {
    // 基本属性...

    @ManyToMany
    @JoinTable(
        name = "t_product_tag",
        joinColumnName = "product_id",
        inverseJoinColumnName = "tag_id"
    )
    List<Tag> tags();
}

@Entity
@Table(name = "t_tag")
public interface Tag {
    @Id
    String id();

    String name();

    // 其他属性...

    @ManyToMany(mappedBy = "tags")
    List<Product> products();
}

在这个定义中,一个商品可以有多个标签,一个标签也可以属于多个商品,它们之间通过中间表t_product_tag关联。

  • 代码示例

下面的例子展示了如何保存一个商品及其多个标签:

@Test
@DisplayName("多对多关联的级联保存")
public void testManyToManyCascadeSave() {
    // 1. 创建测试数据 - 产品和标签
    String productId = TEST_PREFIX + "product-" + UUID.randomUUID().toString();
    String tagId1 = TEST_PREFIX + "tag-1-" + UUID.randomUUID().toString();
    String tagId2 = TEST_PREFIX + "tag-2-" + UUID.randomUUID().toString();

    // 创建包含多个标签的商品
    Product product = ProductDraft.$.produce(draft -> {
        draft.setId(productId);
        draft.setName("多功能平板电脑");
        draft.setPrice(new BigDecimal("2999.00"));
        draft.setStock(50);
        draft.setActive(true);
        draft.setCreatedTime(LocalDateTime.now());

        // 添加关联的标签
        draft.setTags(Arrays.asList(
            TagDraft.$.produce(tagDraft -> {
                tagDraft.setId(tagId1);
                tagDraft.setName("畅销");
                tagDraft.setDescription("销量很好的产品");
                tagDraft.setCreatedTime(LocalDateTime.now());
            }),
            TagDraft.$.produce(tagDraft -> {
                tagDraft.setId(tagId2);
                tagDraft.setName("高性价比");
                tagDraft.setDescription("性价比较高的产品");
                tagDraft.setCreatedTime(LocalDateTime.now());
            })
        ));
    });

    // 2. 执行级联保存
    sqlClient.getEntities().save(product);

    // 3. 验证数据是否正确保存到数据库
    // 验证商品
    Product savedProduct = sqlClient.getEntities().findById(Product.class, productId);
    assertNotNull(savedProduct);

    // 验证标签
    Tag savedTag1 = sqlClient.getEntities().findById(Tag.class, tagId1);
    assertNotNull(savedTag1);

    Tag savedTag2 = sqlClient.getEntities().findById(Tag.class, tagId2);
    assertNotNull(savedTag2);

    // 验证多对多关联 - 使用Fetcher加载关联
    ProductFetcher productFetcher = ProductFetcher.$.tags();
    Product productWithTags = sqlClient.getEntities().findById(productFetcher, productId);

    assertEquals(2, productWithTags.tags().size(), "商品应该关联2个标签");

    // 使用标签ID集合验证关联是否正确
    List<String> tagIds = productWithTags.tags().stream()
        .map(Tag::id)
        .toList();

    assertTrue(tagIds.contains(tagId1), "商品应该关联标签1");
    assertTrue(tagIds.contains(tagId2), "商品应该关联标签2");
}
  • 中间表的自动处理

在上面的例子中,我们只需要设置商品的标签集合,Jimmer会自动处理中间表的插入操作。这个过程包括:

  1. 保存商品实体
  2. 保存标签实体
  3. 在中间表中创建商品与标签的关联记录

这种自动化的处理极大地简化了多对多关联的管理,开发者不需要手动维护中间表的数据。

  • 关联集合的修改

对于已存在的多对多关联,修改关联集合也非常简单:

// 修改商品的标签集合
Product updatedProduct = ProductDraft.$.produce(existingProduct, draft -> {
    // 设置新的标签集合,Jimmer会自动处理差异
    draft.setTags(Arrays.asList(
        TagDraft.$.produce(d -> d.setId(newTagId1)),
        TagDraft.$.produce(d -> d.setId(newTagId2)),
        TagDraft.$.produce(d -> d.setId(newTagId3))
    ));
});

// 保存更新后的商品
sqlClient.getEntities().save(updatedProduct);

此时,Jimmer会执行以下操作:

  1. 识别新增的标签关联,在中间表中添加相应的记录
  2. 识别被移除的标签关联,从中间表中删除相应的记录
  3. 保持已存在且未变化的关联不变

这种智能的差异处理机制简化了多对多关联的维护工作。

5.4.5 复杂实体图的保存策略

在实际应用中,我们经常需要处理更复杂的实体关系图,它可能包含多层嵌套的关联,涉及多种关联类型。Jimmer的级联保存机制能够优雅地处理这种复杂场景。

  • 代码示例

下面是一个复杂实体图保存的示例,它包含了类别、商品和标签的多层级关联:

@Test
@DisplayName("复杂实体图的保存")
public void testComplexEntityGraphSave() {
    // 1. 创建具有复杂关联的测试数据
    String categoryId = TEST_PREFIX + "category-complex-" + UUID.randomUUID().toString();
    String productId1 = TEST_PREFIX + "product-complex-1-" + UUID.randomUUID().toString();
    String productId2 = TEST_PREFIX + "product-complex-2-" + UUID.randomUUID().toString();
    String tagId1 = TEST_PREFIX + "tag-complex-1-" + UUID.randomUUID().toString();
    String tagId2 = TEST_PREFIX + "tag-complex-2-" + UUID.randomUUID().toString();

    // 创建具有多级关联的复杂实体图
    Category category = CategoryDraft.$.produce(draft -> {
        draft.setId(categoryId);
        draft.setName("复杂图谱类别");
        draft.setDescription("用于测试复杂实体图的保存");
        draft.setCreatedTime(LocalDateTime.now());

        // 添加关联的商品集合
        draft.setProducts(Arrays.asList(
            // 第一个商品及其标签
            ProductDraft.$.produce(productDraft -> {
                productDraft.setId(productId1);
                productDraft.setName("复杂图谱商品1");
                productDraft.setPrice(new BigDecimal("1999.00"));
                productDraft.setStock(30);
                productDraft.setActive(true);
                productDraft.setCreatedTime(LocalDateTime.now());

                // 添加商品的标签集合
                productDraft.setTags(Arrays.asList(
                    TagDraft.$.produce(tagDraft -> {
                        tagDraft.setId(tagId1);
                        tagDraft.setName("复杂图谱标签1");
                        tagDraft.setDescription("用于测试复杂实体图的保存");
                        tagDraft.setCreatedTime(LocalDateTime.now());
                    })
                ));
            }),
            // 第二个商品及其标签
            ProductDraft.$.produce(productDraft -> {
                productDraft.setId(productId2);
                productDraft.setName("复杂图谱商品2");
                productDraft.setPrice(new BigDecimal("2999.00"));
                productDraft.setStock(20);
                productDraft.setActive(true);
                productDraft.setCreatedTime(LocalDateTime.now());

                // 添加商品的标签集合 - 包含两个标签
                productDraft.setTags(Arrays.asList(
                    TagDraft.$.produce(tagDraft -> {
                        tagDraft.setId(tagId1);
                        tagDraft.setName("复杂图谱标签1");
                        tagDraft.setDescription("用于测试复杂实体图的保存");
                        tagDraft.setCreatedTime(LocalDateTime.now());
                    }),
                    TagDraft.$.produce(tagDraft -> {
                        tagDraft.setId(tagId2);
                        tagDraft.setName("复杂图谱标签2");
                        tagDraft.setDescription("用于测试复杂实体图的保存");
                        tagDraft.setCreatedTime(LocalDateTime.now());
                    })
                ));
            })
        ));
    });

    // 2. 执行级联保存
    sqlClient.getEntities().save(category);

    // 3. 验证数据是否正确保存到数据库及关联关系是否正确
    // 验证各实体基本信息保存成功
    Category savedCategory = sqlClient.getEntities().findById(Category.class, categoryId);
    assertNotNull(savedCategory);

    Product savedProduct1 = sqlClient.getEntities().findById(Product.class, productId1);
    assertNotNull(savedProduct1);

    Product savedProduct2 = sqlClient.getEntities().findById(Product.class, productId2);
    assertNotNull(savedProduct2);

    Tag savedTag1 = sqlClient.getEntities().findById(Tag.class, tagId1);
    assertNotNull(savedTag1);

    Tag savedTag2 = sqlClient.getEntities().findById(Tag.class, tagId2);
    assertNotNull(savedTag2);

    // 验证多对多关联
    ProductFetcher productFetcher = ProductFetcher.$.tags();

    // 验证商品1与标签的关联
    Product product1WithTags = sqlClient.getEntities().findById(productFetcher, productId1);
    List<String> product1TagIds = product1WithTags.tags().stream().map(Tag::id).toList();
    assertTrue(product1TagIds.contains(tagId1));

    // 验证商品2与标签的关联
    Product product2WithTags = sqlClient.getEntities().findById(productFetcher, productId2);
    List<String> product2TagIds = product2WithTags.tags().stream().map(Tag::id).toList();
    assertTrue(product2TagIds.contains(tagId1));
    assertTrue(product2TagIds.contains(tagId2));
}

在这个例子中,我们创建了一个复杂的实体图:一个类别包含两个商品,第一个商品有一个标签,第二个商品有两个标签(其中一个与第一个商品共享)。通过一次调用save方法,整个实体图被保存到数据库中,包括所有的关联关系。

  • 深层级联的性能考虑

处理复杂的实体图时,性能是一个重要考虑因素。Jimmer采取了多种策略来优化性能:

  1. 智能依赖排序:分析实体间的依赖关系,优化保存顺序
  2. 批量操作:尽可能地使用批量SQL语句减少数据库交互
  3. 并行处理:在适当的情况下并行执行不相互依赖的保存操作
  4. 延迟加载:只加载必要的数据,避免不必要的资源消耗

对于特别复杂或大型的实体图,我们可以考虑以下策略:

  1. 分解保存操作:将大型实体图分解为多个较小的保存操作
  2. 控制级联深度:限制级联的层级,避免过深的级联
  3. 使用异步处理:对于非关键路径的保存操作,可以考虑异步处理

小结

Jimmer的级联保存机制提供了一种优雅且高效的方式来处理实体间的复杂关联关系。通过自动处理依赖关系、关联维护和差异检测,Jimmer大大简化了开发者的工作。无论是简单的一对一关联,还是复杂的多层级实体图,Jimmer都能以一致的方式处理,确保数据的完整性和一致性。

在实际开发中,我们可以充分利用这些特性,构建富有表现力且高效的数据访问层,专注于业务逻辑的实现,而不是底层的数据操作细节。在下一节中,我们将探讨Jimmer的实体删除操作及其级联删除功能。

5.5 实体删除操作

在实际业务中,删除数据是常见且必要的操作。然而,传统的删除操作往往简单粗暴,缺乏对数据间关联关系的考量,容易导致数据不一致或意外删除重要信息。Jimmer提供了一套完整且灵活的删除机制,不仅支持基本的数据删除,还能优雅地处理复杂的实体关联关系,确保数据的一致性和完整性。

5.5.1 基本删除操作

  • 按ID删除

最基本的删除操作是按照主键ID删除单个实体。Jimmer提供了简洁的API支持这种操作:

// 删除指定ID的产品
sqlClient.getEntities().delete(Product.class, productId);

这种操作直接根据ID从数据库中删除对应的记录。在内部,Jimmer会生成类似以下的SQL:

DELETE FROM t_product WHERE id = ?

以下是一个完整的测试用例,展示了按ID删除的基本流程:

@Test
@DisplayName("按ID删除")
public void testDeleteById() {
    // 1. 创建测试数据
    String productId = createTestProduct();

    // 验证数据已创建
    Product product = sqlClient.getEntities().findById(Product.class, productId);
    assertNotNull(product, "商品应该已被创建");

    // 2. 执行删除操作
    sqlClient.getEntities().delete(Product.class, productId);

    // 3. 验证数据已被删除
    Product deletedProduct = sqlClient.getEntities().findById(Product.class, productId);
    assertNull(deletedProduct, "商品应该已被删除");
}

在这个例子中,我们首先创建了一个测试商品,然后通过ID删除它,最后验证商品确实已被删除。这种模式在单元测试中非常常见,既验证了功能的正确性,也提供了清晰的使用示例。

  • 按条件删除

在实际应用中,常常需要根据特定条件批量删除数据。例如,删除所有过期的订单、清理特定分类下的商品等。Jimmer提供了DSL风格的条件删除API,与查询条件语法一致,使用非常自然:

// 删除名称以"待删除"开头的商品
int deletedCount = sqlClient.createDelete(ProductTable.$)
    .where(ProductTable.$.name().like("待删除%"))
    .execute();

这种方式生成的SQL类似于:

DELETE FROM t_product WHERE name LIKE '待删除%'

以下是一个完整的测试用例,展示了如何通过条件删除数据:

@Test
@DisplayName("按条件删除")
public void testDeleteByCondition() {
    // 1. 创建测试数据
    String prefix = TEST_PREFIX + UUID.randomUUID().toString().substring(0, 8) + "-";
    createTestProductWithName(prefix + "product-1", "待删除商品1");
    createTestProductWithName(prefix + "product-2", "待删除商品2");
    createTestProductWithName(prefix + "product-3", "其他商品");

    // 验证数据已创建
    List<Product> beforeDelete = sqlClient.createQuery(ProductTable.$)
            .where(ProductTable.$.id().like(prefix + "%"))
            .select(ProductTable.$)
            .execute();
    assertEquals(3, beforeDelete.size(), "应该创建了3个测试商品");

    // 2. 执行条件删除
    int deleted = sqlClient.createDelete(ProductTable.$)
            .where(ProductTable.$.name().like("待删除%"))
            .where(ProductTable.$.id().like(prefix + "%"))
            .execute();

    // 3. 验证删除结果
    assertEquals(2, deleted, "应该删除了2个商品");
    List<Product> afterDelete = sqlClient.createQuery(ProductTable.$)
            .where(ProductTable.$.id().like(prefix + "%"))
            .select(ProductTable.$)
            .execute();
    assertEquals(1, afterDelete.size(), "应该剩下1个商品");
    assertEquals("其他商品", afterDelete.get(0).name(), "剩下的应该是'其他商品'");
}

在这个测试中,我们创建了三个商品,其中两个名称以"待删除"开头。通过条件删除操作,我们精确地只删除了这两个商品,而保留了其他商品。

条件删除的优势在于:

  1. 灵活性 - 可以组合多个条件,精确定位需要删除的数据
  2. 批量处理 - 一次操作可以删除多条符合条件的记录
  3. 安全性 - 通过明确的条件限制,避免误删数据
  4. 性能优化 - 直接在数据库执行删除操作,无需先查询再删除

  5. 批量删除

当我们已知多个实体的ID,需要一次性删除它们时,可以使用批量删除功能:

// 批量删除多个商品
DeleteResult result = sqlClient.getEntities().deleteAll(
    Product.class, 
    Arrays.asList(productId1, productId2)
);

Jimmer会将这转换为高效的批量删除SQL:

DELETE FROM t_product WHERE id IN (?, ?)

以下是一个完整的测试用例:

@Test
@DisplayName("批量删除")
public void testBatchDelete() {
    // 1. 创建测试数据
    String productId1 = createTestProduct();
    String productId2 = createTestProduct();
    String productId3 = createTestProduct();

    // 验证数据已创建
    assertNotNull(sqlClient.getEntities().findById(Product.class, productId1));
    assertNotNull(sqlClient.getEntities().findById(Product.class, productId2));
    assertNotNull(sqlClient.getEntities().findById(Product.class, productId3));

    // 2. 执行批量删除
    DeleteResult result = sqlClient.getEntities().deleteAll(
            Product.class, 
            Arrays.asList(productId1, productId2)
    );

    // 3. 验证删除结果
    assertEquals(2, result.getTotalAffectedRowCount(), "应该删除了2个商品");
    assertNull(sqlClient.getEntities().findById(Product.class, productId1));
    assertNull(sqlClient.getEntities().findById(Product.class, productId2));
    assertNotNull(sqlClient.getEntities().findById(Product.class, productId3));
}

批量删除返回DeleteResult对象,它提供了关于删除操作的详细信息,包括:

  • 总影响行数
  • 各个表的影响行数
  • 是否存在级联删除

这些信息对于理解删除操作的范围和影响非常有帮助,尤其是在处理复杂关联关系时。

5.5.2 级联删除与关联处理

在实际应用中,实体之间往往存在各种关联关系。当删除一个实体时,需要考虑如何处理与之关联的其他实体。Jimmer提供了灵活的配置选项,可以根据业务需求选择不同的关联处理策略。

  • 级联删除

级联删除是指在删除主实体的同时,也删除与之关联的子实体。这在父子关系明确,子实体不能脱离父实体存在的场景中特别适用。例如,删除订单的同时删除所有订单项。

在Jimmer中,可以通过DeleteCommandsetDissociateAction方法配置级联删除行为:

// 删除类别并级联删除其关联的商品
sqlClient.getEntities().deleteCommand(Category.class, categoryId)
    .setDissociateAction(ProductProps.CATEGORY, DissociateAction.DELETE)
    .execute();

以下是一个完整的测试用例,展示了如何执行级联删除:

@Test
@DisplayName("级联删除 - 一对多关系")
public void testCascadeDeleteOneToMany() {
    // 1. 创建测试数据 - 一个类别包含多个商品
    String categoryId = TEST_PREFIX + "category-" + UUID.randomUUID().toString();
    String productId1 = TEST_PREFIX + "product-cascade-1-" + UUID.randomUUID().toString();
    String productId2 = TEST_PREFIX + "product-cascade-2-" + UUID.randomUUID().toString();

    Category category = CategoryDraft.$.produce(draft -> {
        draft.setId(categoryId);
        draft.setName("级联删除测试类别");
        draft.setDescription("用于测试级联删除的类别");
        draft.setCreatedTime(LocalDateTime.now());

        // 添加关联的商品
        draft.setProducts(Arrays.asList(
            ProductDraft.$.produce(productDraft -> {
                productDraft.setId(productId1);
                productDraft.setName("级联删除测试商品1");
                productDraft.setPrice(new BigDecimal("199.00"));
                productDraft.setStock(10);
                productDraft.setActive(true);
                productDraft.setCreatedTime(LocalDateTime.now());
            }),
            ProductDraft.$.produce(productDraft -> {
                productDraft.setId(productId2);
                productDraft.setName("级联删除测试商品2");
                productDraft.setPrice(new BigDecimal("299.00"));
                productDraft.setStock(20);
                productDraft.setActive(true);
                productDraft.setCreatedTime(LocalDateTime.now());
            })
        ));
    });

    // 保存测试数据
    sqlClient.getEntities().save(category);

    // 验证数据已创建
    assertNotNull(sqlClient.getEntities().findById(Category.class, categoryId));
    assertNotNull(sqlClient.getEntities().findById(Product.class, productId1));
    assertNotNull(sqlClient.getEntities().findById(Product.class, productId2));

    // 2. 执行级联删除
    sqlClient.getEntities().deleteCommand(Category.class, categoryId)
            .setDissociateAction(ProductProps.CATEGORY, DissociateAction.DELETE)
            .execute();

    // 3. 验证删除结果
    assertNull(sqlClient.getEntities().findById(Category.class, categoryId), "类别应该已被删除");
    assertNull(sqlClient.getEntities().findById(Product.class, productId1), "商品1应该已被删除");
    assertNull(sqlClient.getEntities().findById(Product.class, productId2), "商品2应该已被删除");
}

在这个测试中,我们首先创建了一个类别及其关联的两个商品。然后,我们通过设置DissociateAction.DELETE实现了级联删除——当删除类别时,自动删除与之关联的所有商品。

  • 关联解除

在某些场景中,我们希望在删除主实体时,不删除关联的子实体,而是解除它们之间的关联关系。例如,在一个用户被删除时,我们可能希望保留其发表的文章,只是将这些文章变为匿名。

Jimmer通过DissociateAction.SET_NULL支持这种行为:

// 删除类别,但保留商品并将其类别设为null
sqlClient.getEntities().deleteCommand(Category.class, categoryId)
    .setDissociateAction(ProductProps.CATEGORY, DissociateAction.SET_NULL)
    .execute();

以下是一个完整的测试用例:

@Test
@DisplayName("关联解除而非删除")
public void testDissociateInsteadOfDelete() {
    // 1. 创建测试数据 - 一个类别包含多个商品
    String categoryId = TEST_PREFIX + "category-dissoc-" + UUID.randomUUID().toString();
    String productId1 = TEST_PREFIX + "product-dissoc-1-" + UUID.randomUUID().toString();
    String productId2 = TEST_PREFIX + "product-dissoc-2-" + UUID.randomUUID().toString();

    Category category = CategoryDraft.$.produce(draft -> {
        draft.setId(categoryId);
        draft.setName("关联解除测试类别");
        draft.setDescription("用于测试关联解除的类别");
        draft.setCreatedTime(LocalDateTime.now());

        // 添加关联的商品
        draft.setProducts(Arrays.asList(
            ProductDraft.$.produce(productDraft -> {
                productDraft.setId(productId1);
                productDraft.setName("关联解除测试商品1");
                productDraft.setPrice(new BigDecimal("199.00"));
                productDraft.setStock(10);
                productDraft.setActive(true);
                productDraft.setCreatedTime(LocalDateTime.now());
            }),
            ProductDraft.$.produce(productDraft -> {
                productDraft.setId(productId2);
                productDraft.setName("关联解除测试商品2");
                productDraft.setPrice(new BigDecimal("299.00"));
                productDraft.setStock(20);
                productDraft.setActive(true);
                productDraft.setCreatedTime(LocalDateTime.now());
            })
        ));
    });

    // 保存测试数据
    sqlClient.getEntities().save(category);

    // 验证数据已创建
    assertNotNull(sqlClient.getEntities().findById(Category.class, categoryId));
    assertNotNull(sqlClient.getEntities().findById(Product.class, productId1));
    assertNotNull(sqlClient.getEntities().findById(Product.class, productId2));

    // 2. 执行删除操作并指定关联解除行为
    sqlClient.getEntities().deleteCommand(Category.class, categoryId)
            .setDissociateAction(ProductProps.CATEGORY, DissociateAction.SET_NULL)
            .execute();

    // 3. 验证结果
    assertNull(sqlClient.getEntities().findById(Category.class, categoryId), "类别应该已被删除");

    // 商品应该仍然存在,但是分类ID应该变为null
    Product product1 = sqlClient.getEntities().findById(Product.class, productId1);
    assertNotNull(product1, "商品1应该仍然存在");
    assertNull(product1.categoryId(), "商品1的分类ID应该为null");

    Product product2 = sqlClient.getEntities().findById(Product.class, productId2);
    assertNotNull(product2, "商品2应该仍然存在");
    assertNull(product2.categoryId(), "商品2的分类ID应该为null");
}

在这个测试中,删除类别后,商品实体继续存在,只是它们的categoryId被设置为null。这种机制确保了关联实体的独立性,避免了不必要的数据丢失。

  • 配置级联关系的方式

Jimmer提供了两种配置级联关系的方式:

  1. 动态配置:通过deleteCommandsetDissociateAction方法,在执行删除操作时指定关联处理策略。
sqlClient.getEntities().deleteCommand(Category.class, categoryId)
    .setDissociateAction(ProductProps.CATEGORY, DissociateAction.SET_NULL)
    .execute();
  1. 静态配置:通过在实体接口中使用@OnDissociate注解,预先定义关联处理策略。
@Entity
public interface Product {
    // ...

    @ManyToOne
    @OnDissociate(DissociateAction.SET_NULL)
    Category category();

    // ...
}

动态配置的优先级高于静态配置,这提供了灵活性,允许在不同场景中根据业务需求选择不同的处理策略。

5.5.3 物理删除与逻辑删除

在企业应用中,数据的重要性使得我们通常不希望直接删除数据。相反,我们可能希望保留数据,但将其标记为"已删除",这就是所谓的逻辑删除。

  • 物理删除

物理删除是指直接从数据库中移除数据记录。这是最基本的删除方式:

// 执行物理删除
sqlClient.getEntities().delete(Product.class, productId, DeleteMode.PHYSICAL);

物理删除的特点是: - 彻底移除数据,无法恢复 - 释放数据库空间 - 操作简单直接

  • 逻辑删除

逻辑删除是指不实际删除数据,而是通过设置一个标志字段来标记记录为"已删除"状态:

// 执行逻辑删除
sqlClient.getEntities().delete(Product.class, productId, DeleteMode.LOGICAL);

要使用逻辑删除,需要在实体上定义逻辑删除字段:

@Entity
public interface Product {
    // ...

    @LogicalDeleted("true")
    boolean isDeleted();

    // ...
}

逻辑删除的优势包括: - 数据保留,便于恢复或审计 - 不影响引用完整性 - 支持"回收站"功能 - 可以通过查询过滤已删除数据

在Jimmer中,可以通过DeleteMode枚举指定删除模式: - DeleteMode.AUTO:根据实体定义自动选择逻辑删除或物理删除 - DeleteMode.LOGICAL:强制使用逻辑删除 - DeleteMode.PHYSICAL:强制使用物理删除

以下是一个测试用例,展示了物理删除和逻辑删除的使用:

@Test
@DisplayName("物理删除和逻辑删除")
public void testPhysicalAndLogicalDelete() {
    // 1. 创建两个测试商品
    String productId1 = createTestProduct();
    String productId2 = createTestProduct();

    // 2. 执行物理删除
    sqlClient.getEntities().delete(Product.class, productId1, DeleteMode.PHYSICAL);

    // 执行逻辑删除
    // 注意:如果Product实体没有定义@LogicalDeleted,这会抛出异常
    try {
        sqlClient.getEntities().delete(Product.class, productId2, DeleteMode.LOGICAL);
    } catch (Exception e) {
        // 预期会抛出异常,因为Product没有定义逻辑删除字段
        assertTrue(e.getMessage().contains("logical"));
    }

    // 验证结果
    assertNull(sqlClient.getEntities().findById(Product.class, productId1), "产品1应该已被物理删除");
}

5.5.4 删除操作的最佳实践

在设计和实现删除操作时,我们需要考虑以下最佳实践,以确保数据的安全性和一致性:

    1. 选择合适的删除策略

根据业务需求和数据重要性,选择合适的删除策略: - 对于核心业务数据(如订单、用户信息等),优先考虑逻辑删除 - 对于临时数据或大量中间数据,可以考虑物理删除 - 对于关联关系,根据业务依赖程度选择级联删除或关联解除

    1. 保护重要数据

避免意外删除重要数据: - 对删除操作添加权限控制 - 在删除前进行验证 - 考虑添加确认步骤或延迟删除机制 - 实现数据备份策略

    1. 优化删除性能

高效处理大批量删除: - 使用批量删除而非循环单条删除 - 对于大批量删除,考虑分批处理 - 在低峰时段执行耗时的删除操作 - 监控删除操作的性能影响

    1. 确保数据一致性

在涉及关联关系的删除中,确保数据一致性: - 合理配置级联关系 - 使用事务保证操作的原子性 - 处理好关联解除后的数据状态 - 验证删除结果

小结

本节介绍了Jimmer中的实体删除操作,包括基本删除、条件删除、批量删除、级联删除和关联处理等功能。Jimmer提供了直观且强大的API,使开发者能够灵活地处理各种删除场景,同时保持数据的一致性和完整性。

通过合理地使用这些功能,我们可以构建出易于维护、具有高数据质量的应用程序。在实际开发中,删除操作与前面介绍的创建、查询和修改操作相结合,共同构成了完整的数据生命周期管理。

在下一节中,我们将探讨Jimmer的事务管理机制,学习如何通过事务保证一系列数据操作的原子性、一致性、隔离性和持久性(ACID特性),进一步提升应用的数据安全性和可靠性。

5.6 章节总结

5.6.1 数据修改的核心设计理念

在本章中,我们深入探讨了Jimmer框架的数据修改机制,这些机制构成了Jimmer独特的数据访问范式。通过本章的学习,我们可以总结出Jimmer数据修改的几个核心设计理念:

  1. 不可变性与可修改性的平衡

Jimmer通过Draft对象这一巧妙的设计,解决了不可变对象模型中数据修改的难题。这种设计使开发者能够同时享受不可变性带来的线程安全、状态可预测等优势,又能便捷地执行数据修改操作。

%%{ init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#015467', 'primaryTextColor': '#fff', 'primaryBorderColor': '#634f7d', 'lineColor': '#8d5130', 'secondaryColor': '#47a1ad', 'tertiaryColor': '#bccf90' } } }%% flowchart LR A[不可变实体<br/>Immutable Entity] -->|create| B[Draft对象<br/>Mutable View] B -->|modify| B B -->|produce| C[新不可变实体<br/>New Immutable Entity] C -->|save| D[(数据库<br/>Database)] style A fill:#015467,stroke:#634f7d,color:#ffffff style B fill:#47a1ad,stroke:#634f7d,color:#015467,stroke-width:2px style C fill:#015467,stroke:#634f7d,color:#ffffff style D fill:#8d5130,stroke:#634f7d,color:#ffffff

如图所示,Jimmer的数据修改流程包含四个关键步骤: - 从不可变实体创建Draft对象(可变视图) - 在Draft对象上执行修改操作 - 通过produce方法生成新的不可变实体 - 将新实体保存到数据库

  1. 类型安全的数据操作

Jimmer的数据修改API完全保持类型安全,避免了在字符串中编写属性名称或在运行时才发现属性名错误的问题。无论是创建新实体、更新已有数据还是执行删除操作,Jimmer都强制在编译时检查类型正确性,大大降低了由于类型错误导致的运行时异常风险。

  1. 高效变更检测与智能更新

Jimmer的数据修改系统内置了高效的变更检测机制,能够精确识别实体对象中发生变更的属性,并仅针对这些属性生成更新语句。这种"智能更新"机制不仅提高了数据库操作的效率,还降低了并发环境中数据冲突的可能性。

  1. 丰富的关联处理策略

在处理实体关联时,Jimmer提供了灵活多样的级联策略,能够满足从简单到复杂的各种关联场景需求。无论是一对一、一对多还是多对多关系,Jimmer都提供了优雅且高效的处理方法,使开发者能够以直观的方式表达业务逻辑中的关联操作。

5.6.2 Jimmer数据修改API的独特价值

与传统ORM框架相比,Jimmer的数据修改API带来了几项显著的价值:

  1. 声明式对象构建

Jimmer的produce方法提供了一种声明式的方式来构建和修改对象。这种Lambda风格的API设计直观易读,使代码结构更加清晰。以产品更新为例:

// 传统ORM方式
Product product = repository.findById(id);
product.setPrice(product.getPrice().multiply(new BigDecimal("0.9")));
product.setStock(product.getStock() + 50);
repository.save(product);

// Jimmer方式
Product updatedProduct = ProductDraft.$.produce(originalProduct, draft -> {
    draft.setPrice(originalProduct.price().multiply(new BigDecimal("0.9")));
    draft.setStock(originalProduct.stock() + 50);
});
sqlClient.getEntities().save(updatedProduct);

在上面的对比中,我们可以看到Jimmer方式虽然代码行数相似,但概念上更加清晰:所有修改都在一个封闭的Lambda表达式中完成,并通过produce方法一次性生成新的不可变对象。

  1. 一致性保障机制

Jimmer的不可变对象模型和Draft机制共同保障了数据操作的一致性。由于所有修改都在Draft对象的封闭环境中完成,然后一次性"提交"生成新的不可变实体,这种模式天然防止了数据在中间状态被访问的问题,大大降低了并发环境中的数据一致性风险。

  1. 细粒度变更控制

Jimmer允许开发者精确控制实体更新的粒度,从单个属性的修改到整个关联图的级联更新。这种灵活性使得开发者能够根据业务需求和性能考量选择最合适的更新策略。

// 仅更新价格属性
sqlClient.createUpdate(ProductTable.$)
    .set(ProductTable.$.price(), new BigDecimal("99.99"))
    .where(ProductTable.$.id().eq("P001"))
    .execute();

// 全实体智能更新(仅更新已变更的属性)
Product updatedProduct = ProductDraft.$.produce(originalProduct, draft -> {
    draft.setPrice(new BigDecimal("99.99"));
    // 其他属性保持不变
});
sqlClient.getEntities().save(updatedProduct);
  1. 内置的并发控制

Jimmer在数据修改API中内置了乐观锁支持,使并发控制成为框架的原生功能而非附加特性。开发者可以通过简单的注解配置启用乐观锁,而无需编写额外的版本检查逻辑。

5.6.3 下一章预告

在本章中,我们深入探讨了Jimmer框架中的数据修改机制,从不可变对象的修改模式到实体的创建、更新和删除操作,再到复杂关联的处理。这些功能共同构成了一个类型安全、高效且优雅的数据操作体系。

在第六章中,我们将把视角转向另一个重要话题:事务与缓存。我们将探讨Jimmer如何处理事务管理,如何设计高效的缓存策略,以及如何在保证数据一致性的同时提高应用性能。事务和缓存是构建高性能企业级应用的关键组件,理解它们在Jimmer框架中的工作方式对于充分发挥框架潜力至关重要。

下一章将讨论的主要内容包括: - 事务管理基础 - 缓存架构设计 - 缓存类型与应用场景 - 事务与缓存的协同机制 - 缓存失效策略和最佳实践 - 缓存一致性保障

通过这些内容,我们将进一步完善对Jimmer框架的整体理解,构建出兼具高性能和可靠性的企业级应用。