JPA JoinColumn против mappedBy: в чем разница?
Какова разница между двумя приведенными выше аннотациями в контексте JPA (Java Persistence API)?
В первом примере:
@Entity
public class Company {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "companyIdRef", referencedColumnName = "companyId")
private List<Branch> branches;
...
}
аннотация @JoinColumn
используется для определения колонки, которая будет связывать сущность Company
с сущностью Branch
. Здесь указано, что в таблице Branch
будет столбец companyIdRef
, который является внешним ключом, ссылающимся на companyId
в таблице Company
.
Во втором примере:
@Entity
public class Company {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY,
mappedBy = "companyIdRef")
private List<Branch> branches;
...
}
используется аннотация mappedBy
, которая указывает, что связь управляется стороной Branch
и что в классе Branch
есть поле companyIdRef
, которое несет информацию о связи. Таким образом, таблица Branch
должна содержать информацию о том, к какой компании принадлежит каждый филиал.
В чем разница между этими подходами? Какой из них предпочтительнее в определенных ситуациях, и какие могут быть последствия выбора одного подхода над другим?
5 ответ(ов)
Аннотация @JoinColumn
указывает, что эта сущность является владельцем отношения (то есть соответствующая таблица имеет столбец с внешним ключом на связанную таблицу), в то время как атрибут mappedBy
указывает, что сущность с этой стороны является обратной частью отношения, а владелец находится в "другой" сущности. Это также означает, что вы можете получить доступ к другой таблице из класса, который вы аннотировали с помощью mappedBy
(то есть полноценная билинейная связь).
В частности, для приведенного в вопросе кода правильные аннотации будут выглядеть следующим образом:
@Entity
public class Company {
@OneToMany(mappedBy = "company",
orphanRemoval = true,
fetch = FetchType.LAZY,
cascade = CascadeType.ALL)
private List<Branch> branches;
}
@Entity
public class Branch {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "companyId")
private Company company;
}
Я не согласен с принятой здесь ответом Óscar López. Этот ответ неточный!
Именно аннотация @ManyToOne
, а не @JoinColumn
, указывает на то, что данная сущность является владельцем отношений (в его примере).
Аннотации, такие как @ManyToOne
, @OneToMany
и @ManyToMany
, сообщают JPA/Hibernate о необходимости создать связь. По умолчанию это происходит через отдельную таблицу соединений.
@JoinColumn
Цель аннотации
@JoinColumn
- создать колонку соединения, если она еще не существует. Если она уже есть, то эта аннотация может быть использована для именования колонки соединения.
MappedBy
Цель параметра
MappedBy
- дать инструкцию JPA: НЕ создавайте другую таблицу соединений, так как связь уже отображается другой (противоположной) сущностью данного отношения.
Запомните: MappedBy
— это свойство аннотаций отношений, задача которого - создать механизм для установления соотношения между двумя сущностями, которые по умолчанию создаются через таблицу соединений. MappedBy
останавливает этот процесс в одном направлении.
Сущность, не использующая MappedBy
, считается владельцем отношений, поскольку механика отображения определяется внутри ее класса с помощью одной из трех аннотаций отображения, использующих поле внешнего ключа. Это не только указывает на природу отображения, но и дает указание на создание таблицы соединений. Более того, существует возможность подавить создание таблицы соединений, применив аннотацию @JoinColumn
на внешнем ключе, что сохраняет его внутри таблицы сущности-владельца.
Таким образом, в кратком изложении: @JoinColumn
либо создает новую колонку соединения, либо переименовывает существующую, в то время как параметр MappedBy
работает в сотрудничестве с аннотациями отношений другой (дочерней) сущности для создания связи либо через таблицу соединений, либо создает колонку внешнего ключа в связанной таблице сущности-владельца.
Чтобы проиллюстрировать, как работает MappedBy
, рассмотрим код ниже. Если удалить параметр MappedBy
, Hibernate фактически создаст ДВЕ таблицы соединений! Почему? Потому что в отношениях многие-ко-многим существует симметрия, и Hibernate не имеет оснований выбирать одно направление над другим.
Поэтому мы используем MappedBy
, чтобы сказать Hibernate, что мы выбрали другую сущность для определения отображения отношений между двумя сущностями.
@Entity
public class Driver {
@ManyToMany(mappedBy = "drivers")
private List<Cars> cars;
}
@Entity
public class Cars {
@ManyToMany
private List<Drivers> drivers;
}
Добавление @JoinColumn(name = "driverID")
в класс-владелец (см. ниже) предотвратит создание таблицы соединений и вместо этого создаст колонку внешнего ключа driverID в таблице Cars для построения отображения:
@Entity
public class Driver {
@ManyToMany(mappedBy = "drivers")
private List<Cars> cars;
}
@Entity
public class Cars {
@ManyToMany
@JoinColumn(name = "driverID")
private List<Drivers> drivers;
}
Аннотация mappedBy лучше всего использовать на стороне родителя (в классе Company) в двунаправленных отношениях. В данном случае она должна находиться в классе Company и указывать на член переменной 'company' в дочернем классе (классе Branch).
Аннотация @JoinColumn используется для указания столбца, по которому осуществляется связь между сущностями, и может применяться в любом классе (родительском или дочернем). Однако ее следует использовать только с одной стороны (либо в родительском классе, либо в дочернем), но не в обоих. В приведённом примере я использовал ее на стороне дочернего класса (классе Branch), обозначая внешний ключ в классе Branch.
Ниже приведён рабочий пример:
Родительский класс, Company
@Entity
public class Company {
private int companyId;
private String companyName;
private List<Branch> branches;
@Id
@GeneratedValue
@Column(name="COMPANY_ID")
public int getCompanyId() {
return companyId;
}
public void setCompanyId(int companyId) {
this.companyId = companyId;
}
@Column(name="COMPANY_NAME")
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
@OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL, mappedBy="company")
public List<Branch> getBranches() {
return branches;
}
public void setBranches(List<Branch> branches) {
this.branches = branches;
}
}
Дочерний класс, Branch
@Entity
public class Branch {
private int branchId;
private String branchName;
private Company company;
@Id
@GeneratedValue
@Column(name="BRANCH_ID")
public int getBranchId() {
return branchId;
}
public void setBranchId(int branchId) {
this.branchId = branchId;
}
@Column(name="BRANCH_NAME")
public String getBranchName() {
return branchName;
}
public void setBranchName(String branchName) {
this.branchName = branchName;
}
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name="COMPANY_ID")
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
}
Давайте упростим.
Вы можете использовать аннотацию @JoinColumn с любой стороны без учета типа связывания.
Рассмотрим три случая:
- Uni-directional связывание от Branch (филиала) к Company (компании).
- Bi-directional связывание от Company к Branch.
- Uni-directional связывание только от Company к Branch.
Таким образом, любой случай будет подпадать под одну из этих категорий. Далее объясню, как использовать @JoinColumn и mappedBy.
- Uni-directional связывание от Branch к Company.
Используйте @JoinColumn в таблице Branch.
- Bi-directional связывание от Company к Branch.
Используйте mappedBy в таблице Company, как описано в ответе @Mykhaylo Adamovych.
- Uni-directional связывание только от Company к Branch.
Просто используйте @JoinColumn в таблице Company.
@Entity
public class Company {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name="courseId")
private List<Branch> branches;
...
}
Это означает, что на основе внешнего ключа "courseId" в таблице branches получите список всех филиалов. Обратите внимание: в этом случае вы не сможете получить компанию из филиала, так как существует только uni-directional связывание от компании к филиалам.
JPA (Java Persistence API) — это многоуровневый API, и на каждом уровне имеются свои аннотации. Наивысший уровень — это (1) уровень сущностей, в котором описываются постоянные (постоянно хранящиеся) классы. Затем идет (2) уровень реляционной базы данных, который предполагает, что сущности сопоставлены с реляционной базой данных, и (3) уровень модели на Java.
Аннотации уровня 1: @Entity
, @Id
, @OneToOne
, @OneToMany
, @ManyToOne
, @ManyToMany
.
Вы можете внедрить сохранение состояния в своем приложении, используя только эти аннотации высокого уровня. Но в этом случае вам придется создать базу данных в соответствии с предположениями, которые делает JPA. Эти аннотации определяют модель сущностей и отношений.
Аннотации уровня 2: @Table
, @Column
, @JoinColumn
и др.
Эти аннотации влияют на сопоставление сущностей/свойств с таблицами/столбцами реляционной базы данных, если вас не устраивают значения по умолчанию JPA или если вам нужно сопоставить с уже существующей базой данных. Эти аннотации можно рассматривать как аннотации реализации; они определяют, как должно происходить сопоставление.
На мой взгляд, лучше придерживаться как можно больше аннотаций высокого уровня, а затем вводить аннотации более низкого уровня по мере необходимости.
Отвечая на ваши вопросы: сочетание @OneToMany
и mappedBy
является наиболее предпочтительным, так как использует только аннотации из области сущностей. Использование @OneToMany
в сочетании с @JoinColumn
также приемлемо, но в этом случае применяется аннотация реализации, хотя это не является строгой необходимостью.
Как исправить ошибку Hibernate "объект ссылается на несохраненный временный экземпляр - сохраните временный экземпляр перед сбросом"
Возможные значения конфигурации hbm2ddl.auto в Hibernate и их назначение
Как установить Java 8 на Mac
Почему в RecyclerView отсутствует onItemClickListener()?
Что значит 'synchronized'?