Definition
- @SecondaryTable – declares an additional table that shares the primary key with the main @Table, allowing one entity to span multiple physical tables.
- @PrimaryKeyJoinColumn – specifies the column in the secondary table that joins back to the primary table's PK.
- @Column(table="...") – routes a field to the secondary table; fields without this attribute default to the primary table.
- Transparent join – Hibernate generates a JOIN on read and splits INSERTs/UPDATEs across both tables on write.
- Use cases – legacy schemas with split records, separating hot (frequently-read) from cold (rarely-read) columns, and isolating sensitive data.
A customer's full record at National Bank of India is one logical thing, but the data lives in two filing cabinets: the everyday cabinet holds name and balance, while a separate secure cabinet holds the KYC documents and address proof. @SecondaryTable is the clerk who knows both cabinets belong to the same customer number, fetches from both when you ask for the customer, and files into both when you update, so you deal with one customer, not two cabinets.
Understanding the Concepts
What is @SecondaryTable?
A normal entity maps to one table via @Table. But sometimes one logical record is physically spread across two tables that share the same primary key. @SecondaryTable tells Hibernate about the second table and how it joins to the first. Hibernate transparently joins on read and splits writes across both tables.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "accounts") // primary table
@SecondaryTable(
name = "account_kyc", // secondary table
pkJoinColumns = @PrimaryKeyJoinColumn(name = "account_id")
)
public class Account {
@Id
@Column(name = "account_id")
private String accountId; // in BOTH tables as PK
@Column(name = "holder_name")
private String holderName; // primary table (default)
@Column(name = "balance", precision = 15, scale = 2)
private BigDecimal balance; // primary table (default)
@Column(table = "account_kyc", name = "pan")
private String pan; // SECONDARY table
@Column(table = "account_kyc", name = "aadhaar_doc", columnDefinition = "TEXT")
private String aadhaarDoc; // SECONDARY table
}
Generated SQL on read:
SELECT a.account_id, a.holder_name, a.balance, k.pan, k.aadhaar_doc
FROM accounts a
JOIN account_kyc k ON a.account_id = k.account_id
WHERE a.account_id = 'SAV2001'
Generated SQL on write:
INSERT INTO accounts (account_id, holder_name, balance) VALUES (...)
INSERT INTO account_kyc (account_id, pan, aadhaar_doc) VALUES (...)
Why does @SecondaryTable exist?
@SecondaryTable exists because database schemas sometimes split one logical record across multiple tables, but the application should model it as one entity. Without @SecondaryTable, you would need two separate entities with a @OneToOne relationship, adding complexity for what is conceptually one record.
// Without @SecondaryTable: two entities, more complexity
package com.bank.model;
import jakarta.persistence.*;
@Entity
public class Account {
@Id
private String accountId;
private String holderName;
@OneToOne(mappedBy = "account", fetch = FetchType.LAZY)
private AccountKyc kyc; // separate entity, separate management
}
@Entity
public class AccountKyc {
@Id
private String accountId;
@OneToOne
@MapsId
private Account account;
private String pan;
}
// More boilerplate, more joins to manage manually
When should you use @SecondaryTable?
Use @SecondaryTable in three situations. First, legacy schemas where a record was historically split across two tables. Second, separating hot columns (name, balance) from cold columns (KYC documents) to keep common queries fast. Third, isolating sensitive data (PAN, Aadhaar) into a table with stricter access controls.
package com.bank.model;
import jakarta.persistence.*;
@Entity
@Table(name = "accounts")
@SecondaryTable(name = "account_kyc",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "account_id"))
@SecondaryTable(name = "account_limits",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "account_id"))
public class Account {
@Id
private String accountId;
// Primary table: hot, frequently accessed
@Column(name = "holder_name")
private String holderName;
// Secondary table 1: sensitive KYC
@Column(table = "account_kyc", name = "pan")
private String pan;
// Secondary table 2: limits (cold, rarely read)
@Column(table = "account_limits", name = "daily_limit", precision = 15, scale = 2)
private java.math.BigDecimal dailyLimit;
}
Use cases:
1. Legacy schema: record split across tables -> one clean entity
2. Hot/cold split: balance in primary, KYC docs in secondary -> fast scans
3. Sensitive data: PAN in separate table -> stricter DB access controls
How does @PrimaryKeyJoinColumn work?
@PrimaryKeyJoinColumn specifies the column in the secondary table that joins back to the primary table's primary key. The secondary table's primary key column is also a foreign key back to the primary table.
@SecondaryTable(
name = "account_kyc",
pkJoinColumns = @PrimaryKeyJoinColumn(
name = "account_id", // column in account_kyc
referencedColumnName = "account_id" // column in accounts (default: PK)
)
)
Database constraint:
ALTER TABLE account_kyc
ADD CONSTRAINT pk_account_kyc PRIMARY KEY (account_id);
ALTER TABLE account_kyc
ADD CONSTRAINT fk_kyc_account FOREIGN KEY (account_id) REFERENCES accounts(account_id);
What are the performance considerations?
By default, Hibernate joins the secondary table eagerly on every read. If the secondary columns are genuinely cold (KYC docs read once a year), the join is wasteful on every balance query. The alternative is a lazy @OneToOne association that loads only when touched.
// @SecondaryTable: always joined (simple but eager)
// SELECT a.*, k.* FROM accounts a JOIN account_kyc k ON ...
// Alternative: lazy @OneToOne (loads on demand)
package com.bank.model;
import jakarta.persistence.*;
@Entity
public class Account {
@Id
private String accountId;
private String holderName;
@OneToOne(mappedBy = "account", fetch = FetchType.LAZY)
private AccountKyc kyc; // loaded only when account.getKyc() is called
}
Performance comparison:
- @SecondaryTable: always JOINs account_kyc -> adds I/O even for balance queries
- Lazy @OneToOne: no JOIN until getKyc() called -> faster for common reads
- Choose based on how often the secondary data is actually needed
What are common mistakes?
The most common mistake is forgetting @Column(table="...") on fields that belong to the secondary table, which causes Hibernate to map them to the primary table. A second mistake is assuming a missing secondary row is handled gracefully; without nullable columns or an outer join, the entity may fail to load.
// WRONG: missing table attribute -> field mapped to primary table
@Column(name = "pan") // goes to primary table 'accounts'
private String pan; // but 'accounts' has no 'pan' column!
// CORRECT: route to secondary table
@Column(table = "account_kyc", name = "pan") // goes to secondary table
private String pan;
What about multiple secondary tables?
You can declare multiple @SecondaryTable annotations (or use @SecondaryTables) to span three or more tables.
package com.bank.model;
import jakarta.persistence.*;
@Entity
@Table(name = "accounts")
@SecondaryTables({
@SecondaryTable(name = "account_kyc",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "account_id")),
@SecondaryTable(name = "account_limits",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "account_id"))
})
public class Account {
@Id private String accountId;
@Column(table = "account_kyc", name = "pan") private String pan;
@Column(table = "account_limits", name = "daily_limit") private java.math.BigDecimal dailyLimit;
}
What should a senior engineer know?
A senior engineer knows that @SecondaryTable is a mapping convenience, not a sharding or scaling tool. Both tables live in the same database and are joined by the same transaction. For genuine scaling, partitioning or separate services are needed. They also ensure secondary table columns are nullable or that a row always exists to prevent join mismatches.
// Senior pattern: nullable secondary columns with explicit defaults
@Column(table = "account_kyc", name = "pan", nullable = true)
private String pan; // nullable so the entity loads even without a KYC row
Visual Overview
@SecondaryTable Mapping
============================================
Primary Table: accounts Secondary Table: account_kyc
+------------------+ +-----------------------+
| account_id (PK) |<--------->| account_id (PK + FK) |
| holder_name | | pan |
| balance | | aadhaar_doc |
+------------------+ +-----------------------+
One Entity: Account
============================================
@Table(name = "accounts")
@SecondaryTable(name = "account_kyc")
Fields without table attr -> accounts (primary)
@Column(table="account_kyc") -> account_kyc (secondary)
Read: SELECT a.*, k.pan, k.aadhaar_doc
FROM accounts a JOIN account_kyc k
ON a.account_id = k.account_id
Write: INSERT INTO accounts (...)
INSERT INTO account_kyc (...)
Key Takeaway
@SecondaryTable lets a single JPA entity map its fields across two or more physical tables that share the same primary key. Rajesh declares the main table with @Table and each extra table with @SecondaryTable, telling Hibernate the join column with @PrimaryKeyJoinColumn. Then he routes individual fields to the secondary table with @Column(table="..."), while fields without that attribute default to the primary table. On a read, Hibernate generates a join on the shared key and assembles one entity, and on a save it splits the inserts or updates across both tables.
At National Bank of India, the accounts table holds the everyday columns (name, balance) and a secondary account_kyc table holds the PAN and document blobs, yet the code deals with one Account entity. Pankaj reaches for this in legacy schemas where records were split, for separating hot from cold columns, and for isolating sensitive KYC data into a table with stricter access controls.
The senior cautions are that the secondary table is joined eagerly by default, so if the secondary columns are genuinely cold a lazy @OneToOne may be more efficient. He also ensures secondary columns are nullable so the entity loads even without a secondary row, and he remembers that @SecondaryTable is a mapping convenience, not a sharding tool, because both tables live in the same database.
Remember one sentence: @SecondaryTable maps one entity across multiple tables sharing the primary key, with fields routed via @Column(table="...") and Hibernate transparently joining on read and splitting on write.
---