Coding Round
Implement a mini persistence context (identity map / first-level cache).
Example
Within one context the same id always returns the same object and the database is queried once. After clear() a new instance is loaded, like a new transaction.
import java.util.*;
public class Main {
public static void main(String[] args) {
FakeDb db = new FakeDb();
PersistenceContext em = new PersistenceContext(db);
Product a = em.find(1L);
Product b = em.find(1L);
System.out.println("a == b ? " + (a == b));
System.out.println("SELECTs so far: " + db.selects);
em.clear();
Product c = em.find(1L);
System.out.println("a == c after clear ? " + (a == c));
System.out.println("SELECTs so far: " + db.selects);
}
}
class Product {
final long id;
String name;
Product(long id, String name) { this.id = id; this.name = name; }
}
class FakeDb {
int selects = 0;
private final Map<Long, String> rows = Map.of(1L, "Laptop", 2L, "Phone");
Product select(long id) {
selects++;
System.out.println("SQL: select * from product where id=" + id);
return new Product(id, rows.get(id));
}
}
class PersistenceContext {
private final Map<Long, Product> identityMap = new HashMap<>();
private final FakeDb db;
PersistenceContext(FakeDb db) { this.db = db; }
Product find(long id) { return identityMap.computeIfAbsent(id, db::select); }
void clear() { identityMap.clear(); }
}Output
SQL: select * from product where id=1 a == b ? true SELECTs so far: 1 SQL: select * from product where id=1 a == c after clear ? false SELECTs so far: 2
Simulate Hibernate dirty checking with snapshots.
Example
On load the context stores a copy of the state; at flush it compares the current state with the snapshot. Like Hibernate by default, the UPDATE sets all columns (with @DynamicUpdate it would set only the dirty ones).
import java.util.*;
public class Main {
public static void main(String[] args) {
Session session = new Session();
Product laptop = session.load(new Product(1L, "Laptop", 50000, 10));
session.load(new Product(2L, "Phone", 20000, 5));
laptop.price = 45000; // no save() call anywhere
laptop.stock = 9;
System.out.println("-- commit #1");
session.flush();
System.out.println("-- commit #2 (nothing changed)");
session.flush();
}
}
class Product {
Long id; String name; int price; int stock;
Product(Long id, String name, int price, int stock) {
this.id = id; this.name = name; this.price = price; this.stock = stock;
}
Object[] state() { return new Object[] {name, price, stock}; }
}
class Session {
private static final String[] COLS = {"name", "price", "stock"};
private final Map<Long, Product> managed = new LinkedHashMap<>();
private final Map<Long, Object[]> snapshots = new HashMap<>();
Product load(Product p) {
managed.put(p.id, p);
snapshots.put(p.id, p.state());
return p;
}
void flush() {
for (Product p : managed.values()) {
Object[] now = p.state(), old = snapshots.get(p.id);
List<String> dirty = new ArrayList<>();
for (int i = 0; i < COLS.length; i++) {
if (!Objects.equals(now[i], old[i])) dirty.add(COLS[i]);
}
if (dirty.isEmpty()) { System.out.println("Product " + p.id + ": clean, no SQL"); continue; }
System.out.println("Product " + p.id + ": dirty " + dirty);
System.out.printf("SQL: update product set name='%s', price=%d, stock=%d where id=%d%n",
p.name, p.price, p.stock, p.id);
snapshots.put(p.id, now);
}
}
}Output
-- commit #1 Product 1: dirty [price, stock] SQL: update product set name='Laptop', price=45000, stock=9 where id=1 Product 2: clean, no SQL -- commit #2 (nothing changed) Product 1: clean, no SQL Product 2: clean, no SQL
Show the N+1 problem and how JOIN FETCH and batch fetching fix it.
Example
5 orders belong to 4 distinct customers. Lazy loading costs 1 + 4 queries (the first-level cache avoids loading customer 1 twice), batch fetching with size 2 costs 1 + 2, and JOIN FETCH costs 1.
import java.util.*;
public class Main {
record Order(int id, int customerId) {}
static final List<Order> ORDERS = List.of(
new Order(101, 1), new Order(102, 2), new Order(103, 1), new Order(104, 3), new Order(105, 4));
static int queries;
static void sql(String s) { queries++; System.out.println(" " + s); }
public static void main(String[] args) {
System.out.println("Lazy loading:");
queries = 0;
sql("select * from orders");
Set<Integer> loaded = new HashSet<>(); // first-level cache
for (Order o : ORDERS) {
if (loaded.add(o.customerId())) sql("select * from customer where id=" + o.customerId());
}
System.out.println(" total queries = " + queries);
System.out.println("Batch fetching (batch size 2):");
queries = 0;
sql("select * from orders");
List<Integer> ids = new ArrayList<>(new LinkedHashSet<>(ORDERS.stream().map(Order::customerId).toList()));
for (int i = 0; i < ids.size(); i += 2) {
List<Integer> chunk = ids.subList(i, Math.min(i + 2, ids.size()));
sql("select * from customer where id in " + chunk.toString().replace('[', '(').replace(']', ')'));
}
System.out.println(" total queries = " + queries);
System.out.println("JOIN FETCH:");
queries = 0;
sql("select o.*, c.* from orders o join customer c on c.id = o.customer_id");
System.out.println(" total queries = " + queries);
}
}Output
Lazy loading: select * from orders select * from customer where id=1 select * from customer where id=2 select * from customer where id=3 select * from customer where id=4 total queries = 5 Batch fetching (batch size 2): select * from orders select * from customer where id in (1, 2) select * from customer where id in (3, 4) total queries = 3 JOIN FETCH: select o.*, c.* from orders o join customer c on c.id = o.customer_id total queries = 1
Build a lazy-loading proxy and reproduce LazyInitializationException.
Example
Hibernate puts a proxy in place of a lazy @ManyToOne. The first method call loads the row; if the session is already closed, it throws. Here a JDK dynamic proxy does the same thing.
import java.lang.reflect.*;
public class Main {
public static void main(String[] args) {
Session s1 = new Session();
Order o1 = s1.findOrder(101);
System.out.println("customer is a proxy: " + Proxy.isProxyClass(o1.customer.getClass()));
System.out.println("name = " + o1.customer.name()); // triggers the SELECT
System.out.println("city = " + o1.customer.city()); // already loaded, no SQL
Session s2 = new Session();
Order o2 = s2.findOrder(102);
s2.close(); // transaction ended
try {
o2.customer.name();
} catch (LazyInitializationException e) {
System.out.println("LazyInitializationException: " + e.getMessage());
}
}
}
interface Customer { String name(); String city(); }
record CustomerRow(String name, String city) implements Customer {}
class LazyInitializationException extends RuntimeException {
LazyInitializationException(String msg) { super(msg); }
}
class Order {
final int id; final Customer customer;
Order(int id, Customer customer) { this.id = id; this.customer = customer; }
}
class Session {
private boolean open = true;
void close() { open = false; }
Order findOrder(int id) {
System.out.println("SQL: select * from orders where id=" + id);
return new Order(id, lazyCustomer(7));
}
private Customer lazyCustomer(int customerId) {
InvocationHandler handler = new InvocationHandler() {
private Customer target;
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
if (target == null) {
if (!open) throw new LazyInitializationException(
"could not initialize proxy [Customer#" + customerId + "] - no Session");
System.out.println("SQL: select * from customer where id=" + customerId);
target = new CustomerRow("Asha", "Pune");
}
return m.invoke(target, args);
}
};
return (Customer) Proxy.newProxyInstance(
Customer.class.getClassLoader(), new Class<?>[] {Customer.class}, handler);
}
}Output
SQL: select * from orders where id=101 customer is a proxy: true SQL: select * from customer where id=7 name = Asha city = Pune SQL: select * from orders where id=102 LazyInitializationException: could not initialize proxy [Customer#7] - no Session
Implement optimistic locking with a version column.
Example
Two users read the same row (version 0). The first UPDATE matches the version and increments it; the second matches 0 rows, so the write is rejected instead of silently overwriting.
public class Main {
public static void main(String[] args) {
ProductTable table = new ProductTable();
Row asha = table.read();
Row ravi = table.read();
table.update(asha.withStock(asha.stock() - 1));
try {
table.update(ravi.withStock(ravi.stock() - 1));
} catch (OptimisticLockException e) {
System.out.println("Ravi: " + e.getMessage() + " -> reload and retry, or show 'out of stock'");
}
System.out.println("Final: " + table.read());
}
}
record Row(long id, int stock, int version) {
Row withStock(int s) { return new Row(id, s, version); }
}
class OptimisticLockException extends RuntimeException {
OptimisticLockException(String msg) { super(msg); }
}
class ProductTable {
private Row row = new Row(1, 1, 0);
Row read() { return row; }
void update(Row changed) {
int updated = row.version() == changed.version() ? 1 : 0;
System.out.println("SQL: update product set stock=" + changed.stock() + ", version=" + (changed.version() + 1)
+ " where id=1 and version=" + changed.version() + " -> " + updated + " row(s)");
if (updated == 0) throw new OptimisticLockException("Row was updated by another transaction");
row = new Row(changed.id(), changed.stock(), changed.version() + 1);
}
}Output
SQL: update product set stock=0, version=1 where id=1 and version=0 -> 1 row(s) SQL: update product set stock=0, version=1 where id=1 and version=0 -> 0 row(s) Ravi: Row was updated by another transaction -> reload and retry, or show 'out of stock' Final: Row[id=1, stock=0, version=1]
Write a safe equals/hashCode for an entity with a generated id.
Example
The "bad" version hashes the id, so the object is lost in a HashSet once persist assigns the id. The "good" version compares ids (null never equal) and uses a constant hashCode.
import java.util.*;
public class Main {
public static void main(String[] args) {
Set<BadProduct> bad = new HashSet<>();
BadProduct b = new BadProduct("Laptop");
bad.add(b);
b.id = 1L; // persist() assigns the id
System.out.println("bad: found after persist? " + bad.contains(b));
Set<GoodProduct> good = new HashSet<>();
GoodProduct g = new GoodProduct("Laptop");
good.add(g);
g.id = 1L;
System.out.println("good: found after persist? " + good.contains(g));
GoodProduct sameRow = new GoodProduct("Laptop (detached copy)");
sameRow.id = 1L;
System.out.println("good: same row equal? " + g.equals(sameRow));
System.out.println("good: two new objects equal? " + new GoodProduct("A").equals(new GoodProduct("B")));
}
}
class BadProduct {
Long id; String name;
BadProduct(String name) { this.name = name; }
@Override public boolean equals(Object o) { return o instanceof BadProduct other && Objects.equals(id, other.id); }
@Override public int hashCode() { return Objects.hash(id); }
}
class GoodProduct {
Long id; String name;
GoodProduct(String name) { this.name = name; }
@Override public boolean equals(Object o) {
if (this == o) return true;
return o instanceof GoodProduct other && id != null && id.equals(other.id);
}
@Override public int hashCode() { return getClass().hashCode(); } // stable across the lifecycle
}Output
bad: found after persist? false good: found after persist? true good: same row equal? true good: two new objects equal? false
Implement Page and Slice pagination the way Spring Data does.
Example
A Page runs a data query plus a COUNT query; a Slice fetches one extra row to know whether there is a next page and skips the COUNT.
import java.util.*;
import java.util.stream.*;
public class Main {
static final List<String> PRODUCTS = IntStream.rangeClosed(1, 12).mapToObj(i -> "P" + i).toList();
static void page(int number, int size) {
int offset = number * size;
System.out.println("SQL: select * from product order by id offset " + offset + " limit " + size);
System.out.println("SQL: select count(*) from product");
List<String> content = PRODUCTS.subList(Math.min(offset, PRODUCTS.size()), Math.min(offset + size, PRODUCTS.size()));
int totalPages = (PRODUCTS.size() + size - 1) / size;
System.out.printf("Page %d %s totalElements=%d totalPages=%d hasNext=%b%n",
number, content, PRODUCTS.size(), totalPages, number + 1 < totalPages);
}
static void slice(int number, int size) {
int offset = number * size;
System.out.println("SQL: select * from product order by id offset " + offset + " limit " + (size + 1));
List<String> fetched = PRODUCTS.subList(Math.min(offset, PRODUCTS.size()), Math.min(offset + size + 1, PRODUCTS.size()));
boolean hasNext = fetched.size() > size;
List<String> content = hasNext ? fetched.subList(0, size) : fetched;
System.out.printf("Slice %d %s hasNext=%b%n", number, content, hasNext);
}
public static void main(String[] args) {
page(0, 5);
page(2, 5);
slice(1, 5);
slice(2, 5);
}
}Output
SQL: select * from product order by id offset 0 limit 5 SQL: select count(*) from product Page 0 [P1, P2, P3, P4, P5] totalElements=12 totalPages=3 hasNext=true SQL: select * from product order by id offset 10 limit 5 SQL: select count(*) from product Page 2 [P11, P12] totalElements=12 totalPages=3 hasNext=false SQL: select * from product order by id offset 5 limit 6 Slice 1 [P6, P7, P8, P9, P10] hasNext=true SQL: select * from product order by id offset 10 limit 6 Slice 2 [P11, P12] hasNext=false