Challenge

Six repository methods, four are N+1

25 minintermediate115 yrs

Edge cases. You have to reason, and two valid fixes differ.

What this teaches

  • Reading a repository method is not enough; the mapping decides the query count
  • Two of the six are correct and one of those is still the wrong shape for its screen
  • Fewer queries is not the goal — rows on the wire matter as much
  • Pagination rules out the fix that works everywhere else

Starter

Starter.java
import java.util.*;

/**
 * Challenge: six repository methods from one service. Four cause N+1 queries.
 *
 * Two do not, and one of those two is still the wrong shape for the screen it
 * serves. Work out the query count for each as a formula in N before you run
 * anything.
 */
public class Starter {

    // ── 1 ─────────────────────────────────────────────────────────────────
    //   List<Order> findAll();
    //   ...then the caller does order.getItems().size() for each.
    //   Order.items is @OneToMany(fetch = LAZY), no @BatchSize.

    // ── 2 ─────────────────────────────────────────────────────────────────
    //   @Query("select distinct o from Order o join fetch o.items where o.status = :s")
    //   List<Order> findWithItems(Status s);

    // ── 3 ─────────────────────────────────────────────────────────────────
    //   @Query("select o from Order o join fetch o.items")
    //   Page<Order> findAllWithItems(Pageable pageable);

    // ── 4 ─────────────────────────────────────────────────────────────────
    //   @EntityGraph(attributePaths = {"items"})
    //   List<Order> findByStatus(Status s);
    //   ...and the caller then reads order.getCustomer().getName() for each.

    // ── 5 ─────────────────────────────────────────────────────────────────
    //   List<Order> findByCustomerId(Long id);
    //   Order.items is @OneToMany(fetch = LAZY) @BatchSize(size = 50).
    //   The caller reads items for all of them.

    // ── 6 ─────────────────────────────────────────────────────────────────
    //   List<Order> findRecent();     // returns entities
    //   ...used by a dashboard that renders only id, reference and item count.

    public static void main(String[] args) {

        // TODO 1: for each of the six, write the query count as a formula in N
        // (the number of orders returned). Do this before running anything —
        // then model each one and check yourself.

        // TODO 2: four of them are N+1. Name them, and for each give the
        // specific fix and why the other three fixes are worse here.

        // TODO 3: method 3 issues exactly one query. Count the ROWS it reads
        // for a page of 20 out of 100,000 orders. Explain the number, and name
        // the log line that would have told you.

        // TODO 4: method 4 has an entity graph and still does N+1. Where?
        // This is the most common way a "fixed" method stays broken.

        // TODO 5: method 5 is not 1 + N and not 1. Give its formula and say
        // what setting would give every other method the same treatment
        // without touching any of them.

        // TODO 6: method 6 is not an N+1 at all and is still the wrong tool.
        // Say what it should return instead, and what that removes besides
        // queries.

        // TODO 7: rank all six by rows read for N = 100,000, not by query
        // count. The ranking changes. Say what that tells you about "fewer
        // queries is better".

        // TODO 8: write the test that would have caught methods 1, 3 and 4 —
        // one test, asserting an exact number. Say why asserting "fewer than
        // 10" would have been useless.
    }
}

Run it locally:

cd exercises/java/jpa-hibernate/n-plus-one/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Hints

  1. Hint 1

    For each method, write down the query count as a formula in N before you run anything. Then check yourself.

  2. Hint 2

    One method has one query and reads more rows than any of the others. Find it by counting rows, not statements.

  3. Hint 3

    One is a correct fetch join that becomes wrong the moment a Pageable is added. Add one and watch what changes.

  4. Hint 4

    One needs no entities at all. Ask what the screen actually renders.

Done when

  • A query-count formula in N for each of the six
  • The four N+1s identified, each with a specific fix
  • The one-query method that reads the most rows is identified
  • You said which method should not be returning entities, and why
  • You wrote a test that fails when a fix is reverted

← Back to What is the N+1 problem, and how do you detect it?