Kamil
Kamil

Reputation: 135

Hibernate & JPA "mappedBy" vs. owner of the relation & cascades

Background

Suppose we have bidirectional OneToOne relationship. There are User and Address entity. User has many Addresses.

CREATE SEQUENCE IF NOT EXISTS hibernate_sequence;
CREATE TABLE users (
  id BIGINT PRIMARY KEY
);

CREATE TABLE addresses (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL UNIQUE CONSTRAINT fk_addresses_user_id REFERENCES users(id) ON DELETE CASCADE,
);

and

@Table(name = "users")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  //mappings
  private Address address;
}

@Table(name = "addresses")
public class Address {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  //mapings
  private User user;
}

Disclaimer:

I'm using OneToOne as an example but the question is about relations in general so the right approach is valid for ManyToMany as well.

TL;DR - the question is about using mappedBy<->relation owner<->cascades<->managing other side in setters in general

Questions:

  1. In database there should be user_id in address table table, right? Who is the owner of the relation then?
  2. In entities: which one should have mappedBy - (User or Address) / (Owner or Inverse)?
  3. On which side (User or Address) / (mappedBy or inverse-side) the @OneToOne(cascade = {CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH}) should be placed?
  4. And last question: which of the entities should manage the relation like this:
public void addAddress(Address address) {
  if (address != null) {
    address.addUser(this);
  }
  this.addresses.add(address);
}

public void removeAddress(Address address) {
  if (address != null) {
    address.removeUser(this);
  }
  this.addresses.remove(address);
}

public Set<Address> getAddresses() {
  return Collections.unmodifiableSet(this.addresses);
}

the (User or Address) / (mappedBy or inverse-side)?

Moreover - look at theese two links:

Thank you in advance

PS. Previous version of my question was using OneToMany as an example, but because inverse - ManyToOne does not have mapped by I changed it to OneToOne which better shows the problem

Upvotes: 4

Views: 6010

Answers (4)

Vlad Mihalcea
Vlad Mihalcea

Reputation: 153700

Table relationships vs. entity relationships

In a relational database system, there are three types of table relationships:

  • one-to-many (via a Foreign Key column)
  • one-to-one (via a shared Primary Key)
  • many-to-many (via a link table with two Foreign Keys referencing two separate parent tables)

So, a one-to-many table relationship looks like this:

one-to-many table relationship

Note that the relationship is based on the post_id Foreign Key column in the child post_comment table.

So, the Foreign Key column is the single source of truth when it comes to managing a one-to-many table relationship.

Now, let's take a bidirectional JPA entity relationship that maps the one-to-many table relationship we saw previously:

Bidirectional One-To-Many entity association

If you take a look at the diagram above, you can see that there are two ways to manage this relationship.

In the Post entity, you have the comments collection:

@OneToMany(
    mappedBy = "post",
    cascade = CascadeType.ALL,
    orphanRemoval = true
)
private List<PostComment> comments = new ArrayList<>();

And, in the PostComment, the post association is mapped as follows:

@ManyToOne(
    fetch = FetchType.LAZY
)
@JoinColumn(name = "post_id")
private Post post;

So, you have two sides that can change the entity association:

  • By adding an entry in the comments child collection, a new post_comment row should be associated with the parent post entity via its post_id column.
  • By setting the post property of the PostComment entity, the post_id column should be updated as well.

Because there are two ways to represent the Foreign Key column, you must define which is the source of truth when it comes to translating the association state change into its equivalent Foreign Key column value modification.

MappedBy (a.k.a the inverse side)

The mappedBy attribute tells that the @ManyToOne side is in charge of managing the Foreign Key column, and the collection is used only to fetch the child entities and to cascade parent entity state changes to children (e.g., removing the parent should also remove the child entities).

It's called the inverse side because it references the child entity property that manages this table relationship.

Synchronize both sides of a bidirectional association

Now, even if you defined the mappedBy attribute and the child-side @ManyToOne association manages the Foreign Key column, you still need to synchronize both sides of the bidirectional association.

The best way to do that is to add these two utility methods:

public void addComment(PostComment comment) {
    comments.add(comment);
    comment.setPost(this);
}

public void removeComment(PostComment comment) {
    comments.remove(comment);
    comment.setPost(null);
}

The addComment and removeComment methods ensure that both sides are synchronized. So, if we add a child entity, the child entity needs to point to the parent and the parent entity should have the child contained in the child collection.

Upvotes: 6

khldqr
khldqr

Reputation: 41

to make an association the owner, you must mark the other end as being mapped by the other. I think the following link is the best for you question: How to Define Association Mappings between Entities

Upvotes: 0

Kamil
Kamil

Reputation: 135

This is what I have learnt so far.

If I'm mistaken to any of the points - please let me know in comments, and I edit the answer.

Database

Owner of the relation

`According to this answer

Basically, the owner is address, as it's the owner who holds the reference.

So the "owner" of relation in database is entity with foreign key - Address in case of OneToOne/OneToMany. Moreover - it seems that in the same time "owner" of the database relation is the Child in Hibernate relation - but I need confirmation for this

OneToOne

Parent/Child

According to this article

The Post entity is the parent, while the PostDetails is the child association because the Foreign Key is located in the post_details database table

So User is the Parent and Address is the Child here (because Address holds foreign key)

mappedBy

According to this answer

The mappedBy attribute marks the side of a bidirectional association which does not own the association. Usually, that's the side which does not have the Foreign Key.

So mappedBy should be placed in User, because foreign key is in Address. In seems that in @OneToOne mappedBy should be always placed in Parent (entity not holding the foreign key)

JoinColumn

It seems that JoinColumn in OneToOne must be always placed in Child (since it holds foreign key)

Cascades

According to this answer

cascading entity state transitions only makes sense from parents to child entities.

So cascades in OneToOne should be always done in Parent, so - according to previous links - User in this example

set utility method

I'm not sure but it seems that utility setter should be placed in User.

Is it always placed in the Parent?

Outcome

CREATE SEQUENCE IF NOT EXISTS hibernate_sequence;
CREATE TABLE users (
  id BIGINT PRIMARY KEY
);

CREATE TABLE addresses (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL UNIQUE CONSTRAINT fk_addresses_user_id REFERENCES users(id) ON DELETE CASCADE,
);
@Table(name = "users")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = false, orphanRemoval = true)
  private Address address;

  public void setAddress(Address address) {
    if (address != null) {
      address.setUser(this);
    }
    this.address = address;
  }
}

@Table(name = "addresses")
public class Address {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  @OneToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id")
  private User user;
}

OneToMany

Parent/Child

It seems to be the same as with OneToOne, so User is the Parent and Address is the Child (because it has foreign key), but I'm not 100% sure...

mappedBy

In this article mappedBy is placed in Parent (but I'm not sure if this is the rule of thumb though)

JoinColumn

It seems that JoinColumn in OneToMany must be always placed in Child (since it holds foreign key)

Cascades

In above article cascades are also placed in Parent (but I'm not sure if this is the rule of thumb as well)

add/remove utility methods

In above article utility methods are also placed in Parent (but I'm not sure if this is the rule of thumb as well)

Outcome

CREATE SEQUENCE IF NOT EXISTS hibernate_sequence;
CREATE TABLE users (
  id BIGINT PRIMARY KEY
);

CREATE TABLE addresses (
  id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL CONSTRAINT fk_addresses_user_id REFERENCES users(id) ON DELETE CASCADE,
);
@Table(name = "users")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
  private List<Address> addresses = new ArrayList<>();

  public void addAddress(Address address) {
    if (address != null) {
      address.setUser(this);
    }
    this.addresses.add(address);
  }

  public void removeAddress(Address address) {
    this.addresses.remove(address);
    if (address != null) {
      address.setUser(null);
    }
  }

  public Set<Address> getAddresses() {
    return Collections.unmodifiableSet(this.addresses);
  }
}

@Table(name = "addresses")
public class Address {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "user_id")
  private User user;
}

ManyToMany

Parent/Child

According to this article

A typical many-to-many database association includes two parent tables

So both User and Address are Parents

mappedBy

In the same article Vlad wrote:

The mappedBy attribute of the posts association in the Tag entity marks that, in this bidirectional relationship, the Post entity own the association

Therefore here is the eception, because we don't have one Parent. It is not clear to me what Vlad means exactly by "own", but if User "owns" the relation, mappedBy must be placed in Address,

JoinTable

It seems that JoinTable must be always placed in entity that "owns" the relation, so User in this case.

Am I right?

Cascades

According to the same article cascades should be always placed in "owner" defined above, so User in this case.

Also important thing to notice is that we can not use REMOVE cascade

add/remove utility methods

It seems that add/remove utility methods should be placed in User.

Is this the rule of thumb, that utility methods should be alway placed in entity that "owns" the relation?

Outcome

CREATE SEQUENCE IF NOT EXISTS hibernate_sequence;
CREATE TABLE users (
  id BIGINT PRIMARY KEY
);

CREATE TABLE addresses (
  id BIGINT PRIMARY KEY,
);

CREATE TABLE users_addresses (
  user_id BIGINT NOT NULL CONSTRAINT fk_users_addresses_user_id REFERENCES users(id) ON DELETE CASCADE,
  address_id BIGINT NOT NULL CONSTRAINT fk_users_addresses_address_id REFERENCES addresses(id) ON DELETE CASCADE,
  PRIMARY KEY(user_id,address_id)
);

@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @ManyToMany(cascade = {
      CascadeType.PERSIST,
      CascadeType.MERGE
    })
    @JoinTable(name = "post_tag",
      joinColumns = @JoinColumn(name = "post_id"),
      inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    private List<Address> addresses = new ArrayList<>();

    public void addAddress(Address address) {
      addresses.add(address);
      address.getUsers().add(this);
    }

    public void removeAddress(Address address) {
      addresses.remove(address);
      address.getUsers().remove(this);
    }
}

@Table(name = "addresses")
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    @ManyToMany(mappedBy = "addresses")
    private List<User> users = new ArrayList<>();
}

Summary

I think this table sums it up: https://i.ibb.co/zNjZ3md/JPA-relations.png

The last thing I don't unserstand is:

  • why Vlad in this article in "ManyToMany" section uses mappedBy in Author ("owner") instead of Book (especially that it throws exception in my code)
  • why in javadoc mappedBy is on CustomerRecord (child) not on Customer (but here maybe foreign key is located in Customer, so although counter intuitive - it is correct)

Upvotes: 4

RicardoS
RicardoS

Reputation: 2118

  1. In database there should be user_id in address table table, right? Who is the owner of the relation then?
  1. Exactly. Basically, the owner is address, as it's the owner who holds the reference.
  1. In entities: which one should have mappedBy - User or Address?

@ManyToOne doesn't even have the attribute mappedBy. It's present only on xToMany annotations.

Your mapping should be:

@Table(name = "users")
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  @OneToMany(mappedBy="user", cascade = ...) // atenttion, this refer the attribute name as opposed to the column name
  private Set<Address> addresses = new HashSet<>();
}

@Table(name = "addresses")
public class Address {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  @ManyToOne
  @JoinColumn(name="user_id") // the name is optional if you refer to the PK
  private User user;
}

On which side (User or Address?) the @ManyToMany(cascade = {CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH}) should be placed?

And last question: which of the entities should manage the relation like this:

In this case, you want to handle User, and then JPA handles Address automatically. Therefore, the cascade would be on User.
On this side (OneToMany), you probably also want to add orphanRemoval = true.


Trying to adjust after clarification...

About the support, JPA supports cascading from both sides, ok?! From where you should use it depends on your domain.
You are supposed to use it on the Entity which you use on entityManager operations, for instance: em.persist(user) or em.merge(address).
Really, depends on how your code is.

In this specify case, it seems it makes more sense to leave on User. I suppose you have an User CRUD, which also includes address.

In this case, it makes sense when you create or remove an User, then its addresses are created/remove together.

On the other side, it wouldn't make sense to cascade from the Address. If you remove an Address, it does NOT mean that the user should be removed too.

Can you see?! You need to check your use case. The side to use it (whether oneToMany or manyToOne or ...) should be thought and decided based on your needs on that moment.

Upvotes: 0

Related Questions