Spring Data JPA - "无法初始化代理 - 没有会话" - 带有标记为事务的方法

63

我有一个模型,它有一个相当庞大的子实体图,并且Hibernate最终会进行约9个语句才能懒加载所需的所有数据,但是到了大约4层深度,我遇到了“无法初始化代理 - 没有会话”错误,我不确定原因。

控制器

@Transactional(readOnly = true)
@RequestMapping(value = "/v2/plans", method = RequestMethod.GET)
public @ResponseBody List<PlanPresenter> show(HttpServletRequest request) throws Exception {
  List<PlanPresenter> planPresenters = new ArrayList<>();

  CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
  CriteriaQuery<Plan> planQuery = criteriaBuilder.createQuery(Plan.class);
  Root<Plan> root = planQuery.from(Plan.class);

  if (request.getParameter("region") != null || request.getParameter("group") != null) {
    List<Predicate> criteria = new ArrayList<Predicate>();
    if (request.getParameter("region") != null) {
      criteria.add(criteriaBuilder.equal(root.get(Plan_.region), request.getParameter("region")));
    }

    if (request.getParameter("group") != null) {
      criteria.add(criteriaBuilder.equal(root.get(Plan_.groupCode), request.getParameter("group")));
      criteria.add(root.get(Plan_.planSetId).in(groupPlanSetIds));
    } else {
      criteria.add(root.get(Plan_.planSetId).in(currentPlanSetIds));
    }

    Query query = entityManager.createQuery(planQuery.where(criteriaBuilder.and(criteria.toArray(new Predicate[]{}))));

    for (Plan plan : (List<Plan>)query.getResultList()) {
      planPresenters.add(new PlanPresenter(plan));
    }
  }

  return planPresenters;
}

演示者

public class PlanPresenter {
  public String id;
  public String plan_set_id;
  public String region;
  public String name;
  public String description;
  public HashMap<String, Object> details = new HashMap<String, Object>();

  public PlanPresenter(Plan plan) throws Exception {
    this.id = String.valueOf(plan.id);
    this.plan_set_id = String.valueOf(plan.planSetId);
    this.region = plan.region.trim();
    this.name = plan.getName();
    this.description = plan.getDescription();

    this.details.put("spanish_plan", plan.isSpanishPlan());
    this.details.put("mutually_exclusive", plan.isMutuallyExclusive());
    this.details.put("group_plan", plan.isGroupPlan());
    this.details.put("group_code", plan.groupCode.trim());
    this.details.put("family_plan", plan.isFamilyPlan());
    this.details.put("price", plan.getPrice());
    this.details.put("enrollment_fee", plan.getEnrollmentFee());
    this.details.put("riders", plan.getRiders());
  }
}
计划
@Entity
public class Plan implements Serializable {
  private static final long serialVersionUID = 7639611964474770505L;

  private static List<String> familyPlanShortNames = Arrays.asList("ABCD");
  @Transient
  private String description = "";

  (Column definitions)

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "XXXX", insertable = false, updatable = false, nullable = true)
  @NotFound(action = NotFoundAction.IGNORE)
  public PlanDetail planDetail;

  @OneToMany(fetch = FetchType.LAZY)
  @JoinColumn(name = "XXXX", insertable = false, updatable = false, nullable = true)
  @OrderBy("XXXX")
  @NotFound(action = NotFoundAction.IGNORE)
  public List<Rider> riders;

  public String getName() {
    return this.planDetail != null ? this.planDetail.longName.trim() : null;
  }

  public Boolean isSpanishPlan() {
    return this.language.trim().equals("ES");
  }

  public Boolean isMutuallyExclusive() {
    return this.mutuallyExclusive.trim().equals("Y");
  }

  public Boolean isGroupPlan() {
    return this.groupCode != null && !this.groupCode.trim().equals("");
  }

  public Boolean isFamilyPlan() {
    return familyPlanShortNames.contains(this.planDetail.shortName.trim());
  }

  public BigDecimal getPrice() {
    return this.planDetail != null ? this.planDetail.price.setScale(2) : null;
  }

  public BigDecimal getEnrollmentFee() {
    return this.planDetail != null ? this.planDetail.enrollmentFee.setScale(2) : null;
  }

  public String getDescription() {
    if (this.planDetail != null && this.planDetail.brochureSections != null) {
      this.planDetail.brochureSections.forEach((brochureSection) -> {
        if (brochureSection.type.trim().equals("P1") && brochureSection.order == 1) {
          this.description = this.description + " " + brochureSection.text.trim();
        }
      });
    }

    return this.description.trim();
  }

  public List<HashMap<String, Object>> getRiders() {
    List<HashMap<String, Object>> riders = new ArrayList<HashMap<String, Object>>();
    if (this.riders != null && this.riders.size() > 0) {
      this.riders.forEach((rider) -> {
        HashMap<String, Object> planRider = new HashMap<String, Object>();
        planRider.put("name", rider.getName());
        planRider.put("price", rider.getPrice());
        planRider.put("description", rider.getDescription());
        riders.add(planRider);
      });
    }
    return riders;
  }
}

计划详情

@Entity
public class PlanDetail implements Serializable {
  private static final long serialVersionUID = 2256881691562712018L;

  (Column definitions)

  @OneToMany(fetch = FetchType.LAZY)
  @JoinColumn(name = "XXXX", referencedColumnName = "XXXX", insertable = false, updatable = false, nullable = true)
  @OrderBy("XXXX")
  @NotFound(action = NotFoundAction.IGNORE)
  public List<BrochureSection> brochureSections;
}

