【问题标题】:Spring Data REST - Why hibernate fetches children entity with FetchType.LAZY?Spring Data REST - 为什么休眠使用 FetchType.LAZY 获取子实体?
【发布时间】:2020-05-27 11:06:40
【问题描述】:

我有一个简单的用例,我创建了 2 个实体 AccountAccountDetail

这些实体具有如下所示的一对一关系: table relationship

Account 实体:

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@EqualsAndHashCode
public class Account {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    @OneToOne(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private AccountDetail accountDetail;

}

AccountDetail 实体:

@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Setter
@Getter
@EqualsAndHashCode
public class AccountDetail {
    @Id
    private Long id;

    @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @MapsId
    private Account account;

    private String firstName;

    private String lastName;
}

我正在使用具有以下依赖项的 Spring Boot (v2.3.0): - Spring Data JPA:使用 Hibernate 持久化实体 - Rest Repositories:通过 REST 端点公开存储库

问题是 Hibernate 正在获取 AccountDetail 实体,即使我指定了 fetch = FetchType.LAZY

可以在日志中看到2条sql语句: hibernate log


我已经尝试过的:

使用投影:

@Projection(types = {Account.class})
public interface AccountProjection {
    String getUsername();
}

通过以下 GET 请求 http://localhost:8080/accounts/1?projection=accountProjection,我得到:

   {
      "username": "warrior24",
      "_links": {
        "self": {
          "href": "http://localhost:8080/accounts/1"
        },
        "account": {
          "href": "http://localhost:8080/accounts/1{?projection}",
          "templated": true
        },
        "accountDetail": {
          "href": "http://localhost:8080/accounts/1/accountDetail"
        }
      }
    }

不幸的是,使用此解决方案,它仍然发出 2 个 sql 请求。

通过@LazyToOne(LazyToOneOption.NO_PROXY)使用休眠字节码增强

@OneToOne(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@LazyToOne(LazyToOneOption.NO_PROXY)
private AccountDetail accountDetail;

即使这个解决方案也不能解决问题。

结论

我不确定为什么 hibernate 会获取孩子。目前这不是什么大问题,但如果数据库记录的数量变大,可能会影响性能。

有没有人有类似的问题,也许知道如何解决?


更新:包括 GET 请求的调试日志

2020-05-27 15:23:58.414 INFO 2119 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat-3].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet' 2020-05-27 15:23:58.414 INFO 2119 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet' 2020-05-27 15:23:58.414 DEBUG 2119 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Detected StandardServletMultipartResolver 2020-05-27 15:23:58.415 DEBUG 2119 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : enableLoggingRequestDetails='false': request parameters and headers will be masked to prevent unsafe logging of potentially sensitive data 2020-05-27 15:23:58.415 INFO 2119 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms 2020-05-27 15:23:58.415 DEBUG 2119 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/accounts/1", parameters={} 2020-05-27 15:23:58.416 DEBUG 2119 --- [nio-8080-exec-1] o.s.d.r.w.RepositoryRestHandlerMapping : Mapped to org.springframework.data.rest.webmvc.RepositoryEntityController#getItemResource(RootResourceInformation, Serializable, PersistentEntityResourceAssembler, HttpHeaders) 2020-05-27 15:23:58.416 DEBUG 2119 --- [nio-8080-exec-1] o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor 2020-05-27 15:23:58.417 DEBUG 2119 --- [nio-8080-exec-1] stomAnnotationTransactionAttributeSource : Adding transactional method 'findById' with attribute: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly 2020-05-27 15:23:58.417 DEBUG 2119 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(1137877934<open>)] for JPA transaction 2020-05-27 15:23:58.417 DEBUG 2119 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly 2020-05-27 15:23:58.417 DEBUG 2119 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils : Setting JDBC Connection [HikariProxyConnection@646683728 wrapping conn193: url=jdbc:h2:mem:f0df200a-97ea-4e5b-9f80-050ff9a550f7 user=SA] read-only 2020-05-27 15:23:58.417 DEBUG 2119 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@3a65c273] 2020-05-27 15:23:58.417 DEBUG 2119 --- [nio-8080-exec-1] org.hibernate.SQL : select account0_.id as id1_0_0_, account0_.password as password2_0_0_, account0_.username as username3_0_0_ from account account0_ where account0_.id=? 2020-05-27 15:23:58.417 TRACE 2119 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1] 2020-05-27 15:23:58.418 DEBUG 2119 --- [nio-8080-exec-1] org.hibernate.SQL : select accountdet0_.account_id as account_3_1_0_, accountdet0_.first_name as first_na1_1_0_, accountdet0_.last_name as last_nam2_1_0_ from account_detail accountdet0_ where accountdet0_.account_id=? 2020-05-27 15:23:58.418 TRACE 2119 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder : binding parameter [1] as [BIGINT] - [1] 2020-05-27 15:23:58.418 DEBUG 2119 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit 2020-05-27 15:23:58.418 DEBUG 2119 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1137877934<open>)] 2020-05-27 15:23:58.418 DEBUG 2119 --- [nio-8080-exec-1] o.s.jdbc.datasource.DataSourceUtils : Resetting read-only flag of JDBC Connection [HikariProxyConnection@646683728 wrapping conn193: url=jdbc:h2:mem:f0df200a-97ea-4e5b-9f80-050ff9a550f7 user=SA] 2020-05-27 15:23:58.418 DEBUG 2119 --- [nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Not closing pre-bound JPA EntityManager after transaction 2020-05-27 15:23:58.421 DEBUG 2119 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/hal+json', given [*/*] and supported [application/hal+json] 2020-05-27 15:23:58.421 DEBUG 2119 --- [nio-8080-exec-1] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [Resource { content: Account(id=1, username=warrior24, password=1234), links: [<http://localhost:8080 (truncated)...] 2020-05-27 15:23:58.421 DEBUG 2119 --- [nio-8080-exec-1] s.d.r.w.j.PersistentEntityJackson2Module : Serializing PersistentEntity org.springframework.data.jpa.mapping.JpaPersistentEntityImpl@45b7be72. 2020-05-27 15:23:58.423 DEBUG 2119 --- [nio-8080-exec-1] .s.ReloadableResourceBundleMessageSource : No properties file found for [classpath:rest-messages] - neither plain properties nor XML 2020-05-27 15:23:58.424 DEBUG 2119 --- [nio-8080-exec-1] .s.ReloadableResourceBundleMessageSource : No properties file found for [classpath:rest-messages_en] - neither plain properties nor XML 2020-05-27 15:23:58.424 DEBUG 2119 --- [nio-8080-exec-1] .s.ReloadableResourceBundleMessageSource : No properties file found for [classpath:rest-messages_en_GB] - neither plain properties nor XML 2020-05-27 15:23:58.426 DEBUG 2119 --- [nio-8080-exec-1] o.j.s.OpenEntityManagerInViewInterceptor : Closing JPA EntityManager in OpenEntityManagerInViewInterceptor 2020-05-27 15:23:58.426 DEBUG 2119 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 200 OK

【问题讨论】:

