<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Loïc Chau</title>
    <description>The latest articles on DEV Community by Loïc Chau (@loiklak).</description>
    <link>https://dev.to/loiklak</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F834677%2F818ea06b-ad74-4e32-a546-bd4746d2d30c.gif</url>
      <title>DEV Community: Loïc Chau</title>
      <link>https://dev.to/loiklak</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/loiklak"/>
    <language>en</language>
    <item>
      <title>A journey to optimize a SpringData SQL query with N+1s</title>
      <dc:creator>Loïc Chau</dc:creator>
      <pubDate>Wed, 20 Nov 2024 21:24:19 +0000</pubDate>
      <link>https://dev.to/loiklak/a-journey-to-optimize-a-springdata-sql-query-with-n1s-2f87</link>
      <guid>https://dev.to/loiklak/a-journey-to-optimize-a-springdata-sql-query-with-n1s-2f87</guid>
      <description>&lt;h2&gt;
  
  
  Some context
&lt;/h2&gt;

&lt;p&gt;While browsing my app in pre-production, I noticed that the homepage was starting to take a long time to load...&lt;/p&gt;

&lt;h2&gt;
  
  
  Investigating in my APM
&lt;/h2&gt;

&lt;p&gt;I looked in Kibana's APM (Application Performance Management) and...&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuzkdkqohyafrdlmsvzb2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuzkdkqohyafrdlmsvzb2.png" alt="Two API endpoints average response time" width="800" height="178"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A route with 13s and another with 6.5s average latency?????? 🤯&lt;br&gt;
That's much more than I'd want...&lt;/p&gt;
&lt;h2&gt;
  
  
  The N+1 problem
&lt;/h2&gt;

&lt;p&gt;It quickly becomes clear that we have a pretty severe N+1 problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6lxbxrtu12zth8saj563.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6lxbxrtu12zth8saj563.png" alt="Endpoint response breakdown including 849 SELECT clauses" width="800" height="140"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To help you better picture the DB schema :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1o7qxjj1361qvbvc5kiw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1o7qxjj1361qvbvc5kiw.png" alt="A project table in a 1-1 relation with the Relation_1 table and in the 1-n relation with the Relation_2 table" width="800" height="692"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In addition to the expected &lt;code&gt;SELECT&lt;/code&gt; query to retrieve a project line in DB, we execute 849 additional &lt;code&gt;SELECT&lt;/code&gt; queries on Relation_1, which takes 3.6s out of the 4.2s total duration of the HTTP call 🫠&lt;/p&gt;

&lt;p&gt;The worst part : I don't even need the Relation_1 information in this endpoint 🥲 so I'm just wasting ressources&lt;/p&gt;

&lt;p&gt;We can't see it on the capture but there are another 200s &lt;code&gt;SELECT&lt;/code&gt; to get data from Relation_2, so we have a double N+1 queries issue.&lt;/p&gt;
&lt;h2&gt;
  
  
  The &lt;code&gt;EAGER&lt;/code&gt; toxic relation
&lt;/h2&gt;

&lt;p&gt;The culprit in the code was the &lt;code&gt;FetchType.EAGER&lt;/code&gt; on the Relation1 and Relation2 relationships (or so I thought at this point).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Misconception:&lt;/strong&gt; A FetchType.EAGER helps avoid N+1 issues because it fetches everything at once, so JPA must be optimizing and performing JOINs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reframe:&lt;/strong&gt; FetchType.EAGER ensures that database reads are transactional but not optimized. In fact, using FetchType.EAGER guarantees that you’re always in the worst-case scenario for N+1 issues 😱 → which was exactly my case.&lt;/p&gt;

&lt;p&gt;After looking around in Baeldung I discovered Hibernate's &lt;code&gt;FetchMode&lt;/code&gt; :&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In general, &lt;em&gt;FetchMode&lt;/em&gt; defines how &lt;em&gt;Hibernate&lt;/em&gt; will fetch the data (by select, join or subselect). &lt;em&gt;FetchType&lt;/em&gt;, on the other hand, defines whether Hibernate will load data eagerly or lazily.&lt;/p&gt;

&lt;p&gt;The exact rules between these two are as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if the code doesn’t set &lt;em&gt;FetchMode&lt;/em&gt;, the default one is &lt;em&gt;JOIN&lt;/em&gt; and &lt;em&gt;FetchType&lt;/em&gt; works as defined&lt;/li&gt;
&lt;li&gt;with &lt;em&gt;FetchMode.SELECT&lt;/em&gt; or &lt;em&gt;FetchMode.SUBSELECT&lt;/em&gt; set, &lt;em&gt;FetchType&lt;/em&gt; also works as defined&lt;/li&gt;
&lt;li&gt;with &lt;em&gt;FetchMode.JOIN&lt;/em&gt; set, &lt;em&gt;FetchType&lt;/em&gt; is ignored and a query is always eager&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;It seemed to me like Hibernate uses optimized parameters since it uses JOINs by default. I also thought that it would use FetchType.LAZY by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;False assumption (but I didn’t know it at the time):&lt;/strong&gt; Hibernate is well-designed, and by default, all joins have the best default settings: FetchMode.JOIN and FetchType.LAZY.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reality:&lt;/strong&gt; This is actually false (as we’ll see later).&lt;/p&gt;
&lt;h2&gt;
  
  
  ❌ Removing the &lt;code&gt;FetchType.EAGER&lt;/code&gt; and, if needed, replace it with &lt;code&gt;FetchMode.JOIN&lt;/code&gt; (result: -0s).
