Associations
Associations describe how two or more entities form a relationship based on a database joining semantics.
@ManyToOne
@ManyToOne
is the most common association, having a direct equivalent in the relational database as well (e.g. foreign key),
and so it establishes a relationship between a child entity and a parent.
@ManyToOne
association@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
public Person() {
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@ManyToOne
@JoinColumn(name = "person_id",
foreignKey = @ForeignKey(name = "PERSON_ID_FK")
)
private Person person;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
public Long getId() {
return id;
}
public String getNumber() {
return number;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
person_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT PERSON_ID_FK
FOREIGN KEY (person_id) REFERENCES Person
Each entity has a lifecycle of its own. Once the @ManyToOne
association is set, Hibernate will set the associated database foreign key column.
@ManyToOne
association lifecyclePerson person = new Person();
entityManager.persist( person );
Phone phone = new Phone( "123-456-7890" );
phone.setPerson( person );
entityManager.persist( phone );
entityManager.flush();
phone.setPerson( null );
INSERT INTO Person ( id )
VALUES ( 1 )
INSERT INTO Phone ( number, person_id, id )
VALUES ( '123-456-7890', 1, 2 )
UPDATE Phone
SET number = '123-456-7890',
person_id = NULL
WHERE id = 2
@OneToMany
The @OneToMany
association links a parent entity with one or more child entities.
If the @OneToMany
doesn’t have a mirroring @ManyToOne
association on the child side, the @OneToMany
association is unidirectional.
If there is a @ManyToOne
association on the child side, the @OneToMany
association is bidirectional and the application developer can navigate this relationship from both ends.
Unidirectional @OneToMany
When using a unidirectional @OneToMany
association, Hibernate resorts to using a link table between the two joining entities.
@OneToMany
association@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
public Person() {
}
public List<Phone> getPhones() {
return phones;
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
public Long getId() {
return id;
}
public String getNumber() {
return number;
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Phone (
Person_id BIGINT NOT NULL ,
phones_id BIGINT NOT NULL
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
PRIMARY KEY ( id )
)
ALTER TABLE Person_Phone
ADD CONSTRAINT UK_9uhc5itwc9h5gcng944pcaslf
UNIQUE (phones_id)
ALTER TABLE Person_Phone
ADD CONSTRAINT FKr38us2n8g5p9rj0b494sd3391
FOREIGN KEY (phones_id) REFERENCES Phone
ALTER TABLE Person_Phone
ADD CONSTRAINT FK2ex4e4p7w1cj310kg2woisjl2
FOREIGN KEY (Person_id) REFERENCES Person
The |
@OneToMany
associationPerson person = new Person();
Phone phone1 = new Phone( "123-456-7890" );
Phone phone2 = new Phone( "321-654-0987" );
person.getPhones().add( phone1 );
person.getPhones().add( phone2 );
entityManager.persist( person );
entityManager.flush();
person.getPhones().remove( phone1 );
INSERT INTO Person
( id )
VALUES ( 1 )
INSERT INTO Phone
( number, id )
VALUES ( '123-456-7890', 2 )
INSERT INTO Phone
( number, id )
VALUES ( '321-654-0987', 3 )
INSERT INTO Person_Phone
( Person_id, phones_id )
VALUES ( 1, 2 )
INSERT INTO Person_Phone
( Person_id, phones_id )
VALUES ( 1, 3 )
DELETE FROM Person_Phone
WHERE Person_id = 1
INSERT INTO Person_Phone
( Person_id, phones_id )
VALUES ( 1, 3 )
DELETE FROM Phone
WHERE id = 2
When persisting the Person
entity, the cascade will propagate the persist operation to the underlying Phone
children as well.
Upon removing a Phone
from the phones collection, the association row is deleted from the link table, and the orphanRemoval
attribute will trigger a Phone
removal as well.
The unidirectional associations are not very efficient when it comes to removing child entities. In this particular example, upon flushing the persistence context, Hibernate deletes all database child entries and reinserts the ones that are still found in the in-memory persistence context. On the other hand, a bidirectional |
Bidirectional @OneToMany
The bidirectional @OneToMany
association also requires a @ManyToOne
association on the child side.
Although the Domain Model exposes two sides to navigate this association, behind the scenes, the relational database has only one foreign key for this relationship.
Every bidirectional association must have one owning side only (the child side), the other one being referred to as the inverse (or the mappedBy
) side.
@OneToMany
association mappedBy the @ManyToOne
side@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
public Person() {
}
public Person(Long id) {
this.id = id;
}
public List<Phone> getPhones() {
return phones;
}
public void addPhone(Phone phone) {
phones.add( phone );
phone.setPerson( this );
}
public void removePhone(Phone phone) {
phones.remove( phone );
phone.setPerson( null );
}
}
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@NaturalId
@Column(name = "`number`", unique = true)
private String number;
@ManyToOne
private Person person;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
public Long getId() {
return id;
}
public String getNumber() {
return number;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Phone phone = (Phone) o;
return Objects.equals( number, phone.number );
}
@Override
public int hashCode() {
return Objects.hash( number );
}
}
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
person_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT UK_l329ab0g4c1t78onljnxmbnp6
UNIQUE (number)
ALTER TABLE Phone
ADD CONSTRAINT FKmw13yfsjypiiq0i1osdkaeqpg
FOREIGN KEY (person_id) REFERENCES Person
Whenever a bidirectional association is formed, the application developer must make sure both sides are in-sync at all times.
The |
Because the Phone
class has a @NaturalId
column (the phone number being unique),
the equals()
and the hashCode()
can make use of this property, and so the removePhone()
logic is reduced to the remove()
Java Collection
method.
@OneToMany
with an owner @ManyToOne
side lifecyclePerson person = new Person();
Phone phone1 = new Phone( "123-456-7890" );
Phone phone2 = new Phone( "321-654-0987" );
person.addPhone( phone1 );
person.addPhone( phone2 );
entityManager.persist( person );
entityManager.flush();
person.removePhone( phone1 );
INSERT INTO Person
( id )
VALUES ( 1 )
INSERT INTO Phone
( "number", person_id, id )
VALUES ( '123-456-7890', 1, 2 )
INSERT INTO Phone
( "number", person_id, id )
VALUES ( '321-654-0987', 1, 3 )
DELETE FROM Phone
WHERE id = 2
Unlike the unidirectional @OneToMany
, the bidirectional association is much more efficient when managing the collection persistence state.
Every element removal only requires a single update (in which the foreign key column is set to NULL
), and,
if the child entity lifecycle is bound to its owning parent so that the child cannot exist without its parent,
then we can annotate the association with the orphan-removal
attribute and disassociating the child will trigger a delete statement on the actual child table row as well.
@OneToOne
The @OneToOne
association can either be unidirectional or bidirectional.
A unidirectional association follows the relational database foreign key semantics, the client-side owning the relationship.
A bidirectional association features a mappedBy
@OneToOne
parent side too.
Unidirectional @OneToOne
@OneToOne
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne
@JoinColumn(name = "details_id")
private PhoneDetails details;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
public Long getId() {
return id;
}
public String getNumber() {
return number;
}
public PhoneDetails getDetails() {
return details;
}
public void setDetails(PhoneDetails details) {
this.details = details;
}
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
public PhoneDetails() {
}
public PhoneDetails(String provider, String technology) {
this.provider = provider;
this.technology = technology;
}
public String getProvider() {
return provider;
}
public String getTechnology() {
return technology;
}
public void setTechnology(String technology) {
this.technology = technology;
}
}
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
details_id BIGINT ,
PRIMARY KEY ( id )
)
CREATE TABLE PhoneDetails (
id BIGINT NOT NULL ,
provider VARCHAR(255) ,
technology VARCHAR(255) ,
PRIMARY KEY ( id )
)
ALTER TABLE Phone
ADD CONSTRAINT FKnoj7cj83ppfqbnvqqa5kolub7
FOREIGN KEY (details_id) REFERENCES PhoneDetails
From a relational database point of view, the underlying schema is identical to the unidirectional @ManyToOne
association,
as the client-side controls the relationship based on the foreign key column.
But then, it’s unusual to consider the Phone
as a client-side and the PhoneDetails
as the parent-side because the details cannot exist without an actual phone.
A much more natural mapping would be if the Phone
were the parent-side, therefore pushing the foreign key into the PhoneDetails
table.
This mapping requires a bidirectional @OneToOne
association as you can see in the following example:
Bidirectional @OneToOne
@OneToOne
@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne(mappedBy = "phone", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private PhoneDetails details;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
public Long getId() {
return id;
}
public String getNumber() {
return number;
}
public PhoneDetails getDetails() {
return details;
}
public void addDetails(PhoneDetails details) {
details.setPhone( this );
this.details = details;
}
public void removeDetails() {
if ( details != null ) {
details.setPhone( null );
this.details = null;
}
}
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "phone_id")
private Phone phone;
public PhoneDetails() {
}
public PhoneDetails(String provider, String technology) {
this.provider = provider;
this.technology = technology;
}
public String getProvider() {
return provider;
}
public String getTechnology() {
return technology;
}
public void setTechnology(String technology) {
this.technology = technology;
}
public Phone getPhone() {
return phone;
}
public void setPhone(Phone phone) {
this.phone = phone;
}
}
CREATE TABLE Phone (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE PhoneDetails (
id BIGINT NOT NULL ,
provider VARCHAR(255) ,
technology VARCHAR(255) ,
phone_id BIGINT ,
PRIMARY KEY ( id )
)
ALTER TABLE PhoneDetails
ADD CONSTRAINT FKeotuev8ja8v0sdh29dynqj05p
FOREIGN KEY (phone_id) REFERENCES Phone
This time, the PhoneDetails
owns the association, and, like any bidirectional association, the parent-side can propagate its lifecycle to the child-side through cascading.
@OneToOne
lifecyclePhone phone = new Phone( "123-456-7890" );
PhoneDetails details = new PhoneDetails( "T-Mobile", "GSM" );
phone.addDetails( details );
entityManager.persist( phone );
INSERT INTO Phone ( number, id )
VALUES ( '123-456-7890', 1 )
INSERT INTO PhoneDetails ( phone_id, provider, technology, id )
VALUES ( 1, 'T-Mobile', 'GSM', 2 )
When using a bidirectional @OneToOne
association, Hibernate enforces the unique constraint upon fetching the child-side.
If there are more than one children associated with the same parent, Hibernate will throw a org.hibernate.exception.ConstraintViolationException
.
Continuing the previous example, when adding another PhoneDetails
, Hibernate validates the uniqueness constraint when reloading the Phone
object.
@OneToOne
unique constraintPhoneDetails otherDetails = new PhoneDetails( "T-Mobile", "CDMA" );
otherDetails.setPhone( phone );
entityManager.persist( otherDetails );
entityManager.flush();
entityManager.clear();
//throws javax.persistence.PersistenceException: org.hibernate.HibernateException: More than one row with the given identifier was found: 1
phone = entityManager.find( Phone.class, phone.getId() );
Bidirectional @OneToOne
lazy association
Although you might annotate the parent-side association to be fetched lazily,
Hibernate cannot honor this request since it cannot know whether the association is null
or not.
The only way to figure out whether there is an associated record on the child side is to fetch the child association using a secondary query.
Because this can lead to N+1 query issues, it’s much more efficient to use unidirectional @OneToOne
associations with the @MapsId
annotation in place.
However, if you really need to use a bidirectional association and want to make sure that this is always going to be fetched lazily,
then you need to enable lazy state initialization bytecode enhancement and use the
@LazyToOne
annotation as well.
@OneToOne
lazy parent-side association@Entity(name = "Phone")
public static class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne(
mappedBy = "phone",
cascade = CascadeType.ALL,
orphanRemoval = true,
fetch = FetchType.LAZY
)
@LazyToOne( LazyToOneOption.NO_PROXY )
private PhoneDetails details;
public Phone() {
}
public Phone(String number) {
this.number = number;
}
//Getters and setters are omitted for brevity
}
@Entity(name = "PhoneDetails")
public static class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "phone_id")
private Phone phone;
public PhoneDetails() {
}
public PhoneDetails(String provider, String technology) {
this.provider = provider;
this.technology = technology;
}
//Getters and setters are omitted for brevity
}
For more about how to enable Bytecode enhancement, see the BytecodeEnhancement chapter.
@ManyToMany
The @ManyToMany
association requires a link table that joins two entities.
Like the @OneToMany
association, @ManyToMany
can be a either unidirectional or bidirectional.
Unidirectional @ManyToMany
@ManyToMany
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Address> addresses = new ArrayList<>();
public Person() {
}
public List<Address> getAddresses() {
return addresses;
}
}
@Entity(name = "Address")
public static class Address {
@Id
@GeneratedValue
private Long id;
private String street;
@Column(name = "`number`")
private String number;
public Address() {
}
public Address(String street, String number) {
this.street = street;
this.number = number;
}
public Long getId() {
return id;
}
public String getStreet() {
return street;
}
public String getNumber() {
return number;
}
}
CREATE TABLE Address (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
street VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person (
id BIGINT NOT NULL ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Address (
Person_id BIGINT NOT NULL ,
addresses_id BIGINT NOT NULL
)
ALTER TABLE Person_Address
ADD CONSTRAINT FKm7j0bnabh2yr0pe99il1d066u
FOREIGN KEY (addresses_id) REFERENCES Address
ALTER TABLE Person_Address
ADD CONSTRAINT FKba7rc9qe2vh44u93u0p2auwti
FOREIGN KEY (Person_id) REFERENCES Person
Just like with unidirectional @OneToMany
associations, the link table is controlled by the owning side.
When an entity is removed from the @ManyToMany
collection, Hibernate simply deletes the joining record in the link table.
Unfortunately, this operation requires removing all entries associated with a given parent and recreating the ones that are listed in the current running persistent context.
@ManyToMany
lifecyclePerson person1 = new Person();
Person person2 = new Person();
Address address1 = new Address( "12th Avenue", "12A" );
Address address2 = new Address( "18th Avenue", "18B" );
person1.getAddresses().add( address1 );
person1.getAddresses().add( address2 );
person2.getAddresses().add( address1 );
entityManager.persist( person1 );
entityManager.persist( person2 );
entityManager.flush();
person1.getAddresses().remove( address1 );
INSERT INTO Person ( id )
VALUES ( 1 )
INSERT INTO Address ( number, street, id )
VALUES ( '12A', '12th Avenue', 2 )
INSERT INTO Address ( number, street, id )
VALUES ( '18B', '18th Avenue', 3 )
INSERT INTO Person ( id )
VALUES ( 4 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 2 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 3 )
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 4, 2 )
DELETE FROM Person_Address
WHERE Person_id = 1
INSERT INTO Person_Address ( Person_id, addresses_id )
VALUES ( 1, 3 )
For For example, if
|
By simply removing the parent-side, Hibernate can safely remove the associated link records as you can see in the following example:
@ManyToMany
entity removalPerson person1 = entityManager.find( Person.class, personId );
entityManager.remove( person1 );
DELETE FROM Person_Address
WHERE Person_id = 1
DELETE FROM Person
WHERE id = 1
Bidirectional @ManyToMany
A bidirectional @ManyToMany
association has an owning and a mappedBy
side.
To preserve synchronicity between both sides, it’s good practice to provide helper methods for adding or removing child entities.
@ManyToMany
@Entity(name = "Person")
public static class Person {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String registrationNumber;
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Address> addresses = new ArrayList<>();
public Person() {
}
public Person(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
public List<Address> getAddresses() {
return addresses;
}
public void addAddress(Address address) {
addresses.add( address );
address.getOwners().add( this );
}
public void removeAddress(Address address) {
addresses.remove( address );
address.getOwners().remove( this );
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Person person = (Person) o;
return Objects.equals( registrationNumber, person.registrationNumber );
}
@Override
public int hashCode() {
return Objects.hash( registrationNumber );
}
}
@Entity(name = "Address")
public static class Address {
@Id
@GeneratedValue
private Long id;
private String street;
@Column(name = "`number`")
private String number;
private String postalCode;
@ManyToMany(mappedBy = "addresses")
private List<Person> owners = new ArrayList<>();
public Address() {
}
public Address(String street, String number, String postalCode) {
this.street = street;
this.number = number;
this.postalCode = postalCode;
}
public Long getId() {
return id;
}
public String getStreet() {
return street;
}
public String getNumber() {
return number;
}
public String getPostalCode() {
return postalCode;
}
public List<Person> getOwners() {
return owners;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Address address = (Address) o;
return Objects.equals( street, address.street ) &&
Objects.equals( number, address.number ) &&
Objects.equals( postalCode, address.postalCode );
}
@Override
public int hashCode() {
return Objects.hash( street, number, postalCode );
}
}
CREATE TABLE Address (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
postalCode VARCHAR(255) ,
street VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person (
id BIGINT NOT NULL ,
registrationNumber VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person_Address (
owners_id BIGINT NOT NULL ,
addresses_id BIGINT NOT NULL
)
ALTER TABLE Person
ADD CONSTRAINT UK_23enodonj49jm8uwec4i7y37f
UNIQUE (registrationNumber)
ALTER TABLE Person_Address
ADD CONSTRAINT FKm7j0bnabh2yr0pe99il1d066u
FOREIGN KEY (addresses_id) REFERENCES Address
ALTER TABLE Person_Address
ADD CONSTRAINT FKbn86l24gmxdv2vmekayqcsgup
FOREIGN KEY (owners_id) REFERENCES Person
With the helper methods in place, the synchronicity management can be simplified, as you can see in the following example:
@ManyToMany
lifecyclePerson person1 = new Person( "ABC-123" );
Person person2 = new Person( "DEF-456" );
Address address1 = new Address( "12th Avenue", "12A", "4005A" );
Address address2 = new Address( "18th Avenue", "18B", "4007B" );
person1.addAddress( address1 );
person1.addAddress( address2 );
person2.addAddress( address1 );
entityManager.persist( person1 );
entityManager.persist( person2 );
entityManager.flush();
person1.removeAddress( address1 );
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'ABC-123', 1 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '12A', '4005A', '12th Avenue', 2 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '18B', '4007B', '18th Avenue', 3 )
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'DEF-456', 4 )
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 2 )
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 3 )
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 4, 2 )
DELETE FROM Person_Address
WHERE owners_id = 1
INSERT INTO Person_Address ( owners_id, addresses_id )
VALUES ( 1, 3 )
If a bidirectional @OneToMany
association performs better when removing or changing the order of child elements,
the @ManyToMany
relationship cannot benefit from such an optimization because the foreign key side is not in control.
To overcome this limitation, the link table must be directly exposed and the @ManyToMany
association split into two bidirectional @OneToMany
relationships.
Bidirectional many-to-many with a link entity
To most natural @ManyToMany
association follows the same logic employed by the database schema,
and the link table has an associated entity which controls the relationship for both sides that need to be joined.
@Entity(name = "Person")
public static class Person implements Serializable {
@Id
@GeneratedValue
private Long id;
@NaturalId
private String registrationNumber;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PersonAddress> addresses = new ArrayList<>();
public Person() {
}
public Person(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
public Long getId() {
return id;
}
public List<PersonAddress> getAddresses() {
return addresses;
}
public void addAddress(Address address) {
PersonAddress personAddress = new PersonAddress( this, address );
addresses.add( personAddress );
address.getOwners().add( personAddress );
}
public void removeAddress(Address address) {
PersonAddress personAddress = new PersonAddress( this, address );
address.getOwners().remove( personAddress );
addresses.remove( personAddress );
personAddress.setPerson( null );
personAddress.setAddress( null );
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Person person = (Person) o;
return Objects.equals( registrationNumber, person.registrationNumber );
}
@Override
public int hashCode() {
return Objects.hash( registrationNumber );
}
}
@Entity(name = "PersonAddress")
public static class PersonAddress implements Serializable {
@Id
@ManyToOne
private Person person;
@Id
@ManyToOne
private Address address;
public PersonAddress() {
}
public PersonAddress(Person person, Address address) {
this.person = person;
this.address = address;
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
PersonAddress that = (PersonAddress) o;
return Objects.equals( person, that.person ) &&
Objects.equals( address, that.address );
}
@Override
public int hashCode() {
return Objects.hash( person, address );
}
}
@Entity(name = "Address")
public static class Address implements Serializable {
@Id
@GeneratedValue
private Long id;
private String street;
@Column(name = "`number`")
private String number;
private String postalCode;
@OneToMany(mappedBy = "address", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PersonAddress> owners = new ArrayList<>();
public Address() {
}
public Address(String street, String number, String postalCode) {
this.street = street;
this.number = number;
this.postalCode = postalCode;
}
public Long getId() {
return id;
}
public String getStreet() {
return street;
}
public String getNumber() {
return number;
}
public String getPostalCode() {
return postalCode;
}
public List<PersonAddress> getOwners() {
return owners;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Address address = (Address) o;
return Objects.equals( street, address.street ) &&
Objects.equals( number, address.number ) &&
Objects.equals( postalCode, address.postalCode );
}
@Override
public int hashCode() {
return Objects.hash( street, number, postalCode );
}
}
CREATE TABLE Address (
id BIGINT NOT NULL ,
number VARCHAR(255) ,
postalCode VARCHAR(255) ,
street VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE Person (
id BIGINT NOT NULL ,
registrationNumber VARCHAR(255) ,
PRIMARY KEY ( id )
)
CREATE TABLE PersonAddress (
person_id BIGINT NOT NULL ,
address_id BIGINT NOT NULL ,
PRIMARY KEY ( person_id, address_id )
)
ALTER TABLE Person
ADD CONSTRAINT UK_23enodonj49jm8uwec4i7y37f
UNIQUE (registrationNumber)
ALTER TABLE PersonAddress
ADD CONSTRAINT FK8b3lru5fyej1aarjflamwghqq
FOREIGN KEY (person_id) REFERENCES Person
ALTER TABLE PersonAddress
ADD CONSTRAINT FK7p69mgialumhegyl4byrh65jk
FOREIGN KEY (address_id) REFERENCES Address
Both the Person
and the Address
have a mappedBy
@OneToMany
side, while the PersonAddress
owns the person
and the address
@ManyToOne
associations.
Because this mapping is formed out of two bidirectional associations, the helper methods are even more relevant.
The aforementioned example uses a Hibernate specific mapping for the link entity since JPA doesn’t allow building a composite identifier out of multiple |
The entity state transitions are better managed than in the previous bidirectional @ManyToMany
case.
Person person1 = new Person( "ABC-123" );
Person person2 = new Person( "DEF-456" );
Address address1 = new Address( "12th Avenue", "12A", "4005A" );
Address address2 = new Address( "18th Avenue", "18B", "4007B" );
entityManager.persist( person1 );
entityManager.persist( person2 );
entityManager.persist( address1 );
entityManager.persist( address2 );
person1.addAddress( address1 );
person1.addAddress( address2 );
person2.addAddress( address1 );
entityManager.flush();
log.info( "Removing address" );
person1.removeAddress( address1 );
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'ABC-123', 1 )
INSERT INTO Person ( registrationNumber, id )
VALUES ( 'DEF-456', 2 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '12A', '4005A', '12th Avenue', 3 )
INSERT INTO Address ( number, postalCode, street, id )
VALUES ( '18B', '4007B', '18th Avenue', 4 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 1, 3 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 1, 4 )
INSERT INTO PersonAddress ( person_id, address_id )
VALUES ( 2, 3 )
DELETE FROM PersonAddress
WHERE person_id = 1 AND address_id = 3
There is only one delete statement executed because, this time, the association is controlled by the @ManyToOne
side which only has to monitor the state of the underlying foreign key relationship to trigger the right DML statement.
@NotFound
association mapping
When dealing with associations which are not enforced by a Foreign Key, it’s possible to bump into inconsistencies if the child record cannot reference a parent entity.
By default, Hibernate will complain whenever a child association references a non-existing parent record.
However, you can configure this behavior so that Hibernate can ignore such an Exception and simply assign null
as a parent object referenced.
To ignore non-existing parent entity references, even though not really recommended, it’s possible to use the annotation org.hibernate.annotation.NotFound
annotation with a value of org.hibernate.annotations.NotFoundAction.IGNORE
.
Considering the following City
and Person
entity mappings:
@NotFound
mapping example@Entity
@Table( name = "Person" )
public static class Person {
@Id
private Long id;
private String name;
private String cityName;
@ManyToOne( fetch = FetchType.LAZY )
@NotFound ( action = NotFoundAction.IGNORE )
@JoinColumn(
name = "cityName",
referencedColumnName = "name",
insertable = false,
updatable = false
)
private City city;
//Getters and setters are omitted for brevity
}
@Entity
@Table( name = "City" )
public static class City implements Serializable {
@Id
@GeneratedValue
private Long id;
private String name;
//Getters and setters are omitted for brevity
}
If we have the following entities in our database:
@NotFound
mapping exampleCity _NewYork = new City();
_NewYork.setName( "New York" );
entityManager.persist( _NewYork );
Person person = new Person();
person.setId( 1L );
person.setName( "John Doe" );
person.setCityName( "New York" );
entityManager.persist( person );
When loading the Person
entity, Hibernate is able to locate the associated City
parent entity:
@NotFound
find existing entity examplePerson person = entityManager.find( Person.class, 1L );
assertEquals( "New York", person.getCity().getName() );
However, if we change the cityName
attribute to a non-existing city:
@NotFound
change to non-existing City exampleperson.setCityName( "Atlantis" );
Hibernate is not going to throw any exception, and it will assign a value of null
for the non-existing City
entity reference:
@NotFound
find non-existing City examplePerson person = entityManager.find( Person.class, 1L );
assertEquals( "Atlantis", person.getCityName() );
assertNull( null, person.getCity() );