宣传册章节

@Entity
public class BrochureSection implements Serializable {
  private static final long serialVersionUID = 1856191232387921427L;

  (Column definitions)
}

异常

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.models.PlanDetail.brochureSections, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:576) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:215) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:555) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:143) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final]
at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:294) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final]
at java.lang.Iterable.forEach(Iterable.java:74) ~[?:1.8.0_66]
at com.models.Plan.getDescription(Plan.java:100) ~[classes/:?]
at com.presenters.v2.PlanPresenter.<init>(PlanPresenter.java:20) ~[classes/:?]
at com.controllers.v2.PlansController.show(PlansController.java:64) ~[classes/:?]

非常感谢任何帮助。


代码的平静会有所帮助。简单的方法可以帮助您从服务中返回VO对象或更改FetchType。 - Anton N
1
所以,如果我将获取类型更改为EAGER,我可以让它工作,但出于性能原因,我并不真正想这样做。 - douglasrlee
根据错误和代码,它在手册部分失败。你能否在你的getdescription()方法中添加hibernate.instance(this.plandetail.getbrochure())?由于这个属性是延迟加载的,在get description中无法找到它,因此为了使用它,您必须先加载它,然后对其进行任何想做的操作。请让我知道这是否有帮助。顺便说一句,我也是Hibernate的新手,并亲身遇到了这个错误,通过这种方式得以解决。 - LearningPhase
首先,在我看来,将Web层作为事务边界是一个不好的想法。话虽如此,你的控制器并不是事务性的,在你的堆栈跟踪中没有任何与事务相关的内容。在与控制器相同的上下文中加载一个配置类,并添加@EnableTransactionManagement(如果使用XML,则添加<tx:annotation-driven />)。如果你已经这样做了,但是由ContextLoaderListener加载,它将不起作用,因为AOP只应用于同一应用程序上下文中。 - M. Deinum
1
快速解决方案:myEntity.getListOfThings().size(); 强制 JPA 初始化集合。 - Anton N
显示剩余3条评论
7个回答

156
如果你希望保留“懒加载”功能并且正在使用Spring Boot,只需在application.properties中添加以下配置即可:
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

52
实际上,这是一种反模式。如需进一步解释,请查看https://vladmihalcea.com/2016/09/05/the-hibernate-enable_lazy_load_no_trans-anti-pattern/。 - darkled
3
@darkled - 我认为“在使用此功能之前,可能存在潜在的性能影响需要您注意”和“这是反模式”之间实际上存在差异。并非每个应用程序都需要从其数据库访问代码中获得最高可能的性能,有时不必花费数小时的开发人员时间来定义预先加载哪些属性与哪些查询更为重要。 - Jules
3
@Jules同意,不是所有的应用程序都需要最高性能,但这只是一个通常效果不佳且有很高风险的解决方案的例子(这是维基百科反模式的定义 https://en.wikipedia.org/wiki/Anti-pattern)。 - darkled
4
有没有其他解决问题的方法?也许可以直接用JPQL编写查询语句? - robert trudel
22
这是有趣的信息,但问题更多地涉及为什么在我看来 "@Transactional" 不足以保持事务处于打开状态。 - Tristan
显示剩余6条评论

4
“懒加载”可以保留,而无需设置“enable_lazy_load_no_trans”参数。我在使用Spring Data JPA时找到的最简单的解决方案是@NamedEntityGraph。https://www.baeldung.com/spring-data-jpa-named-entity-graphs 缺点是我不能在@NamedEntityGraph中有多个集合。添加第二个集合会导致异常org.hibernate.loader.MultipleBagFetchException:cannot simultaneously fetch multiple bags: 因此,如果您不想使用反模式,并且只有一个要加载的集合,则@NamedEntityGraph和@EntityGrpah可与Spring Data JPA一起使用。

你确实可以通过使用Set而不是Collection或List来避免multipleBagException... - user1006641
1
根据https://vladmihalcea.com/hibernate-multiplebagfetchexception/的说法,使用集合而不是列表是一个坏主意。 - Wim Deblauwe

3

在方法上添加@Transactional对我有用。


同样的,不知道为什么有些服务方法需要,有些方法却不需要。 - undefined

2

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true 这是一种反模式,强烈建议避免使用。

当子类包含 @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) 时,经常会出现 could not initialize proxy 异常。

我建议在你的关系中使用 fetch = FetchType.EAGER 而不是使用 LAZY。虽然不是最好的方法,但比使用反模式好得多。


FetchType.EAGER 对我很有用。我认为你的答案应该得到更多的投票,谈论反模式很容易,但你实际上提供了一种替代方案,谢谢! - chriszichrisz

0

我遇到了同样的异常,问题是我的实体映射丢失了。

@Entity
Class A {
}

@Entity
Class B {

   private A a;
}

解决方案

@Entity
Class B {

  @OneToOne
  @MapsId
  private A a;
}

不确定是否是同一个问题,但也许这会帮助人们在未来理解这一部分。 :) - Raid

0
在我的情况下,我有一个带有多个ManyToOne和ManyToMany关系的类。因此,需要从数据库加载多个对象列表。我通过将这些对象转换为DTO解决了这个问题。 不再传输可能与其他对象关联的对象列表,而是传递一个只包含原始类型和字符串的DTO列表。 这是一个巨大的性能提升。

0
只需在您的服务方法上方添加@Transactional,并确保从SpringBoot而不是jakarta导入Transactional

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接