Spring 数据 REST - 为什么休眠使用 FetchType.LAZY 获取子实体?
Spring Data REST - Why hibernate fetches children entity with FetchType.LAZY?
我有一个简单的用例,我创建了 2 个实体 Account
& AccountDetail
。
这些实体具有如下所示的一对一关系:
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 存储库:通过 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 个请求。
将 Hibernate 字节码增强与 @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
尝试使用 @JsonIgnore
注释,指示注释的方法或字段将被基于内省的序列化和反序列化功能忽略。
@JsonIgnore
@OneToOne(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private AccountDetail accountDetail;
尝试放置 @EqualsAndHashCode.Exclude
注释以从 equals
和 hashCode
方法中排除 accountDetail
@EqualsAndHashCode.Exclude
@OneToOne(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private AccountDetail accountDetail;
解决方案(某种)
好的,我想我找到了解决方案,但不幸的是它与 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(尚不存在)中提供此资源的 link。并且您无法创建 AccountDetail
资源,因为它使用来自帐户 table.
的外键作为主键
我有一个简单的用例,我创建了 2 个实体 Account
& AccountDetail
。
这些实体具有如下所示的一对一关系: 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 存储库:通过 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 个请求。
将 Hibernate 字节码增强与 @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
尝试使用 @JsonIgnore
注释,指示注释的方法或字段将被基于内省的序列化和反序列化功能忽略。
@JsonIgnore
@OneToOne(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private AccountDetail accountDetail;
尝试放置 @EqualsAndHashCode.Exclude
注释以从 equals
和 hashCode
方法中排除 accountDetail
@EqualsAndHashCode.Exclude
@OneToOne(mappedBy = "account", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private AccountDetail accountDetail;
解决方案(某种)
好的,我想我找到了解决方案,但不幸的是它与 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(尚不存在)中提供此资源的 link。并且您无法创建 AccountDetail
资源,因为它使用来自帐户 table.