Jmix 2.x 的 @OneToMany 懒集合在 detached 后会自动加载吗?

环境:Jmix 2.7.6

现象

保存时报错:
IllegalStateException: Cannot get unfetched attribute [productionOrderDetailsLabelList]
from detached object com.sharing.sale.entity.ProductionOrderDetails-xxxx [detached].

场景:我在一个视图里只修改了子实体(DemandDetailsBack)的 purchaseStatus 字段,保存时却报 owner 实体(ProductionOrderDetails)上的 @OneToMany 懒集合 unfetched。

我的理解

DemandDetailsBackProductionOrderDetails@Composition 子实体,所以保存时 owner 被连带拖进保存图;而 ProductionOrderDetails 上有几个 @OneToMany 懒集合(productionOrderDetailsLabelList 等)没在 fetch plan 里声明、实体又已 detached,保存时框架遍历属性访问到 getter 就抛了这个错。

我现在的解法是在保存前用 dataManager.load + 显式 fetchPlan 把这些集合重新加载一遍,因为这个问题是偶尔会出现,很难复现出来,不确定是否能解决。

想问两个问题

  1. @OneToMany 集合在 detached 后会自动懒加载吗?
    官方文档说 detached 对象的「引用属性」会自动懒加载(示例都是 to-one 的 getCustomer().getAddress())。那 @OneToMany / @ManyToMany 这种集合呢?文档里没找到明确说法,想确认一下。

  2. 「保存图级联遍历 + detached 懒集合」这个场景是已知边界吗?
    我上面这种"改 composition 子实体的字段、结果把 owner 的懒集合拖进保存图导致 unfetched"的情况,有没有更标准的写法?还是说我现在这种"保存前 fetch plan 重载"就是官方推荐做法?

谢谢!

  1. 会。但是最好是在 fetch plan 里指定需要使用的子实体的 fetch plan,否则子实体的属性不会加载。
  2. 应该不需要再加载一遍。根据不同使用场景加载不同的 fetch plan,这个是 Jmix 支持的。我用下面的实体测了测,没有重现你的问题。
@JmixEntity
@Table(name = "PROJ2_ORDER")
@Entity(name = "proj2_Order")
public class Order {
    @JmixGeneratedValue
    @Column(name = "ID", nullable = false)
    @Id
    private UUID id;

    @InstanceName
    @Column(name = "NAME")
    private String name;

    @Composition
    @OrderBy("name")
    @OneToMany(mappedBy = "order",fetch =  FetchType.LAZY)
    private List<OrderLine> lines;

    @Composition
    @OrderBy("name")
    @OneToMany(mappedBy = "order",fetch =  FetchType.LAZY)
    private List<OrderTag> tags;
    @Test
    void testOrderLazyLoad() {
        var one = dataManager.load(Order.class)
                .query("select o from proj2_Order o where o.name = :name")
                .parameter("name", "test")
                .fetchPlan(builder -> {
                    builder.addFetchPlan(FetchPlan.BASE);
                    builder.add("lines", FetchPlan.BASE);
//                    builder.add("tags", FetchPlan.BASE); // 不管这行是否注释掉,保存都没有问题
                })
                .one();

        one.getLines().get(0).setName("test" + String.valueOf(Math.random() * 100).substring(0, 2));
        SaveContext saveContext = new SaveContext();
        saveContext.saving(one.getLines().get(0)); // 保存修改的 line
        saveContext.saving(one); // 保存父实体
        var savedEntities = dataManager.save(saveContext);
        System.out.println(1);
    }