Hibernate и JPA "mappedBy" против владельца отношений и каскадов - PullRequest
0 голосов
/ 28 марта 2019

Фон

Предположим, у нас двунаправленные OneToOne отношения. Есть User и Address сущность. User имеет много Address es.

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;

  //mappings
  private Address address;
}

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

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

  //mapings
  private User user;
}

Отказ от ответственности:

Я использую OneToOne в качестве примера, но вопрос касается отношений в целом, поэтому правильный подход применим и для ManyToMany.

TL; DR - вопрос об использовании mappedBy<->relation owner<->cascades<->managing other side in setters в целом

Вопросы:

  1. В базе данных должен быть user_id в таблице адресных таблиц, верно? Кто тогда является владельцем отношения?
  2. В сущностях: какой из них должен иметь mappedBy - (User или Address) / (Владелец или Обратный)?
  3. На какой стороне (User или Address) / (mappedBy или обратная сторона) следует поместить @OneToOne(cascade = {CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})?
  4. И последний вопрос: какая из сущностей должна управлять отношениями следующим образом:
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 или Address) / (отображается с помощью обратной или обратной стороны)?

Более того - посмотрите на эти две ссылки:

Заранее спасибо

PS. Предыдущая версия моего вопроса использовала OneToMany в качестве примера, но поскольку обратное - ManyToOne не отображено, я изменил его на OneToOne, что лучше показывает проблему

Ответы [ 3 ]

0 голосов
/ 28 марта 2019

Атрибут mappedBy отмечает сторону двунаправленной ассоциации, которой не принадлежит эта ассоциация. Обычно это сторона, у которой нет внешнего ключа.

С другой стороны, каскадные переходы состояний сущности имеют смысл только от родителей к дочерним сущностям. Выполнение каскада от дочерней сущности, в которой хранится FK, является запахом кода.

Это так просто.

0 голосов
/ 28 марта 2019

Это то, чему я научился до сих пор.

Если я ошибаюсь в каком-либо из пунктов - сообщите мне об этом в комментариях, и я отредактирую ответ.

База данных

Владелец отношения

