Spring Boot 和 Hibernate 延迟初始化的奇怪行为

标签 spring hibernate jpa spring-boot spring-data

我正在从 Play 框架迁移到 Spring Boot,并且遇到了一些我很难理解的问题。

考虑这些简单的实体:

@Entity
@Table(name = "system")
public class System {

  @Id
  @Column(name = "systemid", unique = true, nullable = false, length = 36)
  public String systemid;

  @ManyToOne(fetch = FetchType.LAZY, optional=false)
  @JoinColumn(name = "systemtypeid", nullable = false)
  public Systemtype systemtype;

  //This column is added just for testing purposes, see comments below
  @Column(name = "systemtypeid", insertable=false, updatable=false)
  public String systemtypeid;

  @OneToMany(fetch = FetchType.LAZY, mappedBy = "system")
  public Set<SystemItem> items = new HashSet<SystemItem>(0);
}


@Entity
@Table(name = "systemtype")
public class Systemtype {

  @Id
  @Column(name = "systemtypeid", unique = true, nullable = false, length = 36)
  @Access(AccessType.PROPERTY)
  public String systemtypeid;

  public String getSystemtypeid() {
    return systemtypeid;
  }

  public void setSystemtypeid(String systemtypeid) {
    this.systemtypeid = systemtypeid;
  }

  @Column(name = "name", length = 60)
  public String name;

  @OneToMany(fetch = FetchType.LAZY, mappedBy = "systemtype")
  public Set<System> systems = new HashSet<System>(0);
  }

@Entity
@Table(name = "systemitem")
public class SystemItem {

  @Id
  @Column(name = "systemitemid", unique = true, nullable = false, length = 36)
  public String systemitemid;

  @Column(name = "name", length = 60)
  public String name;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "systemid", nullable = false)
  public System system;
}

Controller :
    @RestController
    @RequestMapping(value = ApiController.SYSTEM_URL)
    public class SystemController extends ApiController {

      private static final Logger log = LoggerFactory.getLogger(SystemController.class);

      @Autowired
      private SystemService systemService;

      @RequestMapping(method = RequestMethod.GET)
      public Collection<System> getSystems() throws Exception {
        List<System> systems = systemService.getSystems();
        return systems;
      }
}

最后是服务:
@Service
@Transactional(readOnly = true, value = "tmPrimary")
public class SystemService {
  private static final Logger log = LoggerFactory.getLogger(SystemService.class);

  @Autowired
  SystemRepository systemRepository; //Spring-Data's PagingAndSortingRepository

  public List<System> getSystems() {
    return Lists.newArrayList(systemRepository.findAll());
  }
}

因此,当在 Controller 中调用 getSystems() 方法时,我期望得到一个包含所有系统的列表,其中仅填充了基本字段,因为其他所有内容都是延迟加载的。
这就是发生的事情,还检查了 Hibernate 查询,唯一被查询的表确实是 System 表。到现在为止还挺好。

但我的第一个问题是我还期望 system.systemtype.systemtypeid 会被填充,因为这是系统表中的外键,但这始终为空。如果我使用的是 EclipseLink,我相信这将是预期的行为,但它不应该像 Hibernate 那样。
出于验证目的,我向 System object (systemtypeid) 添加了一个虚拟列,这确实被填充了。所以:
system.systemtypeid = "某事"
system.systemtype.systemtypeid = null

我认为这两个都应该是“某事”,所以我在这里遗漏了什么吗?

同时我发现了这一点,如果您将其注释为使用属性级别访问,Hibernate 只会获取外键 ID。这在这里说明:
https://developer.jboss.org/wiki/HibernateFAQ-TipsAndTricks#jive_content_id_How_can_I_retrieve_the_identifier_of_an_associated_object_without_fetching_the_association

我在使用 Play 时没有出现这种行为,我现在猜测 Play 可能会在幕后做一些莫名其妙的事情来生成这些行为。

一旦数据被序列化为 JSON 以便发送到客户端,第二个问题就开始了。
对于初学者,我希望得到延迟初始化异常,因为转换是在 Controller 内部完成的,并且事务注释方法在服务上。令人惊讶的是,这并没有发生,并且项目集被延迟加载,所有项目都被序列化为 JSON。
再次无法理解这种行为,为什么在 Controller 内部完成延迟加载时根本工作? Spring Boot 是否在 Controller 方法开始时打开 session ?

也弄清楚了这一点(感谢 Nico 为我指明了正确的方向),Spring Boot 默认启用 OpenSessionInView(我必须说这真是个坏主意)所以我不得不添加 spring.jpa.open-in-view=false 到我的应用程序.properties。

更令人惊讶的是,在此之后 system.systemtype 仍然为空,即根本不会延迟加载,我可以让 system.systemtype 加载的唯一方法是如果我将其声明为渴望。

我可能在这里遗漏了一些明显的东西,但是我很难理解这种行为,这与我在使用 Play 时所经历的完全不同,我认为行为应该完全相同。

所有编辑后更新:

剩下的唯一问题是为什么 system.systemtype 永远不会延迟加载。我做了一些测试,如果我为 Systemtype 实体的所有字段添加 getter/setter,它只会延迟加载。这是正常的吗?

查看 Hibernate 文档,现在看来它可能是:
https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/performance.html

"默认情况下,Hibernate3 对集合使用惰性选择获取,对单值关联使用惰性代理获取。这些默认设置对大多数应用程序中的大多数关联都有意义。"

"代理获取:当在关联对象上调用标识符 getter 以外的方法时,获取单值关联。"

那么这是否意味着如果我想延迟加载它们,单值关联中使用的所有实体都需要为其所有字段定义 getter 和 setter?这听起来很令人困惑,因为它在所有其他场景中以不同的方式工作。

在 Play 上,我根本不需要使用 getter/setter,所以也许 Play 正在改变单值关联的默认获取策略?有没有办法做到这一点?

最佳答案

如果您将获取类型设置为惰性,您将获得一个空对象,直到您尝试访问它然后它才会被填充,这是 Spring 的行为,我不确定其他平台。

如果您想让您的外国实体保持懒惰并且您仍然想单独访问外国 id,那么您必须向您的实体添加一个虚拟属性,如下所示:

@Column(name = "system_type_id", insertable = false, updatable = false)
public Integer systemTypeId;

关于序列化延迟加载的对象,我不太确定是诚实的,但我猜当 Jackson 尝试序列化对象时,它会调用 getter 并依次填充延迟对象,我不知道为什么在序列化之后延迟加载的对象是仍然为空,这没有意义,如果数据已经被提取,那么应该填充对象。

关于Spring Boot 和 Hibernate 延迟初始化的奇怪行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33753983/

相关文章:

java - 在执行线程上运行 Spring 方面建议

java - 我希望 Spring Boot Java @Entity 模型类中的 String 成员变量之一在发送到客户端时显示为实际的 JSON 对象

java - 使用 Hibernate 时出现通信异常

spring - Spring 中 Hibernate 数据库生成错误

java - EclipseLink 2.3.3 无法更新 PostgreSQL 上的版本化实体

java - 在 JPA 中,我可以将 @OrderColumn 创建的字段访问到收集的实体中吗?

java - Spring MVC 项目中的 HTTP 服务器状态 404 错误

java - 无法将表单服务转换为注释

sql - Groovy 中的动态命名参数?

java - 使用 RequestFactory 和 JPA 删除 OneToMany 中的实体