DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Improving Backend Performance Part 1/3: Lazy Loading in Vaadin Apps
  • An Overview of SQL Server Joins
  • MongoDB to Couchbase: An Introduction to Developers and Experts
  • Best Performance Practices for Hibernate 5 and Spring Boot 2 (Part 1)

Trending

  • How to Submit a Post to DZone
  • Enforcing Architecture With ArchUnit in Java
  • Tired of Spring Overhead? Try Dropwizard for Your Next Java Microservice
  • The Smart Way to Talk to Your Database: Why Hybrid API + NL2SQL Wins
  1. DZone
  2. Data Engineering
  3. Databases
  4. How to Decide Between JOIN and JOIN FETCH

How to Decide Between JOIN and JOIN FETCH

Deciding between JOIN and JOIN FETCH is a matter of performance!

By 
Anghel Leonard user avatar
Anghel Leonard
DZone Core CORE ·
Sep. 21, 20 · Tutorial
Likes (12)
Comment
Save
Tweet
Share
54.5K Views

Join the DZone community and get the full member experience.

Join For Free

Typically, JOIN and JOIN FETCH come into play when the application has lazy associations but some data must be fetched eagerly. Relying on FetchType.EAGER at the entities-level is a code smell.

Consider the Author and Book entities that are involved in a bidirectional-lazy @OneToMany association:

Java
 




x
15


 
1
@Entity 
2
public class Author implements Serializable { 
3
      
4
  private static final long serialVersionUID = 1L;
5
    
6
  @Id 
7
  private Long id; 
8
  private String name; 
9
  private String genre; 
10
  private int age; 
11
    
12
  @OneToMany(cascade = CascadeType.ALL, mappedBy = "author", orphanRemoval = true) 
13
  private List<Book> books = new ArrayList<>(); 
14
  ... 
15
}


Java
 




xxxxxxxxxx
1
13


 
1
@Entity 
2
public class Book implements Serializable { 
3
    
4
  private static final long serialVersionUID = 1L; 
5
    
6
  @Id private Long id; 
7
  private String title; 
8
  private String isbn; 
9
    
10
  @ManyToOne(fetch = FetchType.LAZY) 
11
  @JoinColumn(name = "author_id") 
12
  private Author author; 
13
  ... 
14
}


Consider the following sample of data:

Data sample


And, the goal is to fetch the following data as entities:

  • all Author and their Book that are more expensive than the given price
  • all the Book and their Author

Fetch All Authors and Their Books that Are More Expensive than The Given Price 

To satisfy the first query (fetch all the Author and their Book that are more expensive than the given price) write a Spring repository, AuthorRepository, and add a JOIN and a JOIN FETCH query meant to fetch the same data:

Java
 




xxxxxxxxxx
1
10


 
1
@Repository 
2
@Transactional(readOnly = true) 
3
public interface AuthorRepository extends JpaRepository<Author, Long> { 
4
   
5
  // INNER JOIN 
6
  @Query(value = "SELECT a FROM Author a INNER JOIN a.books b WHERE b.price > ?1") 
7
  List<Author> fetchAuthorsBooksByPriceInnerJoin(int price); 
8
    
9
  // JOIN FETCH 
10
  @Query(value = "SELECT a FROM Author a JOIN FETCH a.books b WHERE b.price > ?1") 
11
  List<Author> fetchAuthorsBooksByPriceJoinFetch(int price); 
12
}



Calling the above repository-methods and displaying the fetched data to the console can be done as follows:

Java
 




xxxxxxxxxx
1
10


 
1
public void fetchAuthorsBooksByPriceJoinFetch() { 
2
     
3
  List<Author> authors = authorRepository.fetchAuthorsBooksByPriceJoinFetch(40);
4
    
5
  authors.forEach((e) -> System.out.println("Author name: " 
6
      + e.getName() + ", books: " + e.getBooks())); 
7
}
8
  
