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