1. Basics of @ManyToOne
When you first hear “relationships in JPA”, you really want to rush off and start adding annotations, because annotations look like magic stickers: you stick one on — and everything works. But in the normal engineering world, a relationship starts not with @ManyToOne, but with a plain-language phrase from the domain. For example: “a product belongs to a category”, “an order item belongs to an order”, “an order item refers to a product”.
That phrase answers the main question: who refers to whom, and where that relationship is stored. In the end-to-end mini-shop project, this is super natural: a product does not exist in a vacuum, it lives in some category; an order item does not exist on its own, it is part of an order; and it also must point to a specific product. Our job is to express this so that it is equally clear to both the database and the Java code.
So that the relationship does not remain at the level of “well, we just agreed on it”, it must appear in the data. In the SQL world, that means a foreign key. In the JPA world, that means an association — a reference in an entity to another entity.
Foreign key in the database: where the “many” lives
Before looking at Java code, it is useful to make a short stop in the SQL world. Here everything is straightforward (and that is part of its beauty): if “many products belong to one category”, then the product table gets a column that stores a reference to the category table. In other words, the foreign key (FOREIGN KEY) usually lives on the “many” side, because it is the “many” that point to the “one”.
It looks roughly like this:
category (1) <--- product (many)
id id
category_id ---> category.id
# the foreign key lives in the "many" table (product)
And if we were writing DDL by hand, the logic would be roughly like this (simplified, without all the other columns):
create table category (
id bigserial primary key
);
create table product (
id bigserial primary key,
category_id bigint not null, -- FK column for the category
constraint fk_product_category
foreign key (category_id) references category(id) -- integrity constraint
);
The important thing here is to catch the idea: a relationship is not “inside Java”, the relationship materializes in tables as a column plus a foreign key constraint. And only after that does ORM/JPA let us work in Java not with a raw category_id, but with an object reference product.getCategory().
If you want to remember this with a very everyday analogy, think of category_id as a “label on a box”. The box is a product row. The label on it (category_id) says which “shelf” (category) this box belongs to. No magic — just storage discipline.
Association in JPA: a reference to an object
Now we move the same idea to the Java level. In JPA, the relationship between entities is called an association. In practice, this is most often an ordinary field that refers to another entity. So from Java’s point of view, it looks like “the Product object has a Category category field”.
The key point is: we store a reference to an object, not a separate categoryId as a primitive. Yes, in the database category_id is still stored. Yes, Hibernate will ultimately write the id value into that column. But in the entity model, we express the relationship through an object, because that makes the model expressive: the product has a category, not “some long”.
Here is a short “transformation chain” that is useful to keep in mind:
%% The same relationship, just in different "views": business -> SQL -> JPA
flowchart LR
A["Business: a product belongs to a category"] --> B["SQL: product.category_id -> category.id (FK)"]
B --> C["JPA: Product.category : Category (@ManyToOne)"]
We do not “invent” a new relationship in Java. We simply give Java a form for what must already exist in the tables.
@ManyToOne: reading the annotation from the field side
One of the most common beginner confusions sounds like this: “But the category contains many products, so why is @ManyToOne on the product, not @OneToMany?” And that is a completely fair confusion — because the brain likes to read the relationship “from above”, from the category. But the annotation is read “from below”, from the field.
The rule is simple, but it saves you from dozens of unnecessary questions: look at the field and read the phrase starting with the field type.
If Product has a Category category field, then the meaning is: “many” products (Product) can have one category (Category). That means @ManyToOne belongs on this field.
So:
- “A category has many products” — that is the business phrase.
- “A product points to one category” — that is what is actually stored in the product table (FK) and what is naturally described as a reference field.
And that is why it is convenient to start explaining relationships with @ManyToOne: it is closer to a real FK and easier to understand — there is an object and there is a reference to another object.
2. Examples of @ManyToOne mapping
A minimal @ManyToOne example on Product.category
Now let us look at how this will appear in our shop-data-jpa project. We already have the Category and Product entities. We add a category field to Product and mark it with @ManyToOne.
The smallest possible fragment (inside the Product class) can look like this:
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
@ManyToOne // many Product -> one Category (read from the field side)
@JoinColumn(name = "category_id") // FK column in the product table
private Category category;
This already expresses the main idea: Product contains a reference to Category, and that reference is stored in the category_id column.
But in a learning project, it is useful to be a bit more explicit: if, by the domain, a product must belong to a category, we want that to be visible in the code too. In mini-shop, we keep a simple baseline: a product always belongs to some category. Then we can add optional = false (this is the JPA level) and nullable = false (this is a hint about the column):
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
@ManyToOne(optional = false) // by the model: a product without a category is invalid
@JoinColumn(name = "category_id", nullable = false) // by the schema: NULL cannot be stored in the column
private Category category;
The difference between optional and nullable often seems redundant to beginners (“it is the same thing twice”), but the meaning is different. optional = false is about the model and ORM expectations: “an entity without a category is invalid.” nullable = false is about physical storage in the table: “NULL cannot be stored in the column.” Even if you are not building the “perfect” schema yet, this clarity helps read the code as a contract. For the current mini-shop, this is a workable rule: if the category changes, the product moves to another one instead of hanging around on its own.
Saving: a reference in Java turns into an FK in the table
From the outside, @ManyToOne looks like you just put one object inside another. And in Java — yes, it is just a reference. But for the persistence model, it means something very specific: when saving a product, Hibernate must write the category.id value into the product.category_id column.
Let us imagine a simplified “manual” code scenario. Suppose the category is already saved and has an id.
import java.math.BigDecimal;
// 1) Save the category first so that it gets an id
Category category = categoryRepository.save(new Category("electronics", "Electronics"));
// 2) Create a product
Product product = new Product("SKU-001", "Phone", new BigDecimal("499.00"));
// 3) Set the reference to the category (this is the future FK)
product.setCategory(category);
// 4) Save the product: Hibernate will write category.id into product.category_id
productRepository.save(product);
In this small fragment, the key line is product.setCategory(category). It looks like an ordinary assignment, but in meaning it is “preparing the future FK”: the product must now store a reference to the category, and the database will verify that such a category really exists.
If we try to say the same thing in “SQL voice”, it would look something like this:
insert into category (...) values (...) returning id; -- got the category id
insert into product (..., category_id) values (..., <category id>); -- wrote the FK into product
So the object reference in Java and the foreign key in the table are two forms of the same relationship. No new entity called “relationship” appears; the relationship simply becomes explicit and verifiable.
OrderItem: two independent @ManyToOnes
The catalog is a good example, but an even more “real-life” example is an order. In mini-shop, an order item (OrderItem) must know two things: which order it belongs to and which product it sells. In the database, this usually means two FK columns in the order_item table: one for customer_order, and another for product.
And in JPA, this is also expressed with two @ManyToOne fields in the same entity. That is normal and even expected: one object can refer to several other objects if the domain is structured that way.
A minimal fragment inside OrderItem can look like this:
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
@ManyToOne(optional = false) // an order item must always belong to a specific order
@JoinColumn(name = "customer_order_id", nullable = false) // FK to customer_order
private CustomerOrder customerOrder;
@ManyToOne(optional = false) // an order item must always point to a specific product
@JoinColumn(name = "product_id", nullable = false) // FK to product
private Product product;
Here the important thing is not to try to “merge” this into one relationship, not to try to be too clever, and not to try to store “everything in one id”. These are two different business relationships, so they are two different references and two different FK columns.
If you want to see this as a table diagram, the idea is:
order_item
id
customer_order_id ---> customer_order.id
product_id ---> product.id
# two independent FK columns: one for the order, another for the product
So OrderItem is the point where two contexts intersect: ordering and catalog. And @ManyToOne is the most direct and understandable way to express that.
Reference vs id: when each option is appropriate
Beginning developers often have a very understandable desire: “Why not avoid pulling a whole Category into Product, and just store categoryId: Long? That is simpler!” Psychologically, that seems “less magical”: here is a number, here is a column, everything is transparent.
The problem is that at the entity level this usually makes the model worse. The very idea of a relationship as part of the domain disappears, and what remains is just a “coordinate in the database”. Then any code starts to look like you are constantly manually reconstructing the object graph:
Long categoryId = product.getCategoryId(); // and then what?
Category category = categoryRepository.findById(categoryId).orElseThrow();
In the end, you either drag the repository into places where it is not needed, or you start building “helper” methods, or (the saddest case) you simply stop using the relationship altogether and begin writing queries “like in JDBC”, while still being on JPA.
The right compromise is usually this: keep a reference in the entity, but at the use-case boundary (for example, in a service method that creates a product) we can absolutely accept categoryId as an input parameter. Because the service is the place where we translate an “id from the outside world” into a “reference to an entity”.
So the natural logic is roughly this: the service received categoryId, found Category through the repository, set product.setCategory(category), and saved the product. The entity itself remains clean and domain-oriented: Product knows Category, not “some id”.
Checking the schema in the database: the column and the FK
When you add @ManyToOne and run the application in a mode where Hibernate manages the schema (the learning starter mode), you usually see two things: a column appears in the “many” table, and a foreign key appears. Even if you do not write DDL by hand, it is useful to be able to “predict” what should be produced.
For Product.category, you would expect:
- the category_id column in the product table;
- the foreign key product.category_id -> category.id.
For OrderItem, it is similar:
- the customer_order_id column in the order_item table and an FK to customer_order;
- the product_id column in the order_item table and an FK to product.
And here is why this matters: if you made a mistake in the model, the error usually becomes visible right here. For example, if you suddenly notice that the FK appeared “in the wrong place”, or the column was named strangely, or an extra join table appeared at all — that is a sign that your relationship is described differently than you thought. ORM is not telepathic; it does what you told it to do (sometimes very literally), not what you “meant”.
3. Typical mistakes when working with @ManyToOne
Mistake #1: confusing “one-to-many” in business terms with @ManyToOne in code.
A beginner often says: “a category is one-to-many, so Product should have @OneToMany.” But the annotation is read from the field side. If the field type is Category, then it is “many products to one category”, which means @ManyToOne. To avoid confusion, it is useful to say out loud: “many Products refer to one Category”.
Mistake #2: trying to put @ManyToOne on a collection.
Sometimes you want to write something like private List<Category> categories; and mark it with @ManyToOne, because “well, these are categories...”. But @ManyToOne is a reference field to one object. If you see List or any collection, that is already the other side of the story, and other annotations exist for it. In this lecture, we intentionally stick to the simple rule: @ManyToOne is placed on a field that is not a collection.
Mistake #3: expecting that “both sides of the relationship store the FK”.
In a real schema, there is usually one FK, and it lives where the “many” is. A product has category_id, a category usually has no product_id at all. In JPA, you should think the same way: the Product.category field is the main “relationship line”. If you try to force it so that “the category also stores a list of products and that also affects the FK”, you are almost guaranteed to get confused and end up with an inconsistent model.
Mistake #4: reducing the relationship to storing an id and constantly manually “reading” the relationship in code.
Storing categoryId instead of Category seems simpler at first, but very quickly starts spreading responsibility across the code: somewhere you forgot to load the category, somewhere you forgot to check existence, somewhere you started writing queries “around” the model. This is one of the first steps toward your JPA project turning into “JDBC with annotations”. It is better to keep the relationship as an object reference and leave work with the id to the service layer.
Mistake #5: not encoding domain invariants in the mapping and leaving the relationship “to chance”.
If, by the business rules, a product must have a category, and an order item must have an order and a product, then leaving these fields “optional” and allowing null is an invitation to weird bugs. In a learning project, it is useful to show the intent directly: optional = false and nullable = false. Then the model reads like a contract, not like “well, maybe this way, maybe another way”.
GO TO FULL VERSION