9
@Transactional(readOnly = true) 
10
public void fetchAuthorsBooksByPriceInnerJoin() { 
11
  
12
  List<Author> authors = authorRepository.fetchAuthorsBooksByPriceInnerJoin(40); 
13
  
14
  authors.forEach((e) -> System.out.println("Author name: " 
15
      + e.getName() + ", books: " + e.getBooks())); 
16
}



How JOIN FETCH Will Act

JOIN FETCH is specific to JPA and it allows associations to be initialized along with their parent objects using a single SELECT. As you will see soon, this is particularly useful for fetching associated collections. This means that calling fetchAuthorsBooksByPriceJoinFetch() will trigger a single SELECT as follows:

SQL
 




xxxxxxxxxx
1
10


 
1
SELECT 
2
  author0_.id AS id1_0_0_, 
3
  books1_.id AS id1_1_1_, 
4
  author0_.age AS age2_0_0_, 
5
  author0_.genre AS genre3_0_0_, 
6
  author0_.name AS name4_0_0_, 
7
  books1_.author_id AS author_i5_1_1_, 
8
  books1_.isbn AS isbn2_1_1_, 
9
  books1_.price AS price3_1_1_, 
10
  books1_.title AS title4_1_1_, 
11
  books1_.author_id AS author_i5_1_0__, 
12
  books1_.id AS id1_1_0__ 
13
FROM author author0_ 
14
INNER JOIN book books1_ 
15
  ON author0_.id = books1_.author_id 
16
WHERE books1_.price > ?



