ExerciseChallenge
Challenge
Six methods, three of them write
25 minintermediate1–15 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 StarterHints
Hint 1
For each method, answer two things before anything else: is the entity managed, and is there a transaction. Everything follows from those.
Hint 2
One method returns early expecting that to prevent the write. Trace what the transaction does after the return.
Hint 3
One method is marked readOnly and mutates. Say what happens, and then say what happens if someone removes the annotation later.
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?