Back to Entity Mapping: Relationships
Q2Question 2 of 5

What is @OneToMany / @ManyToOne / @JoinColumn / mappedBy?

Key Concepts
  • The MANY side (child) with @ManyToOne @JoinColumn owns the foreign key and is the owning side that drives SQL.
  • The ONE side (parent) with @OneToMany(mappedBy=...) is the inverse side; mappedBy names the owning field on the child, not a column.
  • Only the owning side drives the foreign-key SQL; changes to the inverse collection alone leave the FK null.
  • Synchronization helpers (addTrade/removeTrade) update both sides in one call to prevent null FK bugs.
  • Always override @ManyToOne default fetch from EAGER to LAZY to avoid accidental over-fetching.
  • Use JOIN FETCH queries to load the parent with children in one query, avoiding the N+1 problem.

Definition

The MANY side with @ManyToOne @JoinColumn owns the foreign key; the ONE side with @OneToMany(mappedBy=...) is the inverse, and mappedBy names the owning field on the child, not a column.
  • Owning side – the entity whose table physically holds the foreign-key column; in a one-to-many, this is the MANY (child) side with @ManyToOne and @JoinColumn.
  • Inverse side – the entity that does not hold the FK; it carries @OneToMany(mappedBy=...) and does not drive any SQL on its own.
  • @JoinColumn – specifies the name of the actual foreign-key column on the owning side's table.
  • mappedBy – on the inverse side, names the FIELD on the owning entity that holds the @ManyToOne reference; it is NOT a column name.
  • Synchronization helpers – methods like addTrade/removeTrade that update both the collection and the owning reference to prevent null FK bugs.

An account at National Bank of India has many trades. The trades table is where the account_id stamp physically lives on each row, so a trade owns the link and carries @ManyToOne @JoinColumn(account_id). The account only knows about its trades through @OneToMany(mappedBy="account"), like a customer file that references its receipts but where the actual account number is printed on each receipt, not the other way around.

Understanding the Concepts

What is the owning side in a bidirectional relationship?

A foreign key is a single column on ONE physical table. In an account-to-trades relationship, the trades table has an account_id column. There is no column on the accounts table pointing at trades. So in JPA terms, the child (Trade) is the natural OWNER of the relationship because its table physically stores the foreign key.

JAVA
package com.bank.model;

import jakarta.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "trades")
public class Trade {

    @Id
    @Column(name = "trade_id")
    private String tradeId;

    @ManyToOne(fetch = FetchType.LAZY)    // override default EAGER
    @JoinColumn(name = "account_id")      // the actual FK column on trades table
    private Account account;

    @Column(name = "amount", precision = 15, scale = 2)
    private BigDecimal amount;

    @Column(name = "status")
    private String status;

    // getters, setters
}
TEXT
Trade is the OWNING side because:
- trades table has the account_id column (the FK)
- @JoinColumn tells Hibernate the exact FK column name
- Hibernate reads this field to decide what value to write into account_id

What is the inverse side with mappedBy?

The parent entity (Account) references a collection of trades with @OneToMany(mappedBy="account"). The value of mappedBy is the NAME OF THE FIELD on the child entity that owns the relationship. It is NOT a column name. mappedBy tells Hibernate: I am not the owner; the foreign key lives over there.

JAVA
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;

    @Column(name = "holder_name", nullable = false)
    private String holderName;

    @Column(name = "balance", precision = 15, scale = 2)
    private BigDecimal balance;

    @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Trade> trades = new ArrayList<>();

    // getters, setters, helpers
}
TEXT
Account is the INVERSE side because:
- accounts table has NO column pointing at trades
- mappedBy = "account" points to the Trade.account field (the owning side)
- Hibernate does NOT generate SQL from changes to this collection alone

Why must you keep both sides in sync?

Because only the owning side drives the foreign-key SQL, if you add a trade only to the parent's collection and forget to set the child's account field, Hibernate writes a row with a NULL account_id. The fix is synchronization helpers that update both sides at once.

JAVA
// WRONG: only updating the inverse side
package com.bank.service;

import com.bank.model.Account;
import com.bank.model.Trade;

public class WrongExample {
    public void addTrade(Account account, Trade trade) {
        account.getTrades().add(trade);   // inverse side only
        // trade.account is still null!
        // Hibernate writes: INSERT INTO trades (trade_id, amount, account_id) VALUES ('T1', 1000, NULL)
    }
}
JAVA
// CORRECT: synchronization helper updates both sides
package com.bank.model;

import java.util.ArrayList;
import java.util.List;

public class Account {
    // ... fields ...

    private List<Trade> trades = new ArrayList<>();

    public void addTrade(Trade trade) {
        trades.add(trade);           // inverse side (in-memory navigation)
        trade.setAccount(this);      // OWNING side (what Hibernate persists)
    }

    public void removeTrade(Trade trade) {
        trades.remove(trade);
        trade.setAccount(null);      // orphanRemoval -> DELETE on flush
    }
}
TEXT
CORRECT result:
INSERT INTO trades (trade_id, amount, account_id) VALUES ('T1', 1000, 'SAV2001')
account_id is set because trade.setAccount(this) was called.

What does @JoinColumn control?

@JoinColumn specifies the name of the foreign-key column on the owning side's table. It also controls whether the column is nullable, unique, insertable, or updatable. Without @JoinColumn, Hibernate uses a default name derived from the field name and the target entity's id column.

JAVA
package com.bank.model;

import jakarta.persistence.*;

@Entity
public class Trade {