&lt;/h2&gt;

&lt;p&gt;I kept some &lt;code&gt;FetchMode.JOIN&lt;/code&gt; annotations to optimize queries in cases where we’re certain to always fetch the tables together. (Reality: otherwise, my tests broke 🤪).&lt;/p&gt;

&lt;p&gt;Under the hood, &lt;code&gt;FetchMode.JOIN&lt;/code&gt; forces &lt;code&gt;FetchType.EAGER&lt;/code&gt;, but it optimizes the query by performing a join as expected, so in theory we should have no more N+1 queries.&lt;/p&gt;

&lt;p&gt;However, in practice, &lt;code&gt;FetchMode.JOIN&lt;/code&gt; is essentially useless in my app... It’s ignored because we fetch data with SpringData 😩 I guess it is therefore essentially the same as having a &lt;code&gt;FetchType.EAGER&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Spring data internally use Criteria API for the findAll() query , so @Fetch(value = FetchMode.JOIN) will not have any effect.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://stackoverflow.com/a/53939326" rel="noopener noreferrer"&gt;StackOverflow thread for more info&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;Even though &lt;code&gt;FetchMode.JOIN&lt;/code&gt; doesn't work, at least the relations where I removed the &lt;code&gt;fetchType=EAGER&lt;/code&gt; should now be &lt;code&gt;LAZY&lt;/code&gt; and my N+1 problem should improve... Right ??? But nothing has changed 😶&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzq9dqszn2o4rgtlk295i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzq9dqszn2o4rgtlk295i.png" alt="Anakin/Padme meme about FetchMode.JOIN not resolving my N+1 issue" width="500" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why ????
&lt;/h3&gt;

&lt;p&gt;Actually, the &lt;code&gt;@OneToOne&lt;/code&gt; relationship defaults to &lt;code&gt;FetchType.EAGER&lt;/code&gt; 💀 as you can see in the source code of the annotation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdjoro7psfy3kajirx612.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdjoro7psfy3kajirx612.png" alt="FetchType.EAGER annotation source code" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So, we should explicitly set &lt;code&gt;FetchType.LAZY&lt;/code&gt; on the relationship…&lt;/p&gt;

&lt;p&gt;But I think a much more interesting question is: why, in the name of Jesus of Nazareth, doesn’t Spring Data generate a join for us??? We’ll still try both approaches to see what happens.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting &lt;code&gt;FetchType.LAZY&lt;/code&gt; on the @OneToOne ❌
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vydg4wztqxygh5hvxzi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0vydg4wztqxygh5hvxzi.png" alt="IntelliJ warning that non owning side of a one-to-one relationship will always be  raw `FetchType.EAGER` endraw  no matter what" width="800" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem occurs when fetching projects, so I tried setting &lt;code&gt;FetchType.LAZY&lt;/code&gt; on &lt;code&gt;ProjectModel&lt;/code&gt; Relation_1 relationship. But… &lt;code&gt;Project&lt;/code&gt; isn’t the owner of the relationship 💀 which means the fetch is still EAGER!!! AAAAAAAAAAAH 🗿 (thanks IntelliJ for pointing a out this issue with a warning by the way)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Unless you are using Bytecode Enhancement, you cannot fetch lazily the parent-side @OneToOne association.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/1444227/how-can-i-make-a-jpa-onetoone-relation-lazy/57941522#57941522" rel="noopener noreferrer"&gt;StackOverflow thread - How can I make a JPA OneToOne relation lazy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I hope I won't need to make Bytecode enhancement because it seems a bit overkill for my use case 🙃&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;💡 Learning moment:&lt;/strong&gt; Defining a @OneToOne relationship on both sides when it’s not necessary is a bad idea because the non-owning side will always default to FetchType.EAGER, leading to a ton of N+1 issues 💀&lt;/p&gt;



&lt;p&gt;Gladly, in my case, a solution to the N+1 problem is to remove the &lt;code&gt;@OneToOne&lt;/code&gt; relationship annotation on &lt;code&gt;Relation_1&lt;/code&gt; in the &lt;code&gt;Project&lt;/code&gt; model since we never query the project alongside &lt;code&gt;Relation_1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Or, alternatively, I could ensure the relation is properly fetched with a JOIN query — though this is less optimal because it will unnecessarily load the memory with unneeded &lt;code&gt;Relation_1&lt;/code&gt; information when fetching projects.&lt;/p&gt;
&lt;h2&gt;
  
  
  Kindly ask Spring Data to perform JOINs 🤗 (😠)
