Post

Spring) JPA Entity Relationships - Owner of the Foreign Key

Spring) JPA Entity Relationships - Owner of the Foreign Key

This post was migrated from Tistory. You can find the original here.

Entity Relationships

The basics

Only the owner of the foreign key can insert, update, or delete it.
The owner of the foreign key is the entity whose table gets the foreign key column.
The non-owning side can only read the foreign key.

The owner of the foreign key uses @JoinColumn(name = columnName),
and (in a bidirectional relationship) the other side uses (mappedBy = the field name on the FK owner that points back to 'me').

Note that this is the field name, not the column name that points to ‘me’. It’s the field name, not the JoinColumn’s column name.

Field name = the Java object’s field

Column name = the DB column

Unidirectional vs. bidirectional

Let’s look at an example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;
}

public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @OneToMany(mappedBy = "customer")
    private List<Food> foodList = new ArrayList<>();

    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.setCustomer(this);
    }
}

In the code above, the @OneToMany field on Customer isn’t required.

If the non-owning side needs to reference the related object, set up a bidirectional relationship; if not, a unidirectional one is fine.

Something to watch out for with bidirectional relationships

1
2
3
4
5
6
    (1)
    user.getFoodList().add(food);
    (2)
    user.addFoodList(food);

    userRepository.save(user);

As in (1), adding the related object from the non-owning side does not set the foreign key value. That’s why you need a method like (2). In the end, only the owner of the foreign key can insert/update/delete it.

There’s also something to consider when adding a related object on the owning side in a bidirectional relationship.

1
2
3
4
Food food = new Food();
...
food.setCustomer(customer);
foodRepository.save(food);

The code above is fine from the DB’s perspective, but from the object’s perspective, the other side’s entity — the customer’s foodList — doesn’t have food added to it.

1
2
3
4
5
(Food entity)
       public void setCustomer(Customer customer){
        this.customer = customer;
        customer.getFoodList().add(this);
    }

So you can handle it as shown above, making sure food is also added to the customer object.
This is necessary if there’s additional logic elsewhere that references customer after saving food.

N(FK owner):1 vs. 1(FK owner):N

Normally, the “N” side owns the foreign key. So what if the “1” side owns the foreign key instead?

  • Even in a 1(owner)-to-N relationship, there’s no reason for the FK column to end up on the “1” side’s table. So in the DB, the FK column still ends up on the “N” side’s table, but it’s managed by the “1” (FK owner) side. This results in an extra UPDATE.

Let’s take a closer look at that “extra UPDATE happens” part.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;
}

public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

This is a unidirectional relationship where Food is the “N” side and owns the foreign key.

1
2
3
4
5
6
7
8
9
10
    User user = new User();
    user.setName("Robbie");

    Food food = new Food();
    food.setName("Fried chicken");
    food.setPrice(15000);
    food.setUser(user); // set the foreign key (relationship)

    userRepository.save(user);
    foodRepository.save(food);

For this code, excluding selects, 2 inserts are issued.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Food {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private double price;

    @OneToMany
    @JoinColumn(name = "food_id") // food_id column on the users table
    private List<User> userList = new ArrayList<>();
}

public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

This is a unidirectional relationship where Food is the “1” side and owns the foreign key.

1
2
3
4
5
6
7
8
9
10
    User user = new User();
    user.setName("Robbie");

    Food food = new Food();
    food.setName("Fried chicken");
    food.setPrice(15000);
    food.getUserList().add(user); // set the foreign key (relationship)

    userRepository.save(user);
    foodRepository.save(food);

For this code, excluding selects, 2 inserts and 1 update are issued.

Why is that?
Only the FK owner can insert, update, or delete the FK. But the FK managed by the FK owner physically lives in a different table. That’s what causes the extra operation.
When inserting User, we’d like to set the FK at the same time, but User isn’t the owner of the foreign key, so it can’t set it. Only after the insert runs does Food — the actual owner of the foreign key — issue an update query against user to set (fix up) the FK.

Also, the FK isn’t updated at the same time as the save call.
So even if you change the order of the save calls, only the insert order changes — the update still runs last.
It seems that in a 1(FK owner):N relationship, registering the foreign key is deliberately placed at the end of the persistence context’s write-behind queue.

One more thing

Next, let’s look at lazy loading, cascading, and orphan removal for entity options.

2023.07.13 - [spring] - Spring) JPA Entity Options - Lazy Loading / Cascade / Orphan Removal (23-07-12)

This post is licensed under CC BY-NC 4.0 by the author.