Issue
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);
}
}
Solution
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.
What is the difference between Unidirectional and Bidirectional JPA and Hibernate associations?
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.
Answered By - Eugene
Answer Checked By - Terry (JavaFixing Volunteer)