  • 会不会是来自equalsAndHashcode的计算?
  • 根据您在获取Account 实体时的当前配置,Hibernate 不应进行额外调用以获取AccountDetail 实体。 AccountDetail 仅在 Account 中对 AccountDetail 的 getter 方法的显式调用发生时才被初始化。那么您能否调试并检查第二个查询是与第一个查询一起进行还是与您的逻辑中的其他地方一起进行
  • 嗨@Vishnu,是的,我同意你的看法。这就是我感到困惑的原因。我编辑了我的帖子以添加调试日志。
  • 我认为@Builder注释创建的toString方法可能正在调用AccountDetailgetter
  • Hibernate 很可能需要知道它是否为空值,或者它是否应该创建代理并因此预先加载关系。您只能通过将关系标记为强制来避免它。检查这个答案:stackoverflow.com/a/33032122/869225

标签: spring hibernate spring-data-rest spring-hateoas


【解决方案1】:

尝试使用@JsonIgnore 注释,指示注释的方法或字段将被基于自省的序列化和反序列化功能忽略。

@JsonIgnore
@OneToOne(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    private AccountDetail accountDetail;

【讨论】:

    【解决方案2】:

    尝试添加@EqualsAndHashCode.Exclude 注释以从equalshashCode 方法中排除accountDetail

    @EqualsAndHashCode.Exclude    
     @OneToOne(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
                    private AccountDetail accountDetail;
    

    【讨论】:

    • 是的,我也试过了,但我得到了同样的结果。我想知道 Hibernate 是否真的获取数据,或者只是 LOG 显示每个实体(父 + 子)的查询
    • stackoverflow.com/a/50919847/12199915 看起来同样的问题
    • 并非如此,就我而言,我不使用默认的 excerptProjection:@RepositoryRestResource public interface AccountRepository extends JpaRepository&lt;Account, Long&gt; { }
    • 尝试排除带有注释@RestResource(exported = false)的acountDetails字段
    • 好的,我试过了,但它只删除了 JSON 响应中这个 AccountDetail 资源的链接。我仍然在 LOG 中看到它正在被获取。
    【解决方案3】:

    解决方案(种类)

    好的,我想我找到了解决方案,但不幸的是它与 Spring Data Rest 不兼容。

    通过添加optional = false,您可以告诉hibernate,您将确保子引用永远不会为空(在我的情况下为AccountDetail)。

    因此,它不需要获取孩子来检查它是否必须创建代理或分配NULL

    @OneToOne(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL, optional = false)
    @LazyToOne(LazyToOneOption.NO_PROXY)
    private AccountDetail accountDetail;
    

    此解决方案的问题是您无法使用 @RepositoryRestResource 注释提供的 POST 端点创建新的 Account 资源。 由于它现在预计 AccountDetail 不为空,因此您需要在 JSON 中提供此资源的链接(尚不存在)。您无法创建 AccountDetail 资源,因为它使用 Account 表中的外键作为主键。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-08-01
      • 1970-01-01
      • 2019-09-30
      • 1970-01-01
      • 2019-12-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多