&lt;/h2&gt;

&lt;p&gt;As we saw earlier, &lt;code&gt;FetchMode.JOIN&lt;/code&gt; is useless with Spring Data because it internally uses the Criteria API, which doesn’t consider &lt;code&gt;FetchMode&lt;/code&gt; (thank you Criteria API).&lt;/p&gt;

&lt;p&gt;The solution is to use &lt;a href="https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html#jpa.entity-graph" rel="noopener noreferrer"&gt;Entity Graphs&lt;/a&gt; 🥹🥹 the savior.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/6xUnSVTh8fI"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Entity Graph allows us to specify to Spring Data which joins it should make.&lt;/p&gt;

&lt;p&gt;Example :&lt;/p&gt;

&lt;p&gt;On your model :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Entity
// Define an EntityGraph named "Project.forDomain"
@NamedEntityGraph(name = "ProjectModel.forDomain",
        attributeNodes = {
          // This entity graph specifies to eagerly fetch (with JOINs)
          // the following property of the model
          @NamedAttributeNode("relation2")
        })
public class ProjectModel {

    @OneToOne(mappedBy = "project", fetch = FetchType.LAZY)
    public Relation2 relation2;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the SpringData repository&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Repository
public interface ProjectJpaRepository {
    // This method will use the entity graph "ProjectModel.forDomain"
    // to know how to fetch the SQL data
    @EntityGraph(value="ProjectModel.forDomain", type = EntityGraph.EntityGraphType.FETCH)
    List&amp;lt;ProjectModel&amp;gt; findBySomeProperty(String someProperty);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Little bit of thinking 🤔&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Should we use &lt;code&gt;type = EntityGraphType.LOAD&lt;/code&gt; or &lt;code&gt;type = EntityGraphType.FETCH&lt;/code&gt;?&lt;br&gt;
&lt;code&gt;LOAD&lt;/code&gt; = the properties of the graph are EAGER JOIN, while others follow their default fetch specs.&lt;br&gt;
&lt;code&gt;FETCH&lt;/code&gt; = the properties of the graph are EAGER JOIN, while others are LAZY loaded.&lt;/p&gt;

&lt;p&gt;I’ll prefer &lt;code&gt;FETCH&lt;/code&gt; for performance by default and specify EAGER JOINs if I need data to be fetched together.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/31978011/what-is-the-difference-between-fetch-and-load-for-entity-graph-of-jpa/31978349#31978349" rel="noopener noreferrer"&gt;StackOverflow thread - What is the difference between FETCH and LOAD for Entity graph of JPA?&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Well, after all these twists and turns... the entity graph works! 🥹🥹🥹🥹🥹🥹🥹🥹🥹🥹&lt;/p&gt;

&lt;h2&gt;
  
  
  You didn’t think it was over, did you? 😊
&lt;/h2&gt;

&lt;p&gt;Removing the &lt;code&gt;relation1&lt;/code&gt; property from &lt;code&gt;projectModel&lt;/code&gt; means we no longer benefit from &lt;code&gt;orphanRemoval&lt;/code&gt; 🥲 so some of my tests broke.&lt;/p&gt;

&lt;p&gt;In the end, it was pretty quick to fix though.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Removing the reference to &lt;code&gt;relation1&lt;/code&gt; in &lt;code&gt;ProjectModel&lt;/code&gt; helped eliminate the N+1 issues on one of my routes, N+1 which were solely caused by the EAGER &lt;code&gt;@OneToOne&lt;/code&gt; on the non-owning side.&lt;/p&gt;

&lt;p&gt;And the entity graph helped eliminate the N+1 issues on the other route 😍, which was plagued by the relationship with &lt;code&gt;relation2&lt;/code&gt; which wasn't done with a JOIN.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;💡 Thinking back on how to debug SQL performance issues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To iterate: write SQL tests!!!!! It helps you iterate so much faster than by manually testing your app 😱&lt;br&gt;
There is an initial cost to setup a test reproducing your issue but I swear it is worth it.&lt;/p&gt;

&lt;p&gt;By enabling &lt;code&gt;show-sql: true&lt;/code&gt; in the &lt;code&gt;application.yml&lt;/code&gt; for the test environment, you can see the SQL queries that are being executed.&lt;/p&gt;




&lt;p&gt;I hope you learned something from my optimization journey and had some fun as well 😄&lt;br&gt;
If you have any questions ask in the comments I'll respond to the best of my capacities 😉&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>java</category>
      <category>springboot</category>
    </item>
  </channel>
</rss>