    @Id
    private String tradeId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(
        name = "account_id",          // FK column name
        nullable = false,              // NOT NULL constraint
        foreignKey = @ForeignKey(name = "fk_trade_account")  // named FK constraint
    )
    private Account account;
}
TEXT
Generated DDL:
ALTER TABLE trades
  ADD CONSTRAINT fk_trade_account
  FOREIGN KEY (account_id) REFERENCES accounts(account_id);

How does fetch type affect the relationship?

@ManyToOne defaults to EAGER, meaning loading a trade always loads its account in a second query. This is almost always wrong in production. Override it to LAZY. @OneToMany already defaults to LAZY. When you genuinely need the parent with its children, use a JOIN FETCH query.

JAVA
// WRONG: @ManyToOne default EAGER causes unnecessary fetches
@ManyToOne  // default FetchType.EAGER
@JoinColumn(name = "account_id")
private Account account;  // loading ANY trade also loads the full Account

// CORRECT: override to LAZY
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "account_id")
private Account account;  // Account loaded only when accessed
JAVA
package com.bank.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import com.bank.model.Account;
import java.util.Optional;

public interface AccountRepository extends JpaRepository<Account, String> {

    @Query("SELECT a FROM Account a LEFT JOIN FETCH a.trades WHERE a.accountId = :id")
    Optional<Account> findWithTrades(String id);
    // One query fetches Account + all its Trades -- no N+1
}

What is a unidirectional alternative?

If the parent does not need to navigate to its children, you can put @OneToMany @JoinColumn on the parent without any field on the child. But this is less efficient because Hibernate issues extra UPDATE statements to set the foreign key after inserting children.

JAVA
// Unidirectional @OneToMany (less efficient)
package com.bank.model;

import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
public class Account {

    @Id
    private String accountId;

    @OneToMany
    @JoinColumn(name = "account_id")  // no mappedBy, no field on Trade
    private List<Trade> trades = new ArrayList<>();
    // Hibernate: INSERT trade (NULL FK), then UPDATE trade SET account_id = 'SAV2001'
    // Extra UPDATE per child row -> bidirectional with mappedBy is preferred
}

What are common mistakes?

The most common mistake is adding a child only to the parent's collection and forgetting to set the owning reference. A second mistake is confusing mappedBy with a column name.

JAVA
// WRONG: mappedBy points to a column name instead of a field name
@OneToMany(mappedBy = "account_id")   // WRONG! "account_id" is a column, not a field
private List<Trade> trades;

// CORRECT: mappedBy points to the Java field name on Trade
@OneToMany(mappedBy = "account")       // CORRECT! "account" is the field on Trade
private List<Trade> trades;

What should a senior engineer know?

A senior engineer always overrides @ManyToOne from EAGER to LAZY, uses JOIN FETCH when the association is needed, applies cascade only to genuinely owned children, and implements equals/hashCode on a stable business key rather than the generated primary key for Set-based collections.

JAVA
package com.bank.model;

import jakarta.persistence.*;
import java.util.Objects;

@Entity
public class Trade {

    @Id
    private String tradeId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id")
    private Account account;

    // equals/hashCode on stable business key (tradeId), not generated PK
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Trade other)) return false;
        return tradeId != null && tradeId.equals(other.tradeId);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(tradeId);
    }
}

Visual Overview

TEXT
  Bidirectional OneToMany / ManyToOne
  ============================================

  accounts table                    trades table
  +------------------+              +--------------------+
  | account_id (PK)  |              | trade_id (PK)      |
  | holder_name      |              | amount             |
  | balance          |              | account_id (FK) ---+----> accounts.account_id
  +------------------+              | status             |
                                    +--------------------+

  Account (INVERSE)                 Trade (OWNING)
  @OneToMany(mappedBy="account")    @ManyToOne(fetch=LAZY)
  List<Trade> trades                @JoinColumn(name="account_id")
                                    Account account

  Who drives the SQL?
  ============================================
  account.getTrades().add(trade)    -> NO SQL generated (inverse side)
  trade.setAccount(account)         -> INSERT/UPDATE with account_id (owning side)

  Correct: call BOTH
  account.addTrade(trade)
    -> trades.add(trade)            (inverse, for navigation)
    -> trade.setAccount(this)       (owning, for SQL)

Key Takeaway

In a bidirectional one-to-many relationship there is always an owning side and an inverse side, and the distinction comes straight from the database. For an account and its trades at National Bank of India, the trades table holds the account_id column, so the Trade entity is the owning side and carries @ManyToOne with @JoinColumn naming that FK column. The Account entity is the inverse side and carries @OneToMany with mappedBy set to the name of the account field on Trade, not a column name. The inverse side never drives SQL on its own; Hibernate reads the foreign key only from the owning side.

The classic bug this causes is adding a child only to the parent's collection and forgetting to set the child's reference back to the parent, which leaves the foreign key null. Rajesh prevents that with synchronization helpers: addTrade and removeTrade update both the collection and the child's owning reference in one call. Because the relationship is configured with cascade ALL and orphan removal, saving the account once cascades inserts to all new trades, and removing a trade from the collection deletes that row.

The production decisions Pankaj always makes are to override @ManyToOne from EAGER to LAZY, to use JOIN FETCH when the association is needed, to apply cascade only to genuinely owned children, and to base equals/hashCode on a stable business key like the trade id rather than the generated primary key.

Remember one sentence: the MANY side owns the FK with @ManyToOne @JoinColumn, the ONE side is the inverse with @OneToMany(mappedBy), and only the owning side drives the SQL.

---

Share
Use ← → arrow keys to navigate