Spring Boot和Hibernate Lazy Initialization奇怪的行为

时间:2015-11-17 10:00:20

标签: spring hibernate jpa spring-boot spring-data

我已经从Play Framework迁移到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;
}

控制器:

    @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());
  }
}

因此,当在控制器中调用getSystems()方法时,我希望得到一个包含所有系统的列表,只填充基本字段,因为其他所有内容都是延迟加载的。 这就是发生了什么,还检查了Hibernate查询,唯一被查询的表确实是System表。到目前为止一切都很好。

但是我的第一个问题是我还期望system.systemtype.systemtypeid会被填充,因为这是系统表中的外键,但是它总是为空。如果我使用EclipseLink,我相信这将是预期的行为,但它不应该像Hibernate那样。 我为System对象(systemtypeid)添加了一个虚拟列以进行验证,这确实得到了填充。所以: system.systemtypeid =&#34; Something&#34; system.systemtype.systemtypeid = null

我认为这些都应该是&#34; Something&#34;,所以我在这里遗漏了什么?

我同时想到这一点,如果你注释它以使用属性级别访问,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以便发送到客户端,第二个问题就会开始。 对于初学者,我希望得到懒惰的初始化异常,因为转换是在控制器内完成的,并且事务注释方法在服务上。令人惊讶的是,这并没有发生,并且设置的项目被延迟加载,并且所有项目都被序列化为JSON。 再一次无法理解这种行为,为什么延迟加载在控制器内完成呢? Spring Boot是否在控制器方法开始时打开一个会话?

也弄明白了(感谢Nico指出我正确的方向),Spring Boot默认启用OpenSessionInView(我必须说非常糟糕的主意)所以我不得不添加spring.jpa.open-in-view = false我的applications.properties。

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

我可能会遗漏一些明显的东西,但我很难理解这种行为,这与我在使用Play时遇到的情况完全不同,我认为行为应该完全相同。

所有编辑后更新:

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

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

&#34;默认情况下,Hibernate3对集合使用延迟选择提取,对单值关联使用延迟代理提取。这些默认值对大多数应用程序中的大多数关联都有意义。 &#34;

&#34;代理提取:当在关联对象上调用除标识符getter之外的方法时,将获取单值关联。 &#34;

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

On Play我根本不需要使用getter / setter,所以Play可能会为单值关联修改默认的提取策略吗?有没有办法做到这一点?

2 个答案:

答案 0 :(得分:0)

如果你将fetch类型设置为lazy,你将获得一个null对象,直到你尝试访问它然后它被填充,这是春天的行为,我不确定其他平台。

如果你想让你的外国实体变得懒惰并且你仍然想要单独访问外部id,那么你必须像你这样添加一个虚拟属性:

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

关于序列化延迟加载的对象,我并不完全确定是诚实的,但我想当Jackson试图序列化对象时,它会调用getter并反过来填充惰性对象,我不知道为什么在序列化后懒惰 - 加载的对象仍为null,没有意义,如果已经提取了数据,那么应该填充该对象。

答案 1 :(得分:0)

对于关于JSON填充的“第二个问题”,您是否正在使用OpenSessionInView过滤器? http://docs.spring.io/spring/docs/2.5.x/api/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.html