Careeroza
Detached Entities
How Entities Become Detached
- The transaction ends: in Spring Boot the entity returned from a
@Transactionalservice method is detached in the controller. em.detach(entity),em.clear()orem.close().- The object is serialized (e.g. sent to another service) and deserialized again.
Changes to Detached Objects Are Lost
@RestController
class ProductController {
@PutMapping("/api/products/{id}/stock")
public Product setStock(@PathVariable Long id, @RequestParam int stock) {
Product p = productService.get(id); // transaction ended -> p is DETACHED
p.setStock(stock); // nobody is watching: NO UPDATE
return p; // response shows the new stock, database does not!
}
}The fix: do the change inside a transactional service method, where the entity is managed:
@Transactional
public Product setStock(Long id, int stock) {
Product p = repo.findById(id).orElseThrow(); // managed
p.setStock(stock); // UPDATE at commit
return p;
}Reattaching with merge
Example
The detached object keeps its own changes, but only the copy returned by merge is managed. A common bug is to keep using the old reference:
import java.util.*;
public class Main {
static Map<Long, Product> context = new LinkedHashMap<>();
static Map<Long, Integer> table = new TreeMap<>(Map.of(42L, 120));
public static void main(String[] args) {
Product detached = new Product(42L, 120); // loaded in an earlier, finished transaction
detached.stock = 100; // changed while detached
Product managed = merge(detached);
System.out.println("managed.stock = " + managed.stock + ", same object? " + (managed == detached));
detached.stock = 5; // BUG: changing the detached copy
flush();
System.out.println("database stock = " + table.get(42L));
managed.stock = 90; // correct: change the managed instance
flush();
System.out.println("database stock = " + table.get(42L));
}
static Product merge(Product d) {
Product m = context.computeIfAbsent(d.id, id -> new Product(id, table.get(id))); // SELECT
m.stock = d.stock; // copy state
return m;
}
static void flush() { context.values().forEach(p -> table.put(p.id, p.stock)); }
}
class Product {
final Long id; Integer stock;
Product(Long id, Integer stock) { this.id = id; this.stock = stock; }
}Output
managed.stock = 100, same object? false database stock = 100 database stock = 90
Tip: Rule of thumb: product = em.merge(product); — always reassign the result of merge.