【问题标题】:JPA entity relation fetch best practiceJPA 实体关系获取最佳实践
【发布时间】:2026-02-02 07:25:02
【问题描述】:

我们目前正在尝试找出解决常见问题的最佳解决方案。 这是上下文:

上下文:

这就是我们项目的组成方式

  • 我们有model 类,可确保传递的数据有效。
  • 我们有 domain 类,这些是带有 JPA 注释的简单 POJO。


用例

当请求到达 REST API 时,会收到一个格式为 model 对象的对象(作为 JSON)。
然后将该对象转换为要持久化的domain 对象。 最后将持久化的对象转换回model 对象以发送回视图。


问题

当我们将model 对象转换为domain 对象时,我们必须处理子对象。
但在某些情况下,model 对象没有加载子对象,然后我们会遇到 LazyLoading 异常。


具体例子:

model

public class Classroom {

    private final String name;
    private final RoomCapacity roomCapacity;
    private final Set<RoomEquipment> equipments = new HashSet<>();
    private Long id;

    @JsonCreator
    public Classroom(@JsonProperty("name") final String name, @JsonProperty("roomCapacity") final RoomCapacity roomCapacity) {
        if (StringUtils.isBlank(name)) {
            throw new IllegalArgumentException("Cannot build a " + getClass().getName() + " without a name.");
        }
        if (roomCapacity == null) {
            throw new IllegalArgumentException("Cannot build a " + getClass().getName() + " without a " + RoomCapacity.class.getName());
        }
        this.name = name;
        this.roomCapacity = roomCapacity;
    }
}

domain

@Entity
@Table(name = "classroom")
public class ClassroomDomain implements ModelTransformable<Classroom, Long> {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "CLASSROOM_ID")
    private Long id;

    @Column(unique = true)
    private String name;

    @OneToMany(mappedBy = "primaryKey.classroom", cascade = CascadeType.REMOVE)
    private Set<RoomEquipmentDomain> equipments = new HashSet<>();

    private int capacity;


    public ClassroomDomain(Classroom classroom) {
        if (classroom == null) {
            throw new IllegalArgumentException("Cannot instantiate a " + getClass().getName() + " with a null " + Classroom.class.getName());
        }
        id = classroom.getId();
        name = classroom.getName();
        capacity = classroom.getRoomCapacity().getMaxCapacity();
        classroom.getEquipments().forEach(e -> equipments.add(new RoomEquipmentDomain(e, this)));
    }

    @Override
    public Classroom toModel() {
        Classroom classroom = new Classroom(name, new RoomCapacity(capacity));
        classroom.setId(id);
        equipments.forEach(e -> classroom.addEquipment(e.toModel()));

        return classroom;
    }
}

如您所见,domain 类有一个构造函数,它接受一个model 对象。并且domain 可以转换为模型。

因此,当我需要将 domain 转换为 model 时,它会失败,因为在某些情况下,我没有加载 equipments 列表,然后我遇到了 LazyLoading 异常。

当我在 DAO 中调用 toModel() 时它崩溃了。

public Classroom findOneById(Long id) {
        if (id == null) {
            throw new IllegalArgumentException("Cannot find a " + Classroom.class.getName() + " with a null id.");
        }
        ClassroomDomain domain = classroomRepository.findOne(id);

        if (domain == null) {
            throw new ClassroomNotFoundException("No " + Classroom.class.getName() + " found for id :" + id);
        }

        return domain.toModel();
    }


限制

  • 我们希望保留 model 类,因为此应用程序需要是多个程序的通用 API,因此我们需要一个实体模型。
  • 我们不希望将所有关系都作为EAGER 加载。


问题

我们如何在不遇到异常的情况下将数据从domain 转换为model,这种情况下的最佳做法是什么。

【问题讨论】:

  • 不要从 DAO 返回模型(通常是 DTO:数据传输对象)。返回域类(称为实体)。您的业​​务逻辑应该与托管实体一起使用,并且实体到域的转换以及反之亦然在大多数情况下属于表示层。确保您的所有逻辑都在事务内部运行。让 EM 在整个事务期间保持打开状态,以便延迟加载工作。使用 Java EE 容器或 Spring 以声明方式为您管理 EM 和事务。
  • 嗨,@JBNizet 我确实使用弹簧,但我不想使用事务,因为:ClassroomDomain.toModel() 调用 Classroom.getEquipments.toModel(),所以我不每次都想加载整个数据库。该视图实际上是一个角度应用程序,因此:/ 无法访问事务。最后最大的问题是model 类的结构与domain 不同,因为@Embedded 实体。
  • 这没有多大意义。为什么它调用 Classroom.getEquipments.toModel() 是您不想损坏设备吗?为什么你认为加载设备会加载整个数据库?这如何阻止您使用交易?如果您不想加载东西,请不要加载它们。如果你想加载东西,然后加载它们。不使用事务不是避免加载东西的方法。
  • 重点是:当请求对象时(比如说id 为2 的教室),我们得到一个domain 对象,但其余API 应该返回一个model 对象。在某些情况下,domain 对象会加载设备,但有时不会。我不想要交易,因为当我加载包含教室的对象时,.toModelt() 将加载所有教室,然后所有 classromm 将加载他们所有的设备,依此类推。
  • 你不明白什么是交易。交易不会像您声称的那样自动加载所有内容。 您的代码 就是这样做的。通过执行equipments.forEach(e -&gt; classroom.addEquipment(e.toModel()));要求JPA加载教室的设备。它失败了,因为持久性上下文已关闭。使用事务并打开持久性上下文只会使代码做它应该做的事情:成功加载设备。为了避免装载设备,解决方案不是避免交易。解决方案是避免执行该代码。

标签: java jpa lazy-loading relationship dto


【解决方案1】:

我认为您可以使用Open Session in View (or Transaction in View) 设计模式,您将保持数据库连接打开,直到用户请求结束。

当应用程序访问惰性集合时,Hibernate/JPA 将毫无问题地进行数据库查询,不会抛出异常。

请参考:Source 1,Source 2

希望这会有所帮助。

【讨论】:

  • 我使用 Angular 应用程序作为视图。这是问题的一部分,我的后端只是一个 REST API。
  • @AnthonyRaymond 再次,这无关紧要。您可以完美地将 OSIV 与休息控制器一起使用。不过,这在您的情况下是没有用的,因为您从不从控制器返回实体,而是从 DTO 返回。
  • 好吧,我不知道什么叫 DTO,我现在在 google 上找到了一些 instesting 线索。非常感谢。
【解决方案2】:

有一个名为 Blaze-Persistence Entity Views 的库。您仍然需要两种方法,但如果需要,它们可以重用相同的查询逻辑,因为实体视图应用于现有查询。请注意,这也会提高性能,因为它只会获取您实际映射的数据。

我什至为您的确切用例提供了一个示例,external model。通过从外部模型扩展并通过constructor mapping 传递数据,您可以保持外部模型独立,同时获得良好的性能并避免使用EAGER

【讨论】:

    【解决方案3】:

    我最终决定选择:

    • 仅返回对象的方法。
    • 一种返回具有嵌套关联的对象的方法。 (带有 JPQL 请求)。

    不是最好的解决方案,但我找不到另一个解决方案。

    【讨论】: