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

}





























Комментарии

Популярные сообщения из этого блога

Interview questions

Lesson1: JDK, JVM, JRE

Lesson_2: Operations in Java