Running this SQL against the data sample for a given price of 40 dollars will fetch the following data (display the author's names and books):

Textile
 




xxxxxxxxxx
1


 
1
Author name: Joana Nimar, 
2
       books: [Book{id=2, title=A People's History, isbn=002-JN, price=41}]



This looks correct! There is a single book in the database expensive than 40 dollars and its author is Joana Nimar.

How JOIN Will Act

On the other hand, JOIN doesn't allow associated collections to be initialized along with their parent objects using a single SELECT. This means that calling fetchAuthorsBooksByPriceInnerJoin() will result in the following SELECT (the SQL reveals that no book was loaded):

SQL
 




xxxxxxxxxx
1


 
1
SELECT 
2
  author0_.id AS id1_0_, 
3
  author0_.age AS age2_0_, 
4
  author0_.genre AS genre3_0_, 
5
  author0_.name AS name4_0_ 
6
FROM author author0_ 
7
INNER JOIN book books1_ 
8
  ON author0_.id = books1_.author_id 
9
WHERE books1_.price > ?



Running this SQL against the data sample will fetch a single author (Joana Nimar) which is correct. Attempting to display the books written by Joana Nimar via getBooks() will trigger an additional SELECT as follows:

SQL
 




xxxxxxxxxx
1
10
9
10


 
1
SELECT 
2
  books0_.author_id AS author_i5_1_0_, 
3
  books0_.id AS id1_1_0_, 
4
  books0_.id AS id1_1_1_, 
5
  books0_.author_id AS author_i5_1_1_, 
6
  books0_.isbn AS isbn2_1_1_, 
7
  books0_.price AS price3_1_1_,
8
  books0_.title AS title4_1_1_ 
9
FROM book books0_ 
10
WHERE books0_.author_id = ?



Writing a query as below doesn't help either: 

Java
 




xxxxxxxxxx
1


 
1
@Query(value = "SELECT a, b FROM Author a INNER JOIN a.books b WHERE b.price > ?1")



Display the author name and the fetched books:

Textile
 




xxxxxxxxxx
1


 
1
Author name: Joana Nimar, 
2
       books: [ 
3
               Book{id=1, title=A History of Ancient Prague, isbn=001-JN, price=36}, 
4
               Book{id=2, title=A People's History, isbn=002-JN, price=41} 
5
              ]



Two things must be highlighted here: an important drawback and potential confusion.

First, the drawback. Notice that JOIN has fetched the books in an additional SELECT. This can be considered a performance penalty in comparison with JOIN FETCH which needs a single SELECT, therefore a single database roundtrip. 

Second, the potential confusion. Pay extra attention to the interpretation of the WHERE books1_.price > ? clause in the first SELECT. While the application fetches only the authors that have written books more expensive than 40 dollars, when calling getBooks(), the application fetches all books of these authors not only the books more expensive than 40 dollars. This is normal since, when getBooks() is called, the WHERE clause is not there anymore. Therefore, in this case, JOIN produced a different result than JOIN FETCH.

Fetch All Book and Their Author

To satisfy the second query (all the Book and their Author) write a Spring repository, BookRepository, and add two JOINs and a JOIN FETCH query:

Java
 




xxxxxxxxxx
1
10


 
1
@Repository 
2
@Transactional(readOnly = true) 
3
public interface BookRepository extends JpaRepository<Book, Long> {
4
     
5
  // INNER JOIN BAD 
6
  @Query(value = "SELECT b FROM Book b INNER JOIN b.author a") 
7
  List<Book> fetchBooksAuthorsInnerJoinBad(); 
8
    
9
  // INNER JOIN GOOD 
10
  @Query(value = "SELECT b, a FROM Book b INNER JOIN b.author a") 
11
  List<Book> fetchBooksAuthorsInnerJoinGood(); 
12
    
13
  // JOIN FETCH 
14
  @Query(value = "SELECT b FROM Book b JOIN FETCH b.author a") 
15
  List<Book> fetchBooksAuthorsJoinFetch(); 
16
}



Calling the above methods and displaying the fetched data to the console can be done as follows:

Java
 




xxxxxxxxxx
1
10


 
1
public void fetchBooksAuthorsJoinFetch() { 
2
     
3
  List<Book> books = bookRepository.fetchBooksAuthorsJoinFetch(); 
4
   
5
  books.forEach((e) -> System.out.println("Book title: " + e.getTitle() 
6
     + ", Isbn:" + e.getIsbn() + ", author: " + e.getAuthor())); 
7
}
8

          
9
@Transactional(readOnly = true) 
10
public void fetchBooksAuthorsInnerJoinBad/Good() { 
11
   
12
  List<Book> books = bookRepository.fetchBooksAuthorsInnerJoinBad/Good(); 
13
   
14
  books.forEach((e) -> System.out.println("Book title: " + e.getTitle() 
15
     + ", Isbn: " + e.getIsbn() + ", author: " + e.getAuthor())); 
16
}



How JOIN FETCH Will Act

Calling fetchBooksAuthorsJoinFetch() will trigger a single SQL triggered as follows (all authors and books are fetched in a single SELECT):

SQL
 




xxxxxxxxxx
1
10


 
1
SELECT 
2
  book0_.id AS id1_1_0_, 
3
  author1_.id AS id1_0_1_, 
4
  book0_.author_id AS author_i5_1_0_, 
5
  book0_.isbn AS isbn2_1_0_, 
6
  book0_.price AS price3_1_0_, 
7
  book0_.title AS title4_1_0_, 
8
  author1_.age AS age2_0_1_, 
9
  author1_.genre AS genre3_0_1_, 
10
  author1_.name AS name4_0_1_ 
11
FROM book book0_ 
12
INNER JOIN author author1_ 
13
  ON book0_.author_id = author1_.id



Running this SQL against the data sample will output (display only the book title, ISBN, and author):

Textile
 




xxxxxxxxxx
1
10


 
1
Book title: A History of Ancient Prague, Isbn:001-JN, 
2
     author: Author{id=4, name=Joana Nimar, genre=History, age=34} 
3
     
4
Book title: A People's History, Isbn:002-JN, 
5
     author: Author{id=4, name=Joana Nimar, genre=History, age=34} 
6
     
7
Book title: The Beatles Anthology, Isbn:001-MJ, 
8
     author: Author{id=1, name=Mark Janel, genre=Anthology, age=23} 
9
     
10
Book title: Carrie, Isbn:001-OG, 
11
     author: Author{id=2, name=Olivia Goy, genre=Horror, age=43}



Everything looks as expected! There are four books and each of them has an author.

How JOIN Will Act

On the other hand, calling fetchBooksAuthorsInnerJoinBad() will trigger a single SQL as follows (the SQL reveals that no author was loaded):

SQL
 




xxxxxxxxxx
1


 
1
SELECT 
2
  book0_.id AS id1_1_, 
3
  book0_.author_id AS author_i5_1_, 
4
  book0_.isbn AS isbn2_1_, 
5
  book0_.price AS price3_1_, 
6
  book0_.title AS title4_1_ 
7
FROM book book0_ 
8
INNER JOIN author author1_ 
9
  ON book0_.author_id = author1_.id



The returned List<Book> contains four Book. Looping this list and fetching the author of each book via getAuthor() will trigger three additional SELECT statements (there are three SELECT statements instead of four because two of the books have the same author, therefore, for the second of these two books, the author will be fetched from the Persistence Context). So, the below SELECT is triggered three times with different id value:

SQL
 




xxxxxxxxxx
1


 
1
SELECT 
2
  author0_.id AS id1_0_0_, 
3
  author0_.age AS age2_0_0_, 
4
  author0_.genre AS genre3_0_0_, 
5
  author0_.name AS name4_0_0_ 
6
FROM author author0_ 
7
WHERE author0_.id = ?



Displaying the title, ISBN, and author of each book will output:

Textile
 




xxxxxxxxxx
1
10


 
1
Book title: A History of Ancient Prague, Isbn: 001-JN, 
2
     author: Author{id=4, name=Joana Nimar, genre=History, age=34} 
3
     
4
Book title: A People's History, Isbn: 002-JN, 
5
     author: Author{id=4, name=Joana Nimar, genre=History, age=34} 
6
     
7
Book title: The Beatles Anthology, Isbn: 001-MJ, 
8
     author: Author{id=1, name=Mark Janel, genre=Anthology, age=23} 
9
     
10
Book title: Carrie, Isbn: 001-OG, 
11
     author: Author{id=2, name=Olivia Goy, genre=Horror, age=43}



In this case, the performance penalty is obvious. While JOIN FETCH needs a single SELECT, JOIN needs four SELECT statements.

How about calling fetchBooksAuthorsInnerJoinGood()? Well, this will produce the exact same query and result as JOIN FETCH. This is working because the fetched association is not a collection. So, in this case, you can use JOIN or JOIN FETCH.

As a rule of thumb, use JOIN FETCH (not JOIN) whenever the data should be fetched as entities (because the application plans to modify them) and Hibernate should include the associations in the SELECT clause. This is particularly useful for fetching associated collections. In such scenarios, using JOIN is prone to N+1 performance penalties. On the other hand, whenever fetching read-only data (don't plan to modify it), better rely on JOIN + DTO instead of JOIN FETCH.

Pay attention that while a query as SELECT a FROM Author a JOIN FETCH a.books is correct, the following attempts will not work:

SELECT a.age as age FROM Author a JOIN FETCH a.books 

Causes: org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list 

SELECT a FROM Author a JOIN FETCH a.books.title 

Causes: org.hibernate.QueryException: illegal attempt to dereference collection [author0_.id.books] with element property reference [title]

The source code is available on GitHub.

If you liked this article, then you'll my book containing 150+ performance items - Spring Boot Persistence Best Practices.  

This book helps every Spring Boot developer to squeeze the performances of the persistence layer.  

Fetch (FTP client) Joins (concurrency library) Database Book Spring Framework sql Data (computing)

Opinions expressed by DZone contributors are their own.

Related

  • Improving Backend Performance Part 1/3: Lazy Loading in Vaadin Apps
  • An Overview of SQL Server Joins
  • MongoDB to Couchbase: An Introduction to Developers and Experts
  • Best Performance Practices for Hibernate 5 and Spring Boot 2 (Part 1)

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!