`Согласно этому ответу

По сути, владельцем является адрес, так как ссылка принадлежит владельцу.

Таким образом, отношение * owner в базе данных является сущностью с внешним ключом - Address в случае OneToOne / OneToMany. Более того - кажется, что в то же время "owner" отношения базы данных является Child в отношении Hibernate - но мне нужно подтверждение для этого

OneToOne

Parent / Child

Согласно этой статье

Сущность Post является родительской, а PostDetails - дочерней ассоциацией, поскольку внешний ключ находится в таблице базы данных post_details

Итак, User - это Parent, а Address - это Child здесь (потому что Address содержит внешний ключ)

mappedBy

Согласно этот ответ

Атрибут mappedBy отмечает сторону двунаправленной ассоциации, которой не принадлежит эта ассоциация. Обычно это сторона, у которой нет внешнего ключа.

Итак, mappedBy следует поместить в User, потому что foreign key находится в Address. Кажется, что в @OneToOne mappedBy всегда следует помещать в Parent (сущность, не имеющая внешнего ключа)

JoinColumn

Кажется, что JoinColumn в OneToOne всегда должно быть помещено в Child (так как он содержит внешний ключ)

Каскады

Согласно этот ответ

каскадные переходы состояний сущности имеют смысл только от родителей к дочерним сущностям.

Так что cascades в OneToOne всегда следует делать в Parent, поэтому - согласно предыдущим ссылкам - User в этом примере

установить служебный метод

Я не уверен, но кажется, что установщик утилит должен быть помещен в User.

Всегда ли он помещен в Parent?

Результат

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

Это похоже на OneToOne, поэтому User - это Parent, а Address - это Child (потому что он имеет внешний ключ), но я не уверен на 100%. ..

mappedBy

В эта статья mappedBy помещена в Parent (но я не уверен, что это практическое правило)

JoinColumn

Похоже, что JoinColumn в OneToMany всегда нужно помещать в Child (так как он содержит внешний ключ)

Каскады

В вышеприведенной статье cascades также помещены в Parent (но я не уверен, что и это правило большого пальца)

добавить / удалить служебные методы

В вышеприведенной статье utility methods также помещены в Parent (но я не уверен, что и это правило большого пальца)

Результат

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

Согласно этой статье

Типичная ассоциация базы данных «многие ко многим» включает две родительские таблицы

То есть User и Address Parents

mappedBy

В той же статье Влад написал:

Атрибут mappedBy ассоциации сообщений в объекте Tag отмечает, что в этом двунаправленном отношении объект Post владеет ассоциацией

Следовательно, здесь есть смысл, потому что у нас его нет Parent. Мне не ясно, что именно Влад имеет в виду под «своим», но если User «владеет» отношением, mappedBy должно быть помещено в Address,

JoinTable

Кажется, что JoinTable всегда должен быть помещен в сущность, которая "владеет" отношением, поэтому User в этом случае.

Я прав?

Каскады

В соответствии с той же статьей cascades всегда следует указывать в «владельце», указанном выше, поэтому User в этом случае.

Также важно отметить, что мы не можем использовать REMOVE каскад

добавить / удалить служебные методы

Похоже, что add/remove utility methods следует поместить в User.

Это эмпирическое правило, что utility methods всегда следует помещать в сущность, которая "владеет" отношением?

Результат

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<>();
}

Резюме

Я думаю, что эта таблица подводит итог: https://i.ibb.co/zNjZ3md/JPA-relations.png

Последнее, что я не понимаю, это:

  • почему Влад в этой статье в разделе "ManyToMany" использует mappedBy в Author ("владелец") вместо Book (особенно из-за исключения в моем коде)
  • почему в javadoc mappedBy включен CustomerRecord (дочерний) не включен Customer (но здесь, возможно, внешний ключ находится в Customer, поэтому, хотя счетчик интуитивно понятен - это правильно)
0 голосов
/ 28 марта 2019
  1. В базе данных должен быть user_id в таблице адресных таблиц, верно?Кто тогда является владельцем отношения?
  1. Точно.По сути, владельцем является адрес, так как владелец владеет ссылкой.
В сущностях: какой из них должен иметь mappedBy - пользователь или адрес?

@ManyToOne даже не имеет атрибута mappedBy.Он присутствует только в аннотациях xToMany.

Ваше отображение должно быть:

@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;
}

На какой стороне (пользователь или адрес?) @ManyToMany (cascade = {CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH)}) должны быть размещены?

И последний вопрос: какие из сущностей должны управлять отношениями следующим образом:

В этом случае вы хотите обработать User, а затем JPA обрабатываетОбращайтесь автоматически.Следовательно, каскад будет на User.
На этой стороне (OneToMany) вы, вероятно, также захотите добавить orphanRemoval = true.


Попытка настройки после уточнения ...

По поводу поддержки, JPA поддерживает каскадирование с обеих сторон, хорошо ?!От того, где вы должны его использовать, зависит от вашего домена.
Предполагается, что вы будете использовать его на объекте, который вы используете для операций entityManager, например: em.persist(user) или em.merge(address).
Действительно, зависит от того, как вашкод есть.

В этом конкретном случае, кажется, имеет больше смысла оставлять на User.Я полагаю, у вас есть пользовательский CRUD, который также включает в себя адрес.

В этом случае имеет смысл, когда вы создаете или удаляете пользователя, тогда его адреса создаются / удаляются вместе.

С другой стороны, не имеет смысла каскадировать с адреса.Если вы удаляете адрес, это НЕ означает, что пользователь должен быть удален тоже.

Ты видишь ?!Вы должны проверить свой вариант использования.Сторона, которая будет его использовать (oneToMany, manyToOne или ...) должна быть продумана и решена исходя из ваших потребностей на данный момент.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...