When it comes to data fetching we have two options Eager and lazy:
Eager means retrieving everything at once
Lazy means retrieving on demand
Choosing to load an entity using eager or lazy highly depends on your use case as it affects the performance of your app as Eager will retrieve everything which can slow the app. The general rule for this is to use lazy by default as it means to load only what you need but in some use cases, you will may need to use eager. In this topic, we will explore the two options. Let's get started.
Project setup
Start by creating a new project, adding the following dependencies:
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>Gradle - Groovy
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
//...
}Gradle - Kotlin
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.h2database:h2")
//...
}You also need to add the following to your application.properties to display SQL:
spring.jpa.show-sql=trueFetchType
By default, many-to-one relationships have FetchType.EAGER.
@Entity
public class Topic {
@Id
@GeneratedValue
private Long id;
private String title;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "course_id")
private Course course;
// Constructors, Getter and setter omitted
}Conversely, many-to-many relationships default to FetchType.LAZY
@Entity
public class Course {
@Id
@GeneratedValue
private Long id;
private String title;
@OneToMany(mappedBy = "course", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<Topic> topic = new HashSet<>();
// Constructors, Getter and setter omitted
}Next, create repositories for both. You can use any implementation that suits your needs:
public interface TopicRepository extends CrudRepository<Topic, Long> {
}public interface CourseRepository extends CrudRepository<Course, Long> {
}Now, here's the main app code:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
private final CourseRepository courseRepository;
private final TopicRepository topicRepository;
// constructor injection
public Application(CourseRepository courseRepository, TopicRepository topicRepository) {
this.courseRepository = courseRepository;
this.topicRepository = topicRepository;
}
@PostConstruct
public void init() {
Course course = new Course("Java Backend basics");
Topic topic1 = new Topic("Intro");
Topic topic2 = new Topic("Advanced");
topic1.setCourse(course);
topic2.setCourse(course);
course.setTopic(Set.of(topic1, topic2));
courseRepository.save(course);
topicRepository.saveAll(List.of(topic1, topic2));
}
@PersistenceContext
EntityManager entityManager;
@Component
class Runner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// Write any logs here.
}
}
}Though the code might seem extensive, it's straightforward. First, we inject the two repositories. Then, we declare an init() method, annotated with @PostConstruct to fill the database with data when the application starts. The EntityManager is a key component in JPA. It manages the lifecycle of entities and their interaction with the database. It also operates within a persistence context, tracking managed entities within a transaction. We'll use it to illustrate some essential concepts.
In the sections that follow, we will share the code inside the run method to simplify things.
How does lazy loading work?
A proxy is a runtime-generated subclass instance of an entity, carrying the identifiervalue of the entity instance it represents. To implement lazy loading, Hibernate uses this proxy as a placeholder for the entity and smart wrappers for collections. When you retrieve an entity with a lazy-loaded association (like a collection of related entities), Hibernate doesn't immediately load those associated entities. Instead, it creates a lightweight "proxy" object filling in for the associated entities. This proxy object knows how to retrieve the associated entities but doesn't do so until they are accessed. The advantage is that you can load the main entity quickly without the cost of loading all of its associated entities. If you never access the related entities, they never load, saving time and resources.
Let's test this concept:
public void run(ApplicationArguments args) {
Course courseReference = entityManager.getReference(Course.class, 1L);
}No SQL will be executed for this, as it's just a proxy. The courseReference will only be initialized when fields getters or other methods, excluding id getter, access it. Here's an example:
@Transactional
public void run(ApplicationArguments args) {
Course courseReference = entityManager.getReference(Course.class, 1L);
System.out.println(Hibernate.isInitialized(courseReference));
// This also does not initialize the object.
courseReference.getId();
System.out.println(Hibernate.isInitialized(courseReference));
// This will initialize the object.
courseReference.getTitle();
System.out.println(Hibernate.isInitialized(courseReference));
}We used @Transactional to avoid an exception, which we will learn about in the next section. Here's the output:
false
false
Hibernate: select c1_0.id,c1_0.title from course c1_0 where c1_0.id=?
trueThis tells us that when we access the proxy object using field getters, Hibernate will initialize it.
Also, note that the proxy object is not the same as the real object. It's a lightweight version. Here's an example:
Course reference = entityManager.getReference(Course.class, 1L);
System.out.println(Objects.equals(reference, Course.class)); // false
System.out.println(reference.getClass().getSimpleName()); // Course$HibernateProxy$1a2b3c4d
System.out.println(Hibernate.unproxy(reference).getClass().getSimpleName()); // CourseNow that we understand what a proxy object is, let's return to our main example and run the following code:
public void run(ApplicationArguments args) {
courseRepository.findAll().forEach(course -> {
System.out.println(course.getTitle());
});
}The output:
Hibernate: select c1_0.id,c1_0.title from course c1_0
Java Backend basicsThis code works fine! The problem arises when we try to access the proxy object:
public void run(ApplicationArguments args) {
courseRepository.findAll().forEach(course -> {
System.out.println(course.getTopic().stream().toList().get(0).getTitle());
});
}BOOM! Exception! We got a LazyInitializationException!
LazyInitializationException
A LazyInitializationException occurs when you attempt to access a lazily-loaded property or collection outside of the transactional context in which it was originally loaded.
One way to fix this is to switch to eager initialization:
@OneToMany(mappedBy = "course", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Set<Topic> topic = new HashSet<>();Now the code works. Here's the output:
Hibernate: select c1_0.id,c1_0.title from course c1_0
Hibernate: select t1_0.course_id,t1_0.id,t1_0.title from topic t1_0 where t1_0.course_id=?
IntroAs you can see, when we try to access the topic, an SQL select query is executed.
A different solution is to keep the lazy initialization and add @Transactional annotation:
@Transactional
public void run(ApplicationArguments args) {
courseRepository.findAll().forEach(course -> {
System.out.println(course.getTopic().stream().toList().get(0).getTitle());
});
}Using @Transactional ensures that the courseRepository.findAll() method executes within a transaction context. That means all operations performed within the method, including accessing associated entities (topics), are part of the same transaction.
@Transactional advantages:
Lightweight declarative syntax improves code readability.
Automatic rollback in the case of exceptions.
@Transactional disadvantages:
Potential for overuse. Applying
@Transactionalto methods that don't need transactions can introduce unnecessary complexity and potentially affect performance.Increased complexity as you may not comprehend how your transaction is being managed.
Another solution is to use FETCH JOIN in JPQL. Let's see how.
Fetch Join in JPQL
A FETCH JOIN in JPQL (Java Persistence Query Language) is used to eagerly load associated entities or collections along with the main entity being queried. This differs from lazy loading, where associated entities or collections are loaded when first accessed.
Here's an example:
@Repository
public interface CourseRepository extends JpaRepository<Course, Long> {
@Query("SELECT DISTINCT c FROM Course c LEFT JOIN FETCH c.topic WHERE c.id = :courseId")
Optional<Course> findByIdWithTopics(@Param("courseId") Long courseId);
}In this example, we use a JPQL query to select a Course along with its associated Topic entities. The LEFT JOIN FETCH clause is used to perform the fetch join operation, which eagerly loads the associated topics with the course.
Let's use it:
@Override
public void run(String... args) {
courseRepository.findByIdWithTopics(1L)
.ifPresentOrElse(
course -> {
System.out.println("Course Title: " + course.getTitle());
System.out.println("Topics:");
course.getTopic().forEach(topic -> System.out.println(" - " + topic.getTitle()));
},
() -> System.out.println("Course not found")
);
}Here's the output:
Hibernate: select distinct c1_0.id,c1_0.title,t1_0.course_id,t1_0.id,t1_0.title from course c1_0 left join topic t1_0 on c1_0.id=t1_0.course_id where c1_0.id=?
Course Title: Java Backend basics
Topics:
- Advanced
- IntroWe kept the lazy fetch and then used FETCH JOIN JPQL to solve the exception problem. This method has more benefits than just eager initialization:
Fetch join allows you to fetch the associated entities in a single query, avoiding the N+1 query problem.
Fetch join enables optimized database queries. Eager loading might retrieve more data than needed, leading to potential performance issues. With fetch join, you can specify exactly which related entities or collections to retrieve, making your queries more efficient.
In complex object graphs, using eager loading may lead to circular references, causing serialization issues. Fetch join allows you to control which associations to load, helping to avoid circular references.
Eager or lazy?
Simplified: If you know you'll access each item in a collection, loading one at a time is inefficient, so it's better to use eager fetch in the case of to-one relationships. However, if you have, for example, a course with a million students, loading all the students will certainly affect performance, and your better option would be to use a lazy fetch.
Conclusion
Today we learned that eager fetch means to retrieve everything at once and lazy fetch means to load on demand. We saw how lazy loading works, using runtime-generated instances called proxies that carry the ID of the entity instance. We also discussed a common exception, the LazyInitializationException, which occurs when we try to access a lazily loaded association or collection that hasn't been initialized yet. We learned how to avoid this exception by using @Transactional and Fetch Join. Finally, we learned which data fetching type to use according to the context. Now, let's get some practice!