Definition
- @ManyToOne – placed on the child entity (Trade). Points to the parent (Account). This is the owning side that controls the FK column.
- @JoinColumn – specifies the FK column name in the child table:
@JoinColumn(name = "account_id"). - @OneToMany(mappedBy) – placed on the parent entity (Account). Creates the inverse side for navigation.
mappedBy = "account"tells JPA the Trade.account field owns the relationship. - Owning side – the side that controls SQL generation. Only changes to the owning side are persisted.
- Helper methods –
addTrade()andremoveTrade()synchronize both sides of the bidirectional relationship.
At National Bank of India, Account (parent) has many Trades (children). In the database, the trades table has an account_id FK column pointing to accounts. In Java, Trade has @ManyToOne Account account (the trade knows which account it belongs to – owning side). Account has @OneToMany List (the account can see its trades – inverse side). mappedBy tells JPA: "Trade.account is the real owner – I am just the mirror."
Understanding the Concepts
What is the owning side vs inverse side?
The owning side is the entity that has the FK column in its database table. In a one-to-many relationship between Account and Trade, the trades table has the account_id FK column. Therefore Trade is the owning side. JPA reads only the owning side for SQL generation.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
// CHILD (Trade): OWNING SIDE – has the FK column
@Entity
@Table(name = "trades")
public class Trade {
@Id
@Column(name = "trade_id")
private String tradeId;
@ManyToOne(fetch = FetchType.LAZY) // Many trades belong to ONE account
@JoinColumn(name = "account_id", nullable = false) // FK column
private Account account;
private String symbol;
@Column(precision = 15, scale = 2)
private BigDecimal amount;
// getters, setters
}
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
// PARENT (Account): INVERSE SIDE – no FK column
@Entity
@Table(name = "accounts")
public class Account {
@Id
@Column(name = "account_id")
private String accountId;
@OneToMany(mappedBy = "account", cascade = CascadeType.ALL,
orphanRemoval = true)
private List<Trade> trades = new ArrayList<>();
// mappedBy = "account" → Trade.account field owns this relationship
// cascade = ALL → persist/merge/remove propagate to trades
// orphanRemoval = true → removing trade from list deletes it
@Column(name = "holder_name")
private String holderName;
@Column(precision = 15, scale = 2)
private BigDecimal balance;
// getters, setters
}
Why are helper methods critical?
Helper methods synchronize both sides of the bidirectional relationship. Without them, the FK column can be NULL because JPA only reads the owning side.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "accounts")
public class Account {
@Id
@Column(name = "account_id")
private String accountId;
@OneToMany(mappedBy = "account", cascade = CascadeType.ALL,
orphanRemoval = true)
private List<Trade> trades = new ArrayList<>();
private String holderName;
private BigDecimal balance;
// HELPER: synchronize BOTH sides
public void addTrade(Trade trade) {
trades.add(trade);
trade.setAccount(this); // set the owning side!
}
public void removeTrade(Trade trade) {
trades.remove(trade);
trade.setAccount(null); // clear the owning side!
}
}
package com.bank.service;
import com.bank.model.Account;
import com.bank.model.Trade;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class TradeCreationService {
private final AccountRepository accountRepo;
public TradeCreationService(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional
public void addTradeToAccount(String accountId) {
Account account = accountRepo.findById(accountId).orElseThrow();
Trade trade = new Trade();
trade.setTradeId("TRD-099");
trade.setSymbol("RELIANCE");
trade.setAmount(new BigDecimal("245050.00"));
// CORRECT: use helper that sets BOTH sides
account.addTrade(trade);
// trade.setAccount(account) called inside helper
// INSERT INTO trades (..., account_id) VALUES (..., 'SAV2001')
}
}
What happens if you only set one side?
Setting only the inverse side (parent's list) without setting the owning side (child's FK) results in a NULL FK or a constraint violation.
package com.bank.service;
import com.bank.model.Account;
import com.bank.model.Trade;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class TradeMistakeService {
private final AccountRepository accountRepo;
public TradeMistakeService(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional
public void wrongApproach(String accountId) {
Account account = accountRepo.findById(accountId).orElseThrow();
Trade trade = new Trade();
trade.setTradeId("TRD-100");
trade.setSymbol("HDFC");
trade.setAmount(new BigDecimal("150000.00"));
// WRONG: only set the inverse side
account.getTrades().add(trade);
// trade.getAccount() is still null!
// JPA reads the owning side (Trade.account) for SQL generation
// INSERT INTO trades (..., account_id) VALUES (..., NULL)
// Constraint violation: account_id cannot be NULL!
}
@Transactional
public void correctApproach(String accountId) {
Account account = accountRepo.findById(accountId).orElseThrow();
Trade trade = new Trade();
trade.setTradeId("TRD-100");
trade.setSymbol("HDFC");
trade.setAmount(new BigDecimal("150000.00"));
// CORRECT: use helper that sets both sides
account.addTrade(trade);
// INSERT INTO trades (..., account_id) VALUES (..., 'SAV2001')
}
}
What does @JoinColumn configure?
@JoinColumn specifies the FK column details on the owning side.
package com.bank.model;
import jakarta.persistence.*;
@Entity
@Table(name = "trades")
public class Trade {
@Id
private String tradeId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(
name = "account_id", // FK column name in trades table
nullable = false, // NOT NULL constraint
foreignKey = @ForeignKey(name = "fk_trade_account") // custom FK name
)
private Account account;
private String symbol;
private java.math.BigDecimal amount;
}
What is unidirectional vs bidirectional?
Three patterns exist for one-to-many relationships. Bidirectional with helper methods is the recommended approach.
// Pattern 1: UNIDIRECTIONAL @ManyToOne (Trade → Account)
// Trade has @ManyToOne Account account
// Account does NOT have @OneToMany List<Trade> trades
// Use when: you never need account.getTrades()
// Pattern 2: BIDIRECTIONAL (Trade ↔ Account) – RECOMMENDED
// Trade has @ManyToOne Account account (owning)
// Account has @OneToMany(mappedBy) List<Trade> trades (inverse)
// Use when: you need both account.getTrades() AND trade.getAccount()
// Pattern 3: UNIDIRECTIONAL @OneToMany (Account → trades)
// Account has @OneToMany List<Trade> trades (WITHOUT @ManyToOne on Trade)
// AVOID: JPA creates a join table (accounts_trades) instead of a FK!
// Less efficient, harder to understand
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
// AVOID: unidirectional @OneToMany without @ManyToOne
@Entity
@Table(name = "accounts")
public class AccountBad {
@Id
private String accountId;
@OneToMany // NO mappedBy, NO @ManyToOne on Trade
private List<Trade> trades = new ArrayList<>();
// JPA creates: accounts_trades join table!
// INSERT INTO accounts_trades (account_id, trade_id) VALUES (?, ?)
// Less efficient than a simple FK column
}
What are cascade and orphanRemoval?
cascade propagates operations from parent to children. orphanRemoval deletes children that are removed from the parent's collection.
package com.bank.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "accounts")
public class Account {
@Id
private String accountId;
@OneToMany(mappedBy = "account",
cascade = CascadeType.ALL, // persist/merge/remove propagate
orphanRemoval = true) // remove from list = DELETE from DB
private List<Trade> trades = new ArrayList<>();
private String holderName;
private java.math.BigDecimal balance;
public void addTrade(Trade trade) {
trades.add(trade);
trade.setAccount(this);
}
public void removeTrade(Trade trade) {
trades.remove(trade);
trade.setAccount(null);
// orphanRemoval: DELETE FROM trades WHERE trade_id = ?
}
}
package com.bank.service;
import com.bank.model.Account;
import com.bank.model.Trade;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class CascadeDemo {
private final AccountRepository accountRepo;
public CascadeDemo(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional
public void cascadePersist() {
Account account = new Account();
account.setAccountId("SAV2001");
account.setHolderName("Rajesh Kumar");
account.setBalance(new BigDecimal("500000.00"));
Trade t1 = new Trade("TRD-001", "RELIANCE", new BigDecimal("100000"));
Trade t2 = new Trade("TRD-002", "HDFC", new BigDecimal("200000"));
account.addTrade(t1);
account.addTrade(t2);
accountRepo.save(account);
// CascadeType.ALL propagates persist:
// INSERT INTO accounts VALUES ('SAV2001', 'Rajesh Kumar', 500000)
// INSERT INTO trades VALUES ('TRD-001', 'RELIANCE', 100000, 'SAV2001')
// INSERT INTO trades VALUES ('TRD-002', 'HDFC', 200000, 'SAV2001')
// ONE save() call persists parent + all children!
}
}
What should a senior engineer know?
Senior engineers should understand these nuances about entity relationships:
- Fetch strategy interaction.
@ManyToOne(fetch = LAZY)prevents N+1 at the entity level, but JPQLSELECT t FROM Trade tstill fires N+1 if you accesst.getAccount()in a loop. UseJOIN FETCHor@EntityGraphto load relationships in a single query. - Batch size for collections.
@BatchSize(size = 50)on@OneToManyloads children in batches of 50 instead of one query per parent. This is a Hibernate-specific optimization that mitigates N+1 without changing the query. - equals and hashCode. In bidirectional relationships, avoid using the collection side in
equals()/hashCode(). Use the business key or the database ID to prevent infinite recursion and Hibernate proxy issues. - Ordering collections. Use
@OrderBy("amount DESC")on@OneToManyfor database-level ordering or@OrderColumnfor a persistent index column. Do not rely on insertion order. - Bidirectional toString danger. If both Account and Trade include each other in
toString(), you get aStackOverflowError. Exclude the parent from the child'stoString()or use@ToString.Excludewith Lombok.
package com.bank.model;
import jakarta.persistence.*;
import java.util.Objects;
@Entity
@Table(name = "trades")
public class Trade {
@Id
private String tradeId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "account_id")
private Account account;
// Use business key for equals/hashCode, NOT the collection
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Trade t)) return false;
return tradeId != null && tradeId.equals(t.tradeId);
}
@Override
public int hashCode() {
return Objects.hashCode(tradeId);
}
}
What are common mistakes with relationships?
The three most common mistakes are: not setting both sides, forgetting fetch = FetchType.LAZY on @ManyToOne, and using unidirectional @OneToMany without @ManyToOne.
package com.bank.model;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "trades")
public class Trade {
@Id
private String tradeId;
// WRONG: default EAGER on @ManyToOne
// @ManyToOne
// @JoinColumn(name = "account_id")
// private Account account;
// 200 trades = 200 Account SELECTs (N+1)
// CORRECT: explicit LAZY
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "account_id", nullable = false)
private Account account;
private String symbol;
private BigDecimal amount;
}
Visual Overview
Bidirectional @OneToMany / @ManyToOne:
accounts table trades table
┌───────────────┐ ┌───────────────────┐
│ account_id PK │─────────┤ trade_id PK │
│ holder_name │ │ symbol │
│ balance │ ┌────┤ account_id FK │
└───────────────┘ │ │ amount │
│ └───────────────────┘
│
Java mapping: │
Account (INVERSE) │ Trade (OWNING)
@OneToMany(mappedBy) │ @ManyToOne + @JoinColumn
List<Trade> trades ───┘ Account account
Helper method flow:
account.addTrade(trade)
│
├── trades.add(trade) (inverse side)
└── trade.setAccount(this) (owning side → FK set!)
Key Takeaway
JPA connects two tables using @ManyToOne on the child entity (owning side, controls the FK column) and @OneToMany(mappedBy) on the parent entity (inverse side, navigation only). @JoinColumn(name = "account_id") specifies the FK column. mappedBy = "account" tells JPA that the Trade.account field owns the relationship. JPA reads only the owning side for SQL generation.
At National Bank of India, Trade has @ManyToOne(fetch = LAZY) Account account with @JoinColumn(name = "account_id"). Account has @OneToMany(mappedBy = "account", cascade = ALL, orphanRemoval = true). The addTrade() helper synchronizes both sides: it adds to the list (inverse) and calls trade.setAccount(this) (owning). Without the helper, the FK column is NULL because JPA only reads the owning side.
Three relationship patterns exist: unidirectional @ManyToOne (simplest, child knows parent), bidirectional @ManyToOne + @OneToMany(mappedBy) (both navigate, standard), and unidirectional @OneToMany without @ManyToOne (creates an unnecessary join table, avoid it). Always use bidirectional with helper methods for production code, and always set fetch = FetchType.LAZY on @ManyToOne to prevent N+1 queries.
Remember one sentence: the child's @ManyToOne with @JoinColumn owns the FK, the parent's @OneToMany(mappedBy) is just a mirror, and helper methods must set both sides.
---