Challenge

Six methods, three of them write

25 minintermediate115 yrs

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

What this teaches

  • Whether a setter writes depends on state invisible at the call site
  • An early return is not a rollback
  • readOnly turns a bug into a silent no-op rather than an error
  • Two methods are correct and one of those is still a design problem

Starter

Starter.java
import java.util.*;

/**
 * Challenge: six service methods. Three write to the database.
 *
 * None of them calls save(). Two of the three that write look like reads, and
 * one of the three that does not write looks like a write.
 */
public class Starter {

    // ── 1 ─────────────────────────────────────────────────────────────────
    //   @Transactional
    //   public ProductDto get(Long id) {
    //       Product p = repo.findById(id).orElseThrow();
    //       p.setName(p.getName().trim());
    //       return mapper.toDto(p);
    //   }

    // ── 2 ─────────────────────────────────────────────────────────────────
    //   @Transactional(readOnly = true)
    //   public ProductDto describe(Long id) {
    //       Product p = repo.findById(id).orElseThrow();
    //       p.setName(p.getName().trim());
    //       return mapper.toDto(p);
    //   }

    // ── 3 ─────────────────────────────────────────────────────────────────
    //   @Transactional
    //   public void rename(Long id, String name) {
    //       Product p = repo.findById(id).orElseThrow();
    //       p.setName(name);
    //       if (!isValid(name)) return;
    //       audit.record(id, name);
    //   }

    // ── 4 ─────────────────────────────────────────────────────────────────
    //   public void rename(Product detached, String name) {    // no @Transactional
    //       detached.setName(name);
    //       repo.save(detached);
    //   }

    // ── 5 ─────────────────────────────────────────────────────────────────
    //   @Transactional
    //   public Product load(Long id) {
    //       return repo.findById(id).orElseThrow();            // returns the entity
    //   }

    // ── 6 ─────────────────────────────────────────────────────────────────
    //   @Transactional
    //   public void restock(Long id, int qty) {
    //       Product p = repo.findById(id).orElseThrow();
    //       p.setStock(p.getStock() + qty);
    //       repo.save(p);
    //   }

    public static void main(String[] args) {

        // TODO 1: classify each: writes, does not write, or depends on the
        // caller. For each answer, name the two facts that decided it.

        // TODO 2: methods 1 and 2 are the same code with one annotation
        // different. Say what each does to the database, and which one is
        // more dangerous — the answer is not obvious.

        // TODO 3: method 3 returns early. Does the invalid name reach the
        // database? Fix it, and note that a larger if-statement is not the fix.

        // TODO 4: method 4 calls save() on a detached entity. What does save()
        // actually do here, and what does it return? This is where merge()
        // enters the conversation.

        // TODO 5: method 5 is correct and is still a problem. Describe what
        // happens when a caller sets a field on what it returns — and then
        // describe what happens if the caller is itself @Transactional.

        // TODO 6: method 6 calls save() and does not need to. Argue both sides
        // of leaving it in, then state a rule you would apply consistently.

        // TODO 7: one change would remove the ambiguity from most of these.
        // Name it. Say what it costs and what it makes impossible.
    }
}

Run it locally:

cd exercises/java/jpa-hibernate/dirty-checking/02-challenge
javac Starter.java -d /tmp/out && java -cp /tmp/out Starter

Hints

  1. Hint 1

    For each method, answer two things before anything else: is the entity managed, and is there a transaction. Everything follows from those.

  2. Hint 2

    One method returns early expecting that to prevent the write. Trace what the transaction does after the return.

  3. Hint 3

    One method is marked readOnly and mutates. Say what happens, and then say what happens if someone removes the annotation later.

  4. Hint 4

    One method is completely correct and still hands a caller something whose setters behave differently depending on where they are called.

Done when

  • Each of the six is classified as writes / does not write / depends
  • The early-return method is identified and the fix is not a bigger if
  • The readOnly mutation is identified, with what a future maintainer breaks
  • You said which correct method is still a design problem, and why
  • You named the one change that removes this whole class of ambiguity

← Back to What is dirty checking, and why did my entity save without a save() call?