Install
openclaw skills install bookforge-object-relational-structural-mapping-guideObject-relational mapping structural patterns guide. Use when designing or auditing how domain objects map to relational tables — identity fields, foreign key mapping, association table mapping for many-to-many relationships, dependent mapping for child objects with cascade delete, embedded value for value object mapping, and serialized LOB for JSON column or blob storage. Applies when choosing ORM associations (Hibernate, SQLAlchemy, EF Core, ActiveRecord, Django ORM), deciding between a join table and nested foreign keys, mapping address or money value objects as inline columns, or detecting serialized LOB overuse on queryable data. Covers the six PEAA structural patterns: Identity Field (surrogate key vs meaningful key), Foreign Key Mapping (single-valued reference), Association Table Mapping (many-to-many via join table), Dependent Mapping (child lifecycle owned by parent), Embedded Value (value object as columns), Serialized LOB (graph serialized to JSON/BLOB column).
openclaw skills install bookforge-object-relational-structural-mapping-guideSix PEAA patterns that bridge between OO domain objects and relational tables: Identity Field, Foreign Key Mapping, Association Table Mapping, Dependent Mapping, Embedded Value, and Serialized LOB.
Use this skill when you are:
Not for:
inheritance-mapping-selectordata-source-pattern-selectorGather before proceeding:
Required:
Observable from codebase:
@Entity, models.Model, Column, etc.)pom.xml, requirements.txt, *.csproj, Gemfile) to confirm ORM versionAsk if absent:
Sufficiency check: Proceed once you have the domain class list, relationship cardinalities, and a yes/no on value object identity. ORM stack helps with the output but is not blocking.
For each domain class and relationship, classify it into one of these structural types:
| Type | Signal |
|---|---|
| Entity with identity | Can be loaded/deleted independently; has a unique ID |
| Value object | No independent identity; always belongs to one owner (Money, Address, DateRange) |
| Single-valued reference | One object holds a reference to exactly one other entity |
| Collection reference (1:N) | One object holds a collection of other entities; each child knows its parent |
| Many-to-many reference | Both sides hold collections pointing at each other |
| Dependent child | Child exists only in the context of an owner; no external references to child |
| Complex nested graph | Hierarchical or graph structure that would require many joins relationally |
WHY: Each structural type maps to exactly one pattern. Misclassifying here leads to wrong pattern choice (e.g., treating a Value Object as an entity creates an unnecessary table and Identity Map entry).
| Structure | Pattern | Key rule |
|---|---|---|
| Every persistable entity | Identity Field | Use surrogate (auto-assigned) key — never meaningful keys |
| Single-valued reference (1:1, N:1) | Foreign Key Mapping | FK lives in the "many" or "child" table |
| 1:N collection (entity children) | Foreign Key Mapping | FK lives in child table; parent has collection in memory, not in the table |
| N:M relationship | Association Table Mapping | Always use a join table — even if no attributes today |
| Child with no independent identity | Dependent Mapping | No Identity Field on child; owner mapper handles all persistence |
| Value object (DDD Value Object) | Embedded Value | Map value's fields as columns on the owner's table |
| Non-queryable complex subgraph | Serialized LOB | Only when SQL queries will NEVER need to filter by content |
WHY: This routing table encodes the core insight that objects and relations have fundamentally different link representations. Without this explicit classification, teams default to wrong choices: adding FK columns for N:M (violates first normal form), creating tables for value objects (unnecessary complexity), or Serialized LOB for data later needing SQL queries (queryability trap).
For every entity class (not value objects, not dependents):
BIGINT) is the best default — fast equality check, fast increment, effectively unlimited range. UUIDs/GUIDs provide database-wide uniqueness at the cost of larger index size and slower inserts (random insertion order).WHY: The Identity Field is the bridge between the object graph and the relational schema. Without it, you cannot map FK references back to in-memory objects. Without surrogate keys, you inherit the fragility of the real world into your database contract.
Foreign Key Mapping (1:1 and N:1):
Association Table Mapping (N:M):
WHY for Association Table: There is no alternative for N:M in relational databases. Attempts to model N:M with a list-of-IDs column violate first normal form and make queries and updates extremely painful. Even if the association has no attributes today, using a join table preserves schema flexibility for when attributes appear.
Dependent Mapping:
Embedded Value:
employment.salary_amount, employment.salary_currency).WHY for Embedded Value: Value Objects have no identity and should never have their own table — a table of Money values or Address values is meaningless without context. Embedding them preserves the OO semantics (the value is part of the owner, not related to it) while avoiding extra joins and unnecessary tables.
For complex hierarchical or graph structures:
parent_id on an organization table)? If yes, prefer this — it keeps data queryable.WHY: Serialized LOB sacrifices SQL queryability for schema simplicity. This trade-off is acceptable for truly private, complex, non-queryable subgraphs. It is a trap when applied to data that reporting queries, search, or business logic will need to inspect.
For each entity and relationship, output:
[ClassName / Relationship]
Pattern: <pattern name>
Schema: <table/columns/constraints sketch>
ORM config: <annotation or model field>
Rationale: <why this pattern fits>
Anti-pattern warning: <if applicable>
Review cross-cutting concerns:
A structural mapping design document containing:
Output template (per structure):
## [Structure Name]
**Pattern:** [Pattern Name]
**Schema:**
[table_name]([pk] BIGINT PK, [fk] BIGINT FK → [other_table.pk], [field] TYPE, ...)
**ORM:**
[stack-specific annotation/field declaration]
**Rationale:** [1-2 sentences on why this pattern fits]
**Warning:** [if anti-pattern risk exists]
1. Surrogate keys over meaningful keys — always. Meaningful keys (SSN, email, order number) require uniqueness AND immutability from the real world, which human error and business rule changes routinely violate. Surrogate auto-assigned keys give you control over both. Fowler's framing: "take a rare stand on the side of meaninglessness."
2. Association Table Mapping is the only correct answer for N:M. Any attempt to encode a many-to-many with a list-of-IDs column violates first normal form and will make future queries impossible. Use a join table, even if the association has no attributes today — you will thank yourself when it acquires them.
3. Dependent Mapping requires no-external-FK discipline. A child object only qualifies as a dependent if no other table holds a FK reference to its table. The moment another entity needs a direct reference to the child, the child needs its own Identity Field and becomes a standalone entity mapped via Foreign Key Mapping.
4. All Value Objects should use Embedded Value. DDD Value Objects (Money, Address, DateRange) have no independent identity. Giving them their own table and Identity Field is wrong — it implies identity they don't have and adds joins where none are needed. The owner table absorbs their columns.
5. Serialized LOB is a trap for queryable data. The check is binary: will SQL ever need to filter, sort, or join on data inside the column? If yes, normalize it. PostgreSQL JSONB operators and XPath do not change this calculus — they are not portable and do not perform at scale like indexed normalized columns.
6. The structural inversion rule for 1:N. In OO, the parent holds the collection (album has tracks). In the relational model, the FK is on the child (track.album_id). This inversion is the source of most ORM confusion. The rule: the FK always lives on the "many" side of the relationship, regardless of which direction the OO association points.
7. When a join table acquires attributes, promote it to an entity. The moment a link table needs its own data (start_date, end_date, role, weight), it should become a first-class entity with its own Identity Field and explicit Foreign Key Mappings on both sides. This is a DDD relationship-as-entity promotion.
Trigger: Team is designing the persistence layer for an order management system. Domain classes: Customer, Order, LineItem, Address (value object for shipping and billing).
Process:
customer_id BIGINT)order.customer_id FK → customers.customer_id)order_id, sequence composite PK or surrogate; cascade all)shipping_street, shipping_city, shipping_zip, billing_street, etc. on the orders table)order_promotions(order_id, promotion_id))Output:
customers(customer_id BIGINT PK, name VARCHAR)
orders(order_id BIGINT PK, customer_id BIGINT FK,
shipping_street VARCHAR, shipping_city VARCHAR, shipping_zip VARCHAR,
billing_street VARCHAR, billing_city VARCHAR, billing_zip VARCHAR)
line_items(line_item_id BIGINT PK, order_id BIGINT FK, product_id BIGINT FK,
quantity INT, unit_price DECIMAL)
order_promotions(order_id BIGINT FK, promotion_id BIGINT FK, PRIMARY KEY(order_id, promotion_id))
LineItem uses cascade-delete. ShippingAddress and BillingAddress are Embedded Value — no join needed to load them. Order↔Promotion uses a join table with no attributes (yet).
Trigger: Mapping the Artist/Album/Track domain from PEAA Chapter 12.
Process:
artist_id BIGINT)album.artist_id FK → artists.artist_id)Output schema:
artists(artist_id BIGINT PK, name VARCHAR)
albums(album_id BIGINT PK, artist_id BIGINT FK, title VARCHAR)
tracks(album_id BIGINT FK, sequence INT, title VARCHAR, duration INT,
PRIMARY KEY(album_id, sequence))
ORM (Hibernate): @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) on Album → tracks. Tracks loaded when Album is loaded.
Trigger: Existing system stores customer contact preferences as XML CLOB in the customers table. Support team needs to query "all customers who prefer email contact." Currently impossible via SQL.
Process:
customers.preferences_xml CLOB — a Serialized LOB.preferred_channel = 'email' must be queryable.customer_preferences table: (customer_id BIGINT FK, preference_key VARCHAR, preference_value VARCHAR) or (customer_id, channel ENUM, enabled BOOLEAN). Apply Foreign Key Mapping.This skill is licensed under CC-BY-SA-4.0. Source: BookForge — Patterns of Enterprise Application Architecture by Martin Fowler, David Rice, Matthew Foemmel, Edward Hieatt, Robert Mee, Randy Stafford.
Install related skills from ClawhHub:
clawhub install bookforge-data-source-pattern-selectorclawhub install bookforge-inheritance-mapping-selectorclawhub install bookforge-data-access-anti-pattern-auditorOr install the full book set from GitHub: bookforge-skills