Home > Software engineering >  Why Spring JPA still retrieves associated entities with FetchMode.LAZY?
Why Spring JPA still retrieves associated entities with FetchMode.LAZY?

Time:04-09

I have Account entity with 3 associated entities inside with configuration like that:

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

Also I have JpaRepository method

  Optional<Account> findByEmail(String email);

When executing findByEmail it queries with separated SELECT clauses all entities inside Account. Why it happens? I don't use getters inside service logic, it happens exactly on code:

Account account = accountRepository.findByEmail(email).orElseThrow(
        () -> new ResourceNotFoundException("Account with %s email is not found", email));

Parent entity:

@Entity
@Table(name = "account")
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@Builder(toBuilder = true)
@Getter
public class Account extends BaseEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Long id;

  @Column(name = "encoded_password")
  @Setter
  private String password;

  @Column(name = "email")
  @Setter
  private String email;

  @Column(name = "first_name")
  @Setter
  private String firstName;

  @Column(name = "last_name")
  @Setter
  private String lastName;

  @Column(name = "avatar_file_name")
  private String avatarFileName;

  @Column(name = "last_logged_in_time")
  private LocalDateTime lastLoggedInTime;

  @Column(name = "role", updatable = false)
  @Enumerated(EnumType.STRING)
  private AccountRoleEnum role;

  @Column(name = "status")
  @Enumerated(EnumType.STRING)
  @Setter
  private AccountStatusEnum status;

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  private Social social;

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  private Location location;

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  private PaymentInfo paymentInfo;

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  private Activation activation;

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  private Company Company;

  @OneToMany(
      fetch = FetchType.LAZY,
      mappedBy = "account",
      cascade = CascadeType.ALL
  )
  @ToString.Exclude
  private final Set<Resume> resumeSet = new HashSet<>();

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;

    Account account = (Account) o;
    return Objects.equals(id, account.id)
        && Objects.equals(password, account.password)
        && Objects.equals(email, account.email)
        && Objects.equals(firstName, account.firstName)
        && Objects.equals(lastName, account.lastName)
        && Objects.equals(avatarFileName, account.avatarFileName)
        && Objects.equals(lastLoggedInTime, account.lastLoggedInTime)
        && role == account.role
        && status == account.status
        && Objects.equals(social, account.social)
        && Objects.equals(location, account.location)
        && Objects.equals(paymentInfo, account.paymentInfo)
        && Objects.equals(activation, account.activation)
        && Objects.equals(Company, account.Company)
        && Objects.equals(resumeSet, account.resumeSet);
  }

  @Override
  public int hashCode() {
    return Objects.hash(super.hashCode(),
        id, password, email, firstName, lastName, avatarFileName, lastLoggedInTime,
        role, status, social, location, paymentInfo, activation, Company, resumeSet);
  }
}

Child entity:

@Entity
@Table(name = "location")
@NoArgsConstructor
@AllArgsConstructor
@ToString(callSuper = true)
@Builder(toBuilder = true)
@Getter
public class Location extends BaseEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id")
  private Long id;

  @Column(name = "country")
  private String country;

  @Column(name = "city")
  private String city;

  @OneToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "account_id", updatable = false)
  @ToString.Exclude
  private Account account;

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    if (!super.equals(o)) return false;
    Location location = (Location) o;
    return Objects.equals(id, location.id)
        && Objects.equals(country, location.country)
        && Objects.equals(city, location.city)
        && Objects.equals(account, location.account);
  }

  @Override
  public int hashCode() {
    return Objects.hash(super.hashCode(), id, country, city, account);
  }
}

CodePudding user response:

One thing that is definitly a problem is

private final Set<Resume> resumeSet = new HashSet<>();

in your parent entity. Since you are using a Set, Java has to calculate the uniqueness of the elements inside. This is usually done by calling the equals() method inside the child Object. This however causes the (child) Entity with all its childs to be loaded (depending of the "equals" implementation). An easy fix would be to use a List instead of a Set (if possible).

Your equals() and hashCode() functions are still using the Child entiy (location for example). Depending on the call of your parent entity the equals() or hashCode() functions are calld and the Chiulds are loaded.

Using a debugger can "manuipulate" the outcome of your sql fetches. When a breakpoint inside the debugger is called IDEs usually call the toString methods to display the Objects inside the debug window. This can also trigger the childs to be loaded since they are required in the toString implementation.

CodePudding user response:

You have a bi-direction One-To-One relation. By default, Hibernate ignores the fetch strategy of the parent side of a bidirectional One-To-One, but it properly applies it to other associations (Many-To-One, One-To-Many, Many-To-Many, unidirectional One-To-One and ElementCollection).
And, unless you are using bytecode enhancement, you should avoid the bidirectional association.


Solution 1: Use Many-To-One relation instead

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "account_id")
  @ToString.Exclude
  private Account account;

Solution 2: Bytecode Enhancement
Use the bytecode enhancement plugin that enhances the bytecode of entity classes and allows us to utilize No-proxy lazy fetching strategy

<build>
<plugins>
    <plugin>
        <groupId>org.hibernate.orm.tooling</groupId
        <artifactId>hibernate-enhance-maven-plugin</artifactId>
        <version>${hibernate.version}</version>
        <executions>
        <execution>
        <configuration>
           <enableLazyInitialization>true</enableLazyInitialization>         
        </configuration>
        <goals>
            <goal>enhance</goal>
        </goals>
        </execution>
        </executions>
    </plugin>
</plugins>
</build>

Add @LazyToOne annotation in entity classes to let hibernate know that we want to enable no proxy lazy fetching for associated entities.

  @OneToOne(
      fetch = FetchType.LAZY,
      cascade = CascadeType.ALL,
      mappedBy = "account"
  )
  @ToString.Exclude
  @LazyToOne(LazyToOneOption.NO_PROXY)
  private Location location;

Add to config:

hibernate.ejb.use_class_enhancer=true

Before Hibernate 5.5, you must add @LazyToOne(LazyToOneOption.NO_PROXY) to the @OneToOne(fetch=FetchType.LAZY, mappedBy="x").

Starting from Hibernate 5.5, it’s not required anymore, just enable Bytecode Enhancement.
Please note before Hibenrate 5.5 enabling bytecode enhancement can lead to side affects:
HHH-13134 – JOIN FETCH does not work properly with enhanced entities
HHH-14450 – Drop ability to disable “enhanced proxies”

Solution 3: Make relation mandatory
This solution just for information. You should not change the entity restrictions just for lazy loading

@OneToOne(optional = false, fetch = FetchType.LAZY)

Details: Hibernate: one-to-one lazy loading, optional = false
Please note, the optional trick does not work on every version of Hibernate, so it might break if you upgrade. Also it makes additional Not-Null restriction to the relation.

  • Related