skip to content

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