谨慎使用 jpa 关系 - felixs


这篇博文的灵感来自于我为我的上一个客户所做的工作,该客户想要模块化他的单体,但有数百个实体的复杂混乱。我们开始消除模块之间的循环依赖,这种依赖特别是由于 jpa 关系及其对服务层的影响而发生的。这篇博文试图解释为什么更简单的映射方法更好。

JPA 是一个很好的工具,可以将您的实体映射到表,但它通过注释的建模范例导致了复杂的意大利面条式混乱。一种更简单、更好的关系建模方法是只使用普通 ID 而不是引用另一个实体。

@Entity(name = "user")
public class User {
    @Id
    private UUID id;
   
// Use this
    private UUID organizationId;
   
// And NOT this
    @ManyToOne
    @JoinColumn(name=
"organization_id")
    private Organization organization;
}

这种简化的建模方法最重要的好处之一是,如果您将您的实体及其所有字段视为其状态,那么现在很容易意识到组织的状态变化并不意味着用户的状态变化。这听起来很明显,但是如果您一直使用 jpa 关系,您必须知道哪些引用对象是用户实体的一部分,哪些不是。我们正在减少推理用户实体的认知负荷,这反过来又降低了代码库的整体复杂性。
现在可以轻松测试用户实体,而无需创建每个引用的实体。我们将操纵用户内部组织的意大利面条代码的风险降至最低。我们进一步提高了用户的加载性能,因为通常会急切地获取这些类型的多对一关系。

使用普通 ID 应该是您的默认设置。强引用其他实体应该是例外。

强引用参考案例
如果关系是一个实体独有的,您可以使用强引用。

public class Invoice {
    @Id
    private UUID id;
    @OneToMany(cascade=ALL, orphanRemoval=true, fetch=EAGERLY)
    @JoinColumn(name="invoice_id")
    private List<InvoiceItem> items;
    private MonetaryAmount sum;
}

这种建模方式明确了发票项目

InvoiceItem
在整个生命周期中对发票的责任。
必须通过发票实体创建、删除或更新发票项目,然后相应地更新其金额。这也意味着您不应为发票项目提供存储库。您应该通过加载发票来查询项目。

主要区别在于这些项目仅由发票强烈引用,并且此引用专用于发票。更改项目的状态会更改发票的状态。
明确一个实体对另一个实体的所有权。默认使用 id 的弱引用。

视图层的快速提示
有些人可能会建议您应该在视图需要时创建具有强引用的关系。此参数将视图问题强加到您的建模方法中,这会迅速导致N+1 查询问题。它还通过遍历加载数据的强引用来隐藏客户端需求,从而削弱您的服务 api。

// Do not model your relations only to implement these view requirements
public class UserController {
    public List<UserDto> listUsers() {
        return userService.list().stream().map(user -> UserDto(
            user.id,
            OrganizationDto(
                user.organization.id,
                user.organization.name
            )
        )).toList();
    }
}

更好的方法如下:
public class UserController {
    public List<UserDto> listUsers() {
        return userService.list().stream().map(user -> UserDto(
            user.id,
            user.organizationId
        )).toList();
    }
}
public class OrganizationController {
    public List<OrganizationDto> listOrganizations(
        final Set<UUID> ids
    ) {
        return organizationService
                    .listByIds(ids)
                    .stream()
                    .map(organization ->
                        OrganizationDto(
                            organization.id,
                            organization.name
                        )
                    ).toList();
    }
}

现在我们在服务层明确地对查询需求建模,并将我们在领域模型中的耦合保持在最低限度。我们还为我们的客户提供更好的通用 api。随着前端不断变化的需求,我们可以自由地组合我们需要的所有数据,而不必担心因过度获取而导致性能下降。
在此示例中,可能只是删除了在用户列表视图的前端显示组织名称的要求。如果发生这些不断变化的前端需求,我们不必更改后端 api。我们减少了前端和后端需求之间的耦合。