MS Lesson12: LazyInitializationException, n + 1 problem
1. LazyInitializationException
Hibernate session/persistence context artiq baglidirsa Hibernate child-i yukleye bilmir ve LazyInitializationException atir.
package guru.springframework.cruddemo.entity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
@Entity
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "persons")
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false, unique = true)
private String email;
// @OneToOne(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
// @OneToOne(mappedBy = "person", orphanRemoval = true)
// private Address address;
@Builder.Default
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
private List<Car> cars = new ArrayList<>();
}
package guru.springframework.cruddemo.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@Entity
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "addresses")
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String country;
@Column(nullable = false)
private String city;
// @ToString.Exclude
// @OneToOne(optional = false)
// @JoinColumn(name = "person_id", nullable = false, unique = true)
// private Person person;
}
package guru.springframework.cruddemo.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@Entity
@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "cars")
public class Car {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String brand;
@Column(nullable = false)
private String model;
@ToString.Exclude
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "person_id", nullable = false)
private Person person;
}
package guru.springframework.cruddemo;
import guru.springframework.cruddemo.entity.Person;
import guru.springframework.cruddemo.repository.PersonRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.List;
@SpringBootApplication
@RequiredArgsConstructor
public class CrudDemoApplication implements CommandLineRunner {
private final PersonRepository repository;
public static void main(String[] args) {
SpringApplication.run(CrudDemoApplication.class, args);
}
@Override
public void run(String... args) {
List<Person> all = repository.findAll();
}
}
LazyInitializationException-u aradan qaldirmaq ucun ilk variant fetch type EAGER etmekdir. Lakin bu yaxshi hell sayilmir. Cunki bize child entity(ler) lazim olmaya biler ve boshuna cagirmish olacayiq.
Ikinci yox ise @Transactional ishletmekdir, lakin bu da oz novbesinde yaxshi hell sayilmir. Lazy problemini gizletmek ucun bu hellin istifadesi yolverilmezdir, hemcinin tranzaksiya boyunca connection obyektini ishlek saxlamaq duzgun sayilmir.
Ucuncu yox - native query yazmaqdir, lakin bu dolayi hell demekdir. Bu zaman bize projection veya dto lazim olur:
package guru.springframework.cruddemo.projection;
import java.math.BigDecimal;
import java.time.Instant;
public interface PersonPassportRow {
Long getPersonId();
String getPersonName();
String getPersonEmail();
BigDecimal getScholarship();
Instant getCreatedAt();
Long getPassportId();
String getPassportPin();
}
package guru.springframework.cruddemo.dto;
import java.math.BigDecimal;
import java.time.Instant;
public record PersonPassportDto(
Long personId,
String personName,
String personEmail,
BigDecimal scholarship,
Instant createdAt,
Long passportId,
String passportPin
) {
}
package guru.springframework.cruddemo.repository;
import guru.springframework.cruddemo.entity.Person;
import guru.springframework.cruddemo.projection.PersonPassportRow;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
import java.util.Optional;
public interface PersonRepository extends JpaRepository<Person, Long> {
@Query(value = """
select p.id as personId,
p.name as personName,
p.email as personEmail,
p.scholarship as scholarship,
p.created_at as createdAt,
pp.id as passportId,
pp.pin as passportPin
from person p
left join passport pp on pp.person_id = p.id
where p.id = :personId
""", nativeQuery = true)
Optional<PersonPassportRow> findPersonWithPassportNative(Long personId);
}
package guru.springframework.cruddemo.service.impl;
import guru.springframework.cruddemo.dto.PersonPassportDto;
import guru.springframework.cruddemo.entity.Parent;
import guru.springframework.cruddemo.projection.PersonPassportRow;
import guru.springframework.cruddemo.repository.ParentRepository;
import guru.springframework.cruddemo.repository.PersonRepository;
import guru.springframework.cruddemo.service.PersonService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class PersonServiceImpl implements PersonService {
private final PersonRepository repository;
private final ParentRepository parentRepository;
@Override
public Parent findById(Long id) {
return parentRepository.findById(id).orElseThrow();
}
@Override
public PersonPassportDto findPersonWithPassportNative(Long personId) {
PersonPassportRow row = repository.findPersonWithPassportNative(personId).orElseThrow();
return new PersonPassportDto(
row.getPersonId(),
row.getPersonName(),
row.getPersonEmail(),
row.getScholarship(),
row.getCreatedAt(),
row.getPassportId(),
row.getPassportPin()
);
}
}
Dorduncu yol - join fetch.
@Query("""
select p
from Person p
join fetch p.passport
where p.id = :personId
""")
Optional<Person> findByIdWithPassportJoinFetch(Long personId);
Beshinci yol EntityGraph.
package guru.springframework.cruddemo.repository;
import guru.springframework.cruddemo.entity.Parent;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface ParentRepository extends JpaRepository<Parent, Long> {
@EntityGraph(attributePaths = {"child"})
Optional<Parent> findById(Long id);
}
veya
package guru.springframework.cruddemo.repository;
import guru.springframework.cruddemo.entity.Parent;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
public interface ParentRepository extends JpaRepository<Parent, Long> {
@EntityGraph(attributePaths = {"child"})
@Query("select p from Parent p where p.id = :id")
Optional<Parent> foo(Long id);
}
Altinci yol NamedEntityGraph.
package guru.springframework.cruddemo.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.NamedAttributeNode;
import jakarta.persistence.NamedEntityGraph;
import jakarta.persistence.OneToOne;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.proxy.HibernateProxy;
import java.util.Objects;
@Getter
@Setter
@ToString
@RequiredArgsConstructor
@Entity
@NamedEntityGraph(
name = "parent-with-child",
attributeNodes = @NamedAttributeNode("child")
)
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "child_id")
private Child child;
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
Class<?> oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass();
Class<?> thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass();
if (thisEffectiveClass != oEffectiveClass) return false;
Parent parent = (Parent) o;
return getId() != null && Objects.equals(getId(), parent.getId());
}
@Override
public final int hashCode() {
return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode();
}
}
package guru.springframework.cruddemo.repository;
import guru.springframework.cruddemo.entity.Parent;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
public interface ParentRepository extends JpaRepository<Parent, Long> {
@EntityGraph(value = "parent-with-child")
@Query("select p from Parent p where p.id = :id")
Optional<Parent> foo(Long id);
}
N + 1 problemi nedir?
1 sorgu ile esas obyekti getiririk bu zaman n eded netice gelir, sonra ise child-lari getirmek ucun her bir entity ucun 1 sorgu atilir, netice etibari ile 1 + n sorgu alinir.
Mes: 1 query ile 100 Person getiririk, ve her Person ucun ayrica Passport cekirik. toplam 1 + 100 = 101 query alinir.
package guru.springframework.cruddemo;
import guru.springframework.cruddemo.entity.Person;
import guru.springframework.cruddemo.repository.PersonRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@SpringBootApplication
@RequiredArgsConstructor
public class CrudDemoApplication implements CommandLineRunner {
private final PersonRepository repository;
public static void main(String[] args) {
SpringApplication.run(CrudDemoApplication.class, args);
}
@Override
@Transactional(readOnly = true)
public void run(String... args) throws Exception {
List<Person> all = repository.findAll(); // 1 query
for (Person person : all) {
int courseCount = person.getCourses().size(); // N extra queries (one per person)
System.out.println("personId=" + person.getId() + ", courseCount=" + courseCount);
}
}
}
N + 1 problemini hell etmek ucun bir nece usul var.
1. join fetch
2. entity graph
3. named entity graph
4. dto/projection
5. batch fetch
spring:
datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:7777}/${DB_NAME:crud}
username: ${DB_USERNAME:crud}
password: ${DB_PASSWORD:crud}
hikari:
pool-name: MyPostgresHikariCP
maximum-pool-size: 100
minimum-idle: 10 # Lowered this so Hikari has "extra" connections to kill
idle-timeout: 5000 # 10 seconds (time an idle connection survives)
max-lifetime: 5000 # 30 seconds (minimum allowed by Hikari)
connection-timeout: 20000
auto-commit: true # Default is true; Spring @Transactional overrides this
jpa:
open-in-view: false
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
default_batch_fetch_size: 50
package guru.springframework.cruddemo;
import guru.springframework.cruddemo.entity.Person;
import guru.springframework.cruddemo.repository.PersonRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@SpringBootApplication
@RequiredArgsConstructor
public class CrudDemoApplication implements CommandLineRunner {
private final PersonRepository repository;
public static void main(String[] args) {
SpringApplication.run(CrudDemoApplication.class, args);
}
@Override
@Transactional(readOnly = true)
public void run(String... args) throws Exception {
System.out.println("=== findAll + lazy courses with batch fetch ===");
List<Person> all = repository.findAll(); // 1 query
for (Person person : all) {
int courseCount = person.getCourses().size();
System.out.println("personId=" + person.getId() + ", courseCount=" + courseCount);
}
}
}
Комментарии
Отправить комментарий