2
votes

I have a simple use case where I created 2 entities Account & AccountDetail.

These entities have a one-to-one relationship as shown below: table relationship

The Account entity:

@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;

}

The AccountDetail entity:

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

I am using Spring Boot (v2.3.0) with following dependencies: - Spring Data JPA: to persist entities using Hibernate - Rest Repositories: to expose repositories over REST endpoints

The issue is that Hibernate is fetching the AccountDetail entity even though I specified fetch = FetchType.LAZY.

You can see the 2 sql statements in the log: hibernate log


What I already tried:

Using Projection:

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

With the following GET request http://localhost:8080/accounts/1?projection=accountProjection, I get:

   {
      "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"
        }
      }
    }

Unfortunately, with this solution, it still makes 2 sql requests.

Using Hibernate Bytecode Enhancement with @LazyToOne(LazyToOneOption.NO_PROXY)

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

Even this solution does not solve the issue.

Conclusion

I am not sure to understand why hibernate is fetching the child. At the moment it is not a big issue, but if the number of database records gets big it could impact on the performance.

Does anybody have a similar issue and maybe know how to solve it?


Update: Including the debug log for a GET request

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

3
Could it be from the equalsAndHashcode calculation? - Martin Frey
According to your current configuration when you fetch the Account entity, Hibernate should not be making an additional call to get the AccountDetail entity. AccountDetail get initialized only when an explicit call to getter method for AccountDetail in Account happens. So can you debug and check if the second query is made along with first or somewhere else in your logic - vishnu
Hi @Vishnu, yes I agree with you. This is why I am confused. I edited my post to add the debug log. - Servan Fichet
I think the toString method created by @Builder annotated might be making a call to the getter of AccountDetail - vishnu
Most likely hibernate needs to know if it is a null value or if it should create a proxy and thus loading the relation upfront. You can only avoid it by marking the relation as mandatory. Check this answer: stackoverflow.com/a/33032122/869225 - Martin Frey

3 Answers

0
votes

Try to use @JsonIgnore annotation that indicates that the annotated method or field is to be ignored by introspection-based serialization and deserialization functionality.

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

Try to put @EqualsAndHashCode.Exclude annotation to exclude accountDetail from equals and hashCode methods

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

SOLUTION (kind of)

Okay I think I found the solution but unfortunately it is not compatible with Spring Data Rest.

By adding optional = false, you can tell hibernate that you will ensure that the child reference will never be null (in my case AccountDetail).

Therefore, it will not need to fetch the child in order to check if it has to create a proxy or assign NULL.

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

The issue with this solution is that you cannot create a new Account resource using the POST endpoint provided with the @RepositoryRestResource annotation. Since, it now expects the AccountDetail to be not null, you need to provide the link of this resource in the JSON (which is not existing yet). And you cannot create the AccountDetail resource because it uses as primary key the foreign key from the Account table.