skip to content

Join Columns vs Join Tables

How an association is physically stored — a foreign-key column, a join table, or a shared primary key. Interviewers ask about the unidirectional @OneToMany join-table surprise because it quietly doubles your writes.

part ofHibernateoverview, primer and where to startread it →
on this pageshow

questions

5

In JPA, what does the @JoinColumn annotation declare, and which side of an association must carry it?

level: juniorimportance: must knowfreq 75%

answer

  1. @JoinColumn = the FK column
  2. Owning side holds the FK; inverse uses mappedBy
  3. @ManyToOne is always the owner
  4. Default name: field_referencedPk (customer_id)
  5. Duplicate column mapping → insertable/updatable = false

basics

~20 s

@JoinColumn names the foreign-key column that stores the association. It goes on the owning side — the side whose table holds the FK, which for @ManyToOne is always the many side. The inverse side uses mappedBy instead.

solid answer

~50 s

`@JoinColumn` declares the **foreign-key column** that physically stores an association, giving its column name and, optionally, the referenced column, nullability and FK constraint. It must sit on the **owning side** — the entity whose table actually holds the FK. For a `@ManyToOne`/`@OneToMany` pair that is always the *many* side, because the FK lives in the child table. The other side declares `@OneToMany(mappedBy = "parent")` and writes nothing to the database itself. If you omit it, JPA derives a default name from the field and the referenced primary key — for a field `customer` referring to `Customer.id`, that is `customer_id`. Naming it explicitly is normal practice so the schema doesn't depend on a field name. A classic bug: putting `@JoinColumn` on a `@OneToMany` **without** `mappedBy` — that is a valid but different mapping, and it changes what SQL Hibernate emits.

code

java · 14 lines
java
@Entity
class Order {
  @ManyToOne(fetch = FetchType.LAZY, optional = false)
  @JoinColumn(name = "customer_id", nullable = false)
  private Customer customer;
}

@Entity
class Customer {
  @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true)
  private List<Order> orders = new ArrayList<>();

  void addOrder(Order o) { orders.add(o); o.setCustomer(this); }
}

go deeper

for a junior

State that @JoinColumn names the FK column and lives on the owning side, with the Order/Customer example and the mappedBy counterpart.

for a middle

Add the default naming rule, nullable/optional interaction, the repeated-column fix with insertable/updatable, and why @ManyToOne must own.

for a senior

Discuss referencedColumnName trade-offs, composite-key @JoinColumns, FK constraint generation versus migration-owned schema, and the extra UPDATE of unidirectional @OneToMany + @JoinColumn.

for a principal

Frame it as schema ownership: mappings must describe a schema the migration tool owns, associations should reference primary keys, and bidirectional consistency is enforced by helper methods rather than convention.

## What a join column is Relational databases express relationships with a **foreign key**: a column in one table holding the primary-key value of a row in another. `@JoinColumn` is how JPA tells the provider which column plays that role for a given association attribute. ```java @Entity class Order { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "customer_id") private Customer customer; } ``` This says: the `orders` table has a `customer_id` column, and it references `Customer`'s primary key. ## Owning side A bidirectional association is one relationship represented by two Java references. The database has only one FK column, so exactly one side must be designated as the one whose state drives the SQL. That is the **owning side**, and it carries `@JoinColumn`. The other side is the **inverse** side and declares `mappedBy`, naming the field on the owner: ```java @Entity class Customer { @OneToMany(mappedBy = "customer") private List<Order> orders = new ArrayList<>(); } ``` `mappedBy` means "this collection is a read view of the FK maintained by `Order.customer`". Adding an `Order` to `customer.orders` without also setting `order.setCustomer(customer)` writes nothing — the row's FK stays null. This is the single most common JPA bug in a first project, and the standard remedy is a helper method on the parent that sets both directions. For `@ManyToOne` the many side is *necessarily* the owner: the FK can only live in the child table, because one parent row cannot hold many child keys in a single column. ## Useful attributes - **`name`** — the column name. Default is `<fieldName>_<referencedPkColumn>`, e.g. `customer_id`. Rely on the default only if you also control naming strategy; explicit is safer. - **`referencedColumnName`** — which column of the *target* table the FK points at. Defaults to the target's primary key. Set it when you must reference a different unique column, for example a natural key like `country_code`. This works but has real costs: it requires a unique constraint on the target column, it can complicate identity and caching, and it forces Hibernate to key the association on something other than the PK. Prefer PK references unless integrating with a legacy schema. - **`nullable`** — used for DDL generation and, importantly, tells Hibernate whether a to-one association is optional. Combined with `@ManyToOne(optional = false)` it also enables a proxy optimisation and inner rather than outer joins. - **`insertable`/`updatable`** — set both `false` when the same column is also mapped as a plain field (a common pattern for exposing `customerId` alongside `customer`). Without it you get a "repeated column in mapping" error. - **`foreignKey = @ForeignKey(...)`** — controls the generated FK constraint name, or disables it with `ConstraintMode.NO_CONSTRAINT`. Only relevant when the provider generates DDL; in a migration-managed schema the migration tool owns constraints. ## Composite keys When the target has a composite primary key you need one column per key part, using `@JoinColumns`: ```java @ManyToOne @JoinColumns({ @JoinColumn(name = "order_id", referencedColumnName = "order_id"), @JoinColumn(name = "line_no", referencedColumnName = "line_no") }) private OrderLine line; ``` With composite keys `referencedColumnName` becomes mandatory, because ordering alone cannot disambiguate the pairs. ## The @OneToMany + @JoinColumn variant Putting `@JoinColumn` on a `@OneToMany` **without** `mappedBy` is legal and means something specific: a unidirectional one-to-many where the FK still lives in the child table, but the *parent* is responsible for maintaining it. Hibernate implements that by inserting the child with a null FK and then issuing a separate `UPDATE` to set it — an extra statement per child, and it requires the FK column to be nullable. It is better than the join-table default the spec would otherwise apply, but a bidirectional mapping with `mappedBy` is cleaner still. ## How to sanity-check a mapping Read the mapping and answer: which table holds the FK? Whichever entity maps to that table must have `@JoinColumn`; the other must have `mappedBy`. If both sides have `@JoinColumn`, you have accidentally mapped two independent associations onto one relationship and updates will fight each other.

  • A developer adds a child to the parent's mappedBy collection and commits, but the child's FK column stays null. Why?
    Because the collection is the inverse side: Hibernate reads it but never writes from it. Only the owning side's field — the child's @ManyToOne reference — is translated into the FK value in INSERT/UPDATE statements. The fix is to set both directions, conventionally via an addChild helper on the parent that adds to the collection and sets the back-reference.
  • When would you set referencedColumnName, and what does it cost?
    When the FK must point at a unique column other than the target's primary key, typically in a legacy schema keyed on a natural code. The cost is that the target column needs a unique constraint, association resolution no longer goes through the primary key so it interacts awkwardly with identity and caching, and some provider optimisations do not apply. Prefer referencing the PK unless the schema forces otherwise.

The FK column is the one string tying two boxes together; @JoinColumn labels the string, mappedBy says "the other box is holding it".

saying these in an interview costs you the question

  • Putting @JoinColumn on both sides of a bidirectional association
  • Expecting the mappedBy collection to persist the FK on its own
  • Believing @OneToMany with @JoinColumn and no mappedBy is the same as mappedBy (it emits extra UPDATEs)
  • Mapping the same column as both a @JoinColumn and a basic field without insertable=false/updatable=false
  • Thinking @JoinColumn(nullable=false) enforces anything at runtime beyond DDL and optionality hints

context

open as a page

How do you map a many-to-many association in JPA with @JoinTable, and why do experienced teams often replace @ManyToMany with an explicit link entity?

level: middleimportance: must knowfreq 68%

basics

~20 s

@ManyToMany with @JoinTable maps a link table of two FK columns: joinColumns points at the owner, inverseJoinColumns at the target; the other side uses mappedBy. Teams replace it with a link entity when the relationship needs its own columns or stable identity.

open as a page

A developer maps a one-to-many collection in JPA with only @OneToMany and no mappedBy or @JoinColumn. What schema does the provider generate, why is that surprising, and what are the alternatives?

level: middleimportance: should knowfreq 47%

basics

~20 s

The provider creates a join table instead of a foreign key in the child table — the default for a unidirectional one-to-many. Alternatives: add @JoinColumn to keep the FK in the child (costs extra UPDATEs), or make it bidirectional with mappedBy, which is cleanest.

open as a page

How does @MapsId let a JPA @OneToOne share a primary key with its owner, and what problem does that solve compared with a plain @OneToOne @JoinColumn?

level: seniorimportance: should knowfreq 42%

basics

~20 s

@MapsId makes the child's primary key be the foreign key to the parent — one column doing both jobs. It removes the separate surrogate key and unique constraint, and it fixes the lazy one-to-one problem on the parent side by making the child's ID known in advance.

open as a page

What does the referencedColumnName attribute of JPA's @JoinColumn do, when would you set it, and what does it cost?

level: seniorimportance: nice to knowfreq 28%

basics

~20 s

It names which column of the target table the foreign key points at, instead of the default primary key. Use it for legacy schemas keyed on a natural unique column. Cost: the target column needs a unique constraint, and the association no longer resolves through the primary key.

open as a page