Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Fundamentals › JPA

Associations

What is an association, and what does @ManyToOne do for you?

An order needs to know who placed it. In tables, the order row writes the number of the member in one column (a foreign key) and points to the member. In Java objects, Order points directly to a Member object through a field. When an entity points to another entity like this, it is an association. If you write @ManyToOne above that field, JPA connects the two. When it saves, it takes the id out of the Member object and puts it in the foreign key column, and when it reads, it uses that number to find the Member and puts it in the field. So you can just follow the objects, as in order.getMember().getName(). This lab runs the same code, which saves an order and reads it again, on a database where (1, Kim) is already in the member table, and changes only the field declaration. You see what happens if you remove @ManyToOne, and what changes if you keep only a number (Long memberId) in the field instead of an object.

Checked against real execution · Compiled the source of all 4 combinations on Hibernate 7.4.5 and H2 2.4.240, started JPA for real, saved and read again, and checked against the results

Concepts in this lab

Association
An association is a relationship in which one entity points to another entity. In tables, a row writes the primary key value of a row of another table in one column (a foreign key) and points to that row, and in Java objects, a field of one entity points directly to an object of another entity. JPA connects these two expressions. When it saves, it takes the primary key out of the object it points to and puts it in the foreign key column, and when it reads, it uses the foreign key value to find the other entity and puts it in the field. The JPA specification (which calls this a relationship) says to put one of @OneToOne, @OneToMany, @ManyToOne, and @ManyToMany on a field or property that shows an association. If you keep only a number in the field instead of the other side's object, it is not an association but an ordinary number field, and you have to find the other entity directly with that number.
@ManyToOne
@ManyToOne is an annotation that you put on a field to show that "this field points to one other entity, and many of this entity can point to the same one". A typical case is many orders pointing to one member. The type of the target is found from the type of the field. In tables, the table of the entity that has this field gets a foreign key column that holds the primary key value of the other side, and if you do not set the column name separately, it becomes the name that joins the field name, an underscore, and the name of the other side's primary key column (member_id for a member field of the type Member). If a field of another entity type has no such association mark, Hibernate 7.4.5 cannot find a way to put that field in a column and throws an error while starting.
@JoinColumn
@JoinColumn is an annotation that specifies the foreign key column of an association, and you use it together with an association mark such as @ManyToOne. You write the column name in name, and if you do not write it, the name becomes the name that joins the field name, an underscore, and the name of the other side's primary key column. For example, if you put @JoinColumn(name = "buyer_id"), the foreign key column becomes buyer_id, not member_id. What changes is the column name in the table, and the way of following objects in the entity code does not change.
Primary key, unique key, foreign key
A primary key (PK) is the column that uniquely identifies each row of a table. Its values cannot be duplicated or empty, and a table has only one. A unique key is a unique constraint, which prevents duplicate values, and a table can have several. A foreign key (FK) is a column that references the primary key (or a unique key) of another table. It guarantees that the referenced row really exists.

Try these

Running with the default settings

  1. When the program starts, it tells JPA (here, Hibernate) about the Order and Member classes, and JPA reads the classes. The line that registers them and the settings that connect to the database are shortened on the screen. JPA looks at the member field of Order, and its type is Member, which is another entity, and @ManyToOne is above it. So JPA accepts this field as "a field that points to another entity".
  2. JPA makes the orders table in the H2 database following this mapping. The columns are id, member_id, and column member_id holds the id of Member. A foreign key constraint is also made, so the value of this column must be a number that exists in the id of the member table. The column name member_id joins the field name member, an underscore, and the name of the column it points to, id (the default rule of the JPA specification when @JoinColumn is not written). This lab is set so that JPA creates the table when it starts (hibernate.hbm2ddl.auto=create-drop), and we did not write CREATE TABLE. The member table already has the row (1, Kim) that the lab put in ahead of time. This is a preparation that does not appear on the screen.
  3. Member kim = em.find(Member.class, 1L): JPA reads the row with id 1 from the member table and makes the Member object kim. In this run, 1 SELECT was sent. kim is an ordinary object that has id 1 and the name Kim.
  4. em.persist(new Order(10L, kim)): it creates an order object and puts the kim object in the member field, not the number 1. While this order is passed to JPA and the saving finishes, 1 INSERT is sent. In this run, the statement that Hibernate 7.4.5 made is "insert into orders (member_id,id) values (?,?)". JPA took the id 1 out of the kim object and put it in the place of member_id. We did not take out the number and write it. A row (id 10, member_id 1) now exists in the orders table.
  5. Order o = em2.find(Order.class, 10L): it reads the order with em2, which is a different em from the one used for saving. The separately made em2 does not know the earlier objects, so it reads from the database. 1 SELECT was sent on this line, and that statement read not only the orders table but the member table too. JPA uses the value 1 of member_id in the row to find the Member and puts it in the member field of o. We did not call a separate find to look for the Member.
  6. System.out.println(o.getMember().getName()): o.getMember() is the Member object that JPA put in, and getName() returns the name of that object. Kim is printed on the screen. We did not handle a number. We only followed the objects.

Result of this run

Code

@Entity
@Table(name = "orders")
public class Order {
    @Id
    private Long id;
    // Association: point to the member who placed this order as an object
    @ManyToOne
    private Member member;
}

// Calling side: save an order, read it again, and look at the name of the member who placed it
Member kim = em.find(Member.class, 1L);
em.persist(new Order(10L, kim));
// After saving (commit) is done, read it again from the database with a new EntityManager (em2)
Order o = em2.find(Order.class, 10L);
System.out.println(o.getMember().getName());

Assumptions of this model

This was checked against real execution on Hibernate 7.4.5 and H2 2.4.240 (in-memory database). Spring was not used, and Hibernate was started directly in code (the Member and Order classes were registered in a Configuration, without persistence.xml). All 4 combinations were checked: the same text as the Order source on the screen (with only the package and import lines, a constructor, and getters added) was compiled together with the Member source, and a new Java process was started. What the screen shortens is the package and import lines, the constructor and getters of Order, the Member entity (an ordinary entity with the fields id and name), and the lines that get the EntityManagers em and em2. The "calling side" on the screen is really code inside main, and the part that saves with em is between the lines that start and end (commit) a transaction. Those lines are shortened on the screen. The reading line was run after saving finished, with the newly made em2. If you read with the same EntityManager, it gives back the object you just saved from memory, not from the database, and the reading step could not be shown. (1, Kim) in the member table was put in directly with JDBC right after JPA started and made the tables. The column names are set with a rule that puts an _ before each capital letter of a field name (hibernate.physical_naming_strategy=PhysicalNamingStrategySnakeCaseImpl). This is the same class that the spring-boot-hibernate jar of Spring Boot 4.1.1 points to as its default naming rule (this was checked by the class name inside the jar, and Spring Boot was not actually started), and what the column name becomes without this setting was not checked. What was checked against the run is whether JPA fails while starting and the kind and first line of the message of the exception, the tables and columns in H2 right after the start and after the run (INFORMATION_SCHEMA.COLUMNS), the foreign key constraint (INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS and KEY_COLUMN_USAGE), the SQL caught with StatementInspector (the number and text of SELECT and INSERT, split into the saving step and the reading step), the text printed on the screen, and the rows of the orders table after the run. H2 stores table names in capital letters, but the screen writes them in lowercase, and the member table that the foreign key points to is written as Member, following the entity name. The order of the columns is as seen in H2 and is not the same as the order in which the fields are written (when buyer_id was set with @JoinColumn, buyer_id came before id). What the documentation defines is Section 2.11 of the Jakarta Persistence 3.2 specification (a field that shows an association gets one of @OneToOne, @OneToMany, @ManyToOne, and @ManyToMany), Section 2.12 (the default mapping of associations) and Section 2.12.3.2 (for a unidirectional ManyToOne, a foreign key column that points to the primary key of the other table is placed in the table of this entity, and the name of that column joins the field name, an underscore, and the name of the other side's primary key column), and the API documentation of ManyToOne and JoinColumn (the default naming rule when the name of JoinColumn is not written). The body of the specification has no separate description of the annotations, so these two were read from the API documentation. These documents were opened with WebFetch, and what came back was a summary, not the original text, so each sentence could not be checked against the original. The following is not defined by the documentation but is behavior observed on this version. If you remove @ManyToOne, JPA throws a MappingException when it starts (the first line of the message is Incorrect use of entity type 'app.Member' (possibly due to missing association mapping annotation), and the inner cause is that it could not decide the JdbcType that fits the Java type 'app.Member'), and not one table is created. The member table is not created either, so the row (1, Kim) that is put in ahead of time does not exist. When there is an association, the SELECT that reads the order again was 1, and it was a statement that read the member table together (left join). When only a number was kept, it was 2 in all: 1 SELECT that read the order and 1 SELECT of the Member find that we called. The details of when and how these SELECT statements are sent are covered in the labs on the study side. The text of a statement may differ by Hibernate version, so the screen says "In this run, the statement that Hibernate 7.4.5 made". The @Table that sets the table name to orders was added because order is a reserved word in SQL, and it is not the topic of this lab. Only the fields id and member (or memberId) were covered, and the opposite direction (a bidirectional association where Member has a list of orders), the owning side of an association, lazy loading, the case where the other side does not exist (optional), and cascade were not covered. The association in this lab is unidirectional: it points only from Order to Member. A check against execution on MySQL was not done.

Previous lab: Repositories · Next lab: Lazy fetching

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these