---
title: 'Object and Table Binding'
source: 'https://academia.sh/en/courses/enterprise-application-framework/object-table-binding'
course: 'The Enterprise Application Framework'
language: en
updated: '2026-09-13T05:08:57+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Measures lazy loading by counting that two of an entity object's four fields load at load time and two load on first access"
    - 'Shows that a second access to the same lazy field does not read the table again, it returns from the cache'
    - 'Measures that the table cannot be reached, and the defect stays silent, when a lazy field that was never accessed before is accessed after the object has detached from the container'
---

# Object and Table Binding

Two of an entity object's four fields load at load time, two load on first access. A second access does not read the table again, it returns from the cache; but once the object has detached from the container, accessing a field that was never accessed before cannot reach the table, and the defect silently comes back as an empty value.

The previous lesson measured which implementation stands behind an interface, but it never
asked how the implementation's own fields match between an object and a repository. This
lesson takes this up: it measures that the binding between an object's fields and a table's
columns comes from an annotation in the source, and that a field that is never accessed is
never loaded at all. In the course's terminology decision, this connection is
**object-table binding**; the object itself is an **entity object**.

The table is modeled here too: no real database connection is opened, no real query is run.
A "table" is nothing but a column-name-to-value structure accessed by row identifier —
`Map<Integer, Map<String, Object>>`. An entity object is bound to one of these rows, and
which of the object's fields corresponds to which column is specified in the source with two
annotations: `@Column` for fields that always load, `@LazyColumn` for fields that load only
when accessed.

It should not be forgotten that this distinction is a question of cost. In a real
repository, every column can carry a disk access, a network delay; always reading only two
columns of a row and reading the rest only when needed means never paying anything for
fields of an object that will never be used. This lesson does not measure the cost itself —
no duration or delay is written — but it measures **where the cost falls**: the `loadOrder`
list is a log that records which column was read and when, and this log records every read,
both at load time and at access time, in the same form.

## Two Fields at Load Time, Two Fields on Access

- **WD20.** `ProductCard` carries four fields: `name` and `quantity` are `@Column`,
  `descriptionCache` and `imagePathCache` are `@LazyColumn`. Field names do not have to
  match column names exactly; the annotation's `name` parameter makes the match
  independent of the field's own name in the source.
- **WD21.** A lazy field's real value is kept in a "cache" field; this field, `null` the
  first time, is never reset again after the first successful load.

When `Container.load` turns a row into an object, it fills only the fields carrying
`@Column`; it never touches the ones carrying `@LazyColumn`. The object carries which
container and which row it belongs to through the `session` and `rowId` fields inherited
from the `EntityObject` superclass — these two are not `@Column`, they are a link the object
container sets up itself.

```java
// Container.java — object-table binding arises from an annotation, a lazy column loads at access time
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;

public class Container {

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE)
    @interface Table { String name(); }

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
    @interface Column { String name(); }

    @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
    @interface LazyColumn { String name(); }

    final Map<Integer, Map<String, Object>> table = new LinkedHashMap<>();
    final List<String> loadOrder = new ArrayList<>();

    void addRow(int id, Map<String, Object> columns) { table.put(id, columns); }

    Object readColumn(int id, String columnName, String label) {
        loadOrder.add(columnName + ": " + label);
        return table.get(id).get(columnName);
    }

    <T> T load(Class<T> type, int id) throws ReflectiveOperationException {
        T object = type.getDeclaredConstructor().newInstance();
        for (Field f : type.getDeclaredFields()) {
            f.setAccessible(true);
            Column c = f.getAnnotation(Column.class);
            if (c != null) f.set(object, readColumn(id, c.name(), "at-load-time"));
        }
        Field sessionField = type.getSuperclass().getDeclaredField("session");
        Field idField = type.getSuperclass().getDeclaredField("rowId");
        sessionField.setAccessible(true);
        idField.setAccessible(true);
        sessionField.set(object, this);
        idField.set(object, id);
        return object;
    }

    void detach(EntityObject object) throws ReflectiveOperationException {
        Field sessionField = EntityObject.class.getDeclaredField("session");
        sessionField.setAccessible(true);
        sessionField.set(object, null);
    }

    public static void main(String[] args) throws Exception {
        Container container = new Container();
        container.addRow(1, Map.of("name", "screw", "quantity", 12, "description", "6mm screw with a nut", "imagePath", "image/screw.png"));

        System.out.println("-- loading through the container: only two columns are read at load time --");
        ProductCard product = container.load(ProductCard.class, 1);
        System.out.println("load order after loading: " + container.loadOrder);
        System.out.println("total fields: 4, read at load time: "
                + container.loadOrder.stream().filter(s -> s.endsWith("at-load-time")).count());

        System.out.println("-- accessing a lazy field: the first call reads the table --");
        System.out.println("description: " + product.description());
        System.out.println("description again: " + product.description());
        System.out.println("load order (after description): " + container.loadOrder);
        System.out.println("how many times the table was read for description: "
                + container.loadOrder.stream().filter(s -> s.startsWith("description:") && s.endsWith("at-access-time")).count());
        System.out.println("imagePath was never accessed, never appears in the record: "
                + container.loadOrder.stream().noneMatch(s -> s.startsWith("imagePath")));

        System.out.println("-- lazy field never accessed before detaching from the container --");
        container.detach(product);
        System.out.println("imagePath (first access after detaching from the container): '" + product.imagePath() + "'");
        System.out.println("defect type: silent (no exception, an empty value silently came back)");

        System.out.println("-- manually constructed object: the container is never involved --");
        ProductCard manual = new ProductCard();
        System.out.println("in a manually constructed object, description on first access: '" + manual.description() + "'");
    }
}

class EntityObject {
    transient Container session;
    transient int rowId;

    Object lazyRead(String columnName, String cache) {
        if (cache != null) return cache;
        if (session == null) return "";
        return String.valueOf(session.readColumn(rowId, columnName, "at-access-time"));
    }
}

@Container.Table(name = "product_card")
class ProductCard extends EntityObject {
    @Container.Column(name = "name") String name;
    @Container.Column(name = "quantity") int quantity;
    @Container.LazyColumn(name = "description") String descriptionCache;
    @Container.LazyColumn(name = "imagePath") String imagePathCache;

    String description() {
        descriptionCache = (String) lazyRead("description", descriptionCache);
        return descriptionCache;
    }

    String imagePath() {
        imagePathCache = (String) lazyRead("imagePath", imagePathCache);
        return imagePathCache;
    }
}
```

```
-- loading through the container: only two columns are read at load time --
load order after loading: [name: at-load-time, quantity: at-load-time]
total fields: 4, read at load time: 2
-- accessing a lazy field: the first call reads the table --
description: 6mm screw with a nut
description again: 6mm screw with a nut
load order (after description): [name: at-load-time, quantity: at-load-time, description: at-access-time]
how many times the table was read for description: 1
imagePath was never accessed, never appears in the record: true
-- lazy field never accessed before detaching from the container --
imagePath (first access after detaching from the container): ''
defect type: silent (no exception, an empty value silently came back)
-- manually constructed object: the container is never involved --
in a manually constructed object, description on first access: ''
```

The initial state of `container.loadOrder` carries only two entries: `name` and `quantity`.
`description` and `imagePath` do not appear in this list at all, because `load` never read
them — a four-field object was built by reading only two of the table's columns. This is not
a result that contradicts the source text, it is exactly what the source says: `@Column`
exists on only two fields, `@LazyColumn` on the other two, and `load` fills only the ones
carrying `@Column`. But someone reading the row knows all four of `ProductCard`'s fields
come from a table row; knowing which two come **when** requires reading the body of
`Container.load`.

## A Lazy Field Loads Only Once

- **WD22.** When the `description()` method is called, it first checks its own cache
  field; if that field is filled, it never goes to the table at all. This is the second
  meaning of the method being "lazy": it loads not merely on first access, but at most
  **once**.

`description()` is called twice, and only one `"description: at-access-time"` entry is
added to the `loadOrder` list. The first call sees that the `descriptionCache` field is
`null`, reads the table, and writes the result into the same field. The second call sees
that the same field is now filled and never goes to the table at all. This is a behavior
from the same family as the singleton scope inside the container — there, a component's
second request returned the same instance; here, a field's second access returns the same
value — but its source is different: singleton scope lived in the container's own rule
table, here the caching is a check written into the `lazyRead` method's own body. Since the
`imagePath` field is never accessed, there is no entry for this column in the `loadOrder`
list at all — the fourth field of a four-field object can go the whole length of the program
without ever being asked of the table. This is lazy loading's real payoff: an object
carrying all of its fields does not mean all of them are actually read, and no cost is paid
for a field that is never read.

Caching carries not only a performance decision but a consistency decision too. If some
other path changed the `description` column of the same row between two calls to
`description()`, the `product` object would never see it — once the cache is filled,
`lazyRead` never looks at the table again. This lesson does not run this scenario, but the
result can be inferred from the source: a lazy field carries only the value it read at the
moment it was **first** read, and the object does not change even if the table changes after
that moment. For a caller that holds an entity object for a long time, this means the load
moment is a genuine snapshot, and nowhere is it promised that this snapshot will remain
correct indefinitely.

## Lazy Loading Does Not Work Outside the Container: A Silent Gap

- **WD23.** `detach` only sets the `session` field to `null`; the object's other fields,
  including the already-loaded `name` and `quantity`, do not change at all.
- **WD24.** `lazyRead`, when it sees `session == null`, returns an empty string instead of
  throwing an exception. This is the lesson's second silent defect: the access does not
  fail, it merely produces a wrong value.

The call `container.detach(product)` cuts `product`'s link with the container's session — in
a real application, this corresponds to the object being carried outside the container, to
some other layer, once a request has finished processing. Up to this point, `imagePath` had
never been accessed; the first time it is called after `detach`, `lazyRead` now sees that
`session` is `null`, and there is no way left to ask the table. The method returns an empty
string instead of throwing an exception, and the caller cannot tell this empty string apart
from a genuine "there is no image path" state. The defect here is silent too: the program
does not crash, it prints no warning, it just continues with a wrong value.

This is the lesson's bounding measurement: **lazy loading does not work outside the
container; a field accessed after the object has left the container stays empty.** `name`
and `quantity` do not have this problem, because both were filled at load time and are
unaffected by `detach` — the problem surfaces only in lazy fields that were never accessed.
This also reveals a hidden rule of the loading strategy: how long an entity object stays
inside the container decides which of its lazy fields get accessed, and once that access
window closes, any lazy field left unaccessed can never reach its correct value again.

The last measurement tests this rule with an object that never had it set up in the first
place: an object manually constructed with `new ProductCard()` has a `session` field that
was never bound to a container, `null` from the start. Calling `description()` on this
object also gives an empty string — behavior identical to the detached object, the same
silent result. Both paths land on the same point: the object either was never bound to the
container's session or its bond broke later, and in either case accessing the lazy field
ends in the same empty value. The `@LazyColumn` annotation keeps standing on the
`ProductCard` class — it was not removed — but without a session to fulfill it, this once
again confirms the rule seen in the previous lessons: an annotation is a request, and if
there is no party to fulfill the request, nothing happens.

The only difference between the two is how surprising the defect is. In a manually
constructed object, it is already clear from the start that the container is never
involved — a developer constructing this object with `new` has to expect that lazy loading
will not work. But the object that went through `detach` was inside the container a moment
ago; its `name` and `quantity` fields carry correct values, and the object looks "real" in
every respect. This appearance gives the impression that a field never accessed, like
`imagePath`, is just as trustworthy — when in fact that field is still dependent on the
container's session, despite the object's apparent completeness. The bounding measurement's
real value is here: the problem does not surface in some random field, it surfaces exactly
at the intersection of **leaving the container most recently** and **never having been
touched**, and this intersection is marked nowhere in the source text.

## Summary

- Two of `ProductCard`'s four fields (`name`, `quantity`) were read from the table at load
  time; two (`descriptionCache`, `imagePathCache`) were read only on first access.
- A second access to a lazy field never went to the table at all; when the cache field was
  filled, `lazyRead` returned it directly.
- A lazy field that was never accessed (`imagePath`) was never asked of the table for the
  rest of the program; this is the side of lazy loading that charges no cost for a field
  that is never read.
- When a lazy field that was never accessed was accessed after the object detached from
  the container, the table could not be reached and the method returned an empty string
  instead of an exception; the defect stayed silent.
- Accessing the same lazy field on a manually constructed object gave the exact same empty
  result as the detached object; what both were missing was the container's session.

## Next Step

This lesson measured when an object's fields load, but it never asked where this loading,
and the changes made on top of it, begin and end. The next lesson takes this up: it
measures that a declarative transaction boundary is an annotation in the source, and how a
component calling itself skips this boundary.
