Hibernate: Różnice pomiędzy wersjami

Z An IT Expert PKBase
*>Kprzybylowicz
 
m 1 wersja
 
(Brak różnic)

Aktualna wersja na dzień 14:09, 19 maj 2026

Przekierowanie do:

Pod żadnym pozorem nie uzurpuję sobie praw do poniższych treści. To cenna wiedza do której czesto sięgam i o którą czasem jestem pytany, wiec sobie troche to ułatwiłem opracowując w moim rodzimym języku treści dostępne w oryginale na stronie oficjalnej dokumentacji Hibernate.org 7.3, starałem sie zapewnić dobrą jakość przekładu.

3.8 Asocjacje

Chcesz zapanować nad relacjami? Używaj Hibernate :)

@ManyToOne

Najczęstsze skojarzenie, posiadające bezpośredni odpowiednik w relacyjnej bazie danych, a więc adnotacja ta ustanawia relację między podmiotem potomnym a rodzicem. Każdy podmiot ma swój własny cykl życia. Po ustawieniu skojarzenia @ManyToOne, Hibernate dodaje kolumnę powiązanego klucza obcego.

Person 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

Łączy podmiot nadrzędny z jednym lub większą liczbą podmiotów potomnych. Jeśli @OneToMany nie ma odwrotnego skojarzenia @ManyToOne po stronie dziecka, to skojarzenie @OneToMany jest jednokierunkowe. Jeśli istnieje @ManyToOne po stronie dziecka, skojarzenie @OneToMany jest dwukierunkowe, a twórca aplikacji może poruszać się po tej relacji z obu stron.

Jednokierunkowy @OneToMany

W jednokierunkowym skojarzeniu @OneToMany, Hibernate tworzy połączenia między dwoma obiektami.

@Entity(name = "Person")
public static class Person {

	@Id
	@GeneratedValue
	private Long id;

	@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
	private List<Phone> phones = new ArrayList<>();

	//Getters and setters are omitted for brevity

}

@Entity(name = "Phone")
public static class Phone {

	@Id
	@GeneratedValue
	private Long id;

	@Column(name = "`number`")
	private String number;

	//Getters and setters are omitted for brevity

}
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

Asocjacja @OneToMany jest z definicji asocjacją nadrzędną, niezależnie od tego, czy jest jedno, czy dwukierunkowa. Tylko strona nadrzędna asocjacji ma sens, aby kaskadować przejścia stanu encji do jej elementów podrzędnych.

Person 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

Podczas utrwalania encji Person, kaskada rozpropaguje operację utrwalania również do bazowych elementów potomnych Phone. Po usunięciu elementu Phone z kolekcji phones, wiersz skojarzenia jest usuwany z tabeli linków, a atrybut orphanRemoval również spowoduje usunięcie elementu Phone. Jednokierunkowe powiązania nie są zbyt wydajne w przypadku usuwania encji podrzędnych. W powyższym przykładzie, po opróżnieniu kontekstu utrwalania, Hibernate usuwa wszystkie wiersze bazy danych z tabeli linków (np. Person_Phone), które są powiązane z nadrzędną encją Person, i ponownie wstawia te, które nadal znajdują się w kolekcji @OneToMany. Z drugiej strony, dwukierunkowe powiązanie @OneToMany jest znacznie wydajniejsze, ponieważ encja podrzędna kontroluje powiązanie.

Dwukierunkowe powiązanie @OneToMany

Dwukierunkowe powiązanie @OneToMany wymaga również powiązania @ManyToOne po stronie potomnej. Chociaż model domeny udostępnia dwie strony do nawigacji po tym powiązaniu, w tle relacyjna baza danych ma tylko jeden klucz obcy dla tej relacji.

Każde powiązanie dwukierunkowe musi mieć tylko jedną stronę właścicielską (stronę potomną), a druga jest określana jako strona odwrotna (lub strona mappedBy).

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

	//Getters and setters are omitted for brevity

	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;

	//Getters and setters are omitted for brevity

	@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

Za każdym razem, gdy tworzone jest skojarzenie dwukierunkowe, programista aplikacji musi upewnić się, że obie strony są zawsze zsynchronizowane. Metody addPhone() i removePhone() to metody narzędziowe, które synchronizują obie strony za każdym razem, gdy dodawany lub usuwany jest element podrzędny. Ponieważ klasa Phone ma kolumnę @NaturalId (numer telefonu jest unikalny), equals() i hashCode() mogą korzystać z tej właściwości, a logika removePhone() zostaje zredukowana do metody remove() z Java Collection.

W przeciwieństwie do jednokierunkowego @OneToMany, dwukierunkowe skojarzenie jest znacznie bardziej wydajne w zarządzaniu stanem trwałości kolekcji. Każde usunięcie elementu wymaga tylko jednej aktualizacji (w której kolumna klucza obcego jest ustawiana na NULL), a jeśli cykl życia encji potomnej jest powiązany z jej elementem nadrzędnym, tak że encja potomna nie może istnieć bez niego, wówczas możemy opatrzyć skojarzenie adnotacją atrybutu orphanRemoval, a usunięcie encji potomnej spowoduje również instrukcję delete dla wiersza tabeli potomnej.

@OneToOne

Asocjacja @OneToOne może być jednokierunkowa lub dwukierunkowa. Asocjacja jednokierunkowa jest zgodna z semantyką klucza obcego relacyjnej bazy danych, a właścicielem relacji jest strona klienta. Asocjacja dwukierunkowa zawiera również stronę nadrzędną mappedBy @OneToOne.

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

	//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;

	//Getters and setters are omitted for brevity

}
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

Z punktu widzenia relacyjnej bazy danych, schemat bazowy jest identyczny z jednokierunkowym skojarzeniem @ManyToOne, ponieważ strona klienta kontroluje relację w oparciu o kolumnę klucza obcego. Nietypowe jest jednak traktowanie telefonu jako strony klienta, a szczegółów telefonu jako strony nadrzędnej, ponieważ szczegóły nie mogą istnieć bez faktycznego telefonu. Znacznie bardziej naturalnym odwzorowaniem byłoby umieszczenie telefonu jako strony nadrzędnej, a zatem umieszczenie klucza obcego w tabeli szczegółów telefonu.

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

	//Getters and setters are omitted for brevity

	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;

	//Getters and setters are omitted for brevity

}
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

Tym razem właścicielem skojarzenia jest PhoneDetails i, jak w przypadku każdego dwukierunkowego skojarzenia, strona nadrzędna może propagować swój cykl życia do strony podrzędnej poprzez kaskadowanie.

Phone 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 )

Podczas korzystania z dwukierunkowego skojarzenia @OneToOne, Hibernate wymusza ograniczenie unikalności podczas pobierania strony potomnej. Jeśli z tym samym obiektem nadrzędnym powiązanych jest więcej niż jeden obiekt potomny, Hibernate zgłosi wyjątek org.hibernate.exception.ConstraintViolationException. Np, podczas dodawania kolejnego obiektu PhoneDetails, Hibernate weryfikuje ograniczenie unikalności podczas ponownego ładowania obiektu Phone.

PhoneDetails otherDetails = new PhoneDetails( "T-Mobile", "CDMA" );
otherDetails.setPhone( phone );
entityManager.persist( otherDetails );
entityManager.flush();
entityManager.clear();

//throws jakarta.persistence.PersistenceException: org.hibernate.HibernateException: More than one row with the given identifier was found: 1
phone = entityManager.find( Phone.class, phone.getId() );
Dwukierunkowe, leniwe skojarzenie @OneToOne

Chociaż można dodać adnotację do skojarzenia po stronie nadrzędnej, aby było ono pobierane leniwie, Hibernate nie może zrealizować tego żądania, ponieważ nie wie, czy skojarzenie jest puste, czy nie.

Jedynym sposobem na sprawdzenie, czy po stronie podrzędnej istnieje skojarzenie, jest pobranie skojarzenia potomnego za pomocą zapytania pomocniczego. Ponieważ może to prowadzić do problemów z zapytaniami N+1, znacznie bardziej efektywne jest użycie jednokierunkowych skojarzeń @OneToOne z adnotacją @MapsId.

Jeśli jednak naprawdę potrzebujesz użyć skojarzenia dwukierunkowego i chcesz mieć pewność, że zawsze będzie ono pobierane leniwie, musisz włączyć rozszerzenie bajtkodu inicjalizacji stanu leniwego.

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

	//Getters and setters are omitted for brevity

	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;

	//Getters and setters are omitted for brevity

}
@ManyToMany

Asocjacja @ManyToMany wymaga tabeli linków, która łączy dwie encje. Podobnie jak asocjacja @OneToMany, @ManyToMany może być jednokierunkowa lub dwukierunkowa.

Jednokierunkowy @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<>();

	//Getters and setters are omitted for brevity

}

@Entity(name = "Address")
public static class Address {

	@Id
	@GeneratedValue
	private Long id;

	private String street;

	@Column(name = "`number`")
	private String number;

	//Getters and setters are omitted for brevity

}
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

Podobnie jak w przypadku jednokierunkowych skojarzeń @OneToMany, tabela linków jest kontrolowana przez stronę posiadającą. Gdy encja jest usuwana z kolekcji @ManyToMany, Hibernate po prostu usuwa rekord łączenia z tabeli linków. Niestety, operacja ta wymaga usunięcia wszystkich wpisów powiązanych z danym elementem nadrzędnym i ponownego utworzenia tych, które są wymienione w bieżącym kontekście utwalania.

Person 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 )
W przypadku asocjacji @ManyToMany kaskadowanie przejścia stanu encji REMOVE nie ma sensu, ponieważ będzie ono propagowane poza tabelę linków. Ponieważ do drugiej strony mogą odwoływać się inne encje po stronie nadrzędnej, automatyczne usunięcie może zakończyć się wyjątkiem ConstraintViolationException.
Na przykład, gdyby zdefiniowano @ManyToMany(cascade = CascadeType.ALL) i pierwsza osoba zostałaby usunięta, Hibernate zgłosiłby wyjątek, ponieważ inna osoba nadal jest powiązana z usuwanym adresem.
Person person1 = entityManager.find(Person.class, personId);
entityManager.remove(person1);

Caused by: jakarta.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: could not execute statement
Caused by: org.hibernate.exception.ConstraintViolationException: could not execute statement
Caused by: java.sql.SQLIntegrityConstraintViolationException: integrity constraint violation: foreign key no action; FKM7J0BNABH2YR0PE99IL1D066U table: PERSON_ADDRESS

Poprzez proste usunięcie strony nadrzędnej Hibernate może bezpiecznie usunąć powiązane rekordy łączy, jak widać w poniższym przykładzie:

Person person1 = entityManager.find( Person.class, personId );
entityManager.remove( person1 );
DELETE FROM Person_Address
WHERE  Person_id = 1

DELETE FROM Person
WHERE  id = 1
Dwukierunkowy @ManyToMany

Dwukierunkowe skojarzenie @ManyToMany ma stronę „właściciel” i „mappedBy”. Aby zachować synchronizację między obiema stronami, warto zapewnić metody pomocnicze do dodawania lub usuwania encji podrzędnych.

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

	//Getters and setters are omitted for brevity

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

	//Getters and setters are omitted for brevity

	@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


Dzięki zastosowaniu metod pomocniczych zarządzanie synchronizacją może zostać uproszczone, co widać w poniższym przykładzie:

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");

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 )

Jeśli dwukierunkowe skojarzenie @OneToMany działa lepiej podczas usuwania lub zmiany kolejności elementów potomnych, relacja @ManyToMany nie może skorzystać z takiej optymalizacji, ponieważ klucz obcy nie ma nad nią kontroli. Aby obejść to ograniczenie, tabela linków musi być bezpośrednio ujawniona, a skojarzenie @ManyToMany musi zostać podzielone na dwie dwukierunkowe relacje @OneToMany.

Dwukierunkowa komunikacja wiele-do-wielu z jednostką łącza

Najbardziej naturalne skojarzenie @ManyToMany opiera się na tej samej logice, co schemat bazy danych, a tabela łączy ma skojarzoną encję, która kontroluje relację dla obu stron, które mają zostać połączone.

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

	//Getters and setters are omitted for brevity

	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;

	//Getters and setters are omitted for brevity

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

	//Getters and setters are omitted for brevity

	@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

Zarówno Person, jak i Address mają stronę mappedBy @OneToMany, podczas gdy PersonAddress jest właścicielem osoby i adresu @ManyToOne. Ponieważ to mapowanie składa się z dwóch dwukierunkowych skojarzeń, metody pomocnicze są jeszcze bardziej istotne.

W powyższym przykładzie wykorzystano mapowanie specyficzne dla Hibernate dla encji łącza, ponieważ Jakarta Persistence nie pozwala na utworzenie identyfikatora złożonego z wielu powiązań @ManyToOne.
Aby uzyskać więcej informacji, zapoznaj się z sekcją dotyczącą identyfikatorów złożonych z powiązaniami.

Przejścia stanów encji są lepiej zarządzane niż w poprzednim przypadku dwukierunkowego @ManyToMany.

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

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

Wykonana zostaje tylko jedna instrukcja usunięcia, ponieważ tym razem powiązanie jest kontrolowane przez stronę @ManyToOne, która musi jedynie monitorować stan podstawowej relacji klucza obcego, aby wywołać odpowiednią instrukcję DML.

@NotFound

W przypadku skojarzeń, które nie są wymuszane przez fizyczny klucz obcy, istnieje możliwość, że wartość klucza obcego inna niż null będzie wskazywać na nieistniejącą wartość w tabeli skojarzonej jednostki.

Zdecydowanie odradza się egzekwowanie stosowania fizycznych kluczy obcych na poziomie bazy danych.

Hibernate zapewnia obsługę takich modeli, korzystając z adnotacji @NotFound, która akceptuje wartość NotFoundAction wskazującą sposób zachowania się Hibernate w przypadku napotkania takich uszkodzonych kluczy obcych.

EXCEPTION
(default) Hibernate zuci wyjątek (FetchNotFoundException)
IGNORE
powiązanie będzie traktowane jako null

Zarówno @NotFound(IGNORE), jak i @NotFound(EXCEPTION) powodują, że Hibernate zakłada brak fizycznego klucza obcego.

Powiązania @ManyToOne i @OneToOne z adnotacją @NotFound są zawsze pobierane chciwie, nawet jeśli strategia pobierania jest ustawiona na FetchType.LAZY.

Jeśli aplikacja sama zarządza integralnością referencyjną i może zagwarantować brak uszkodzonych kluczy obcych, można zamiast tego użyć metody jakarta.persistence.ForeignKey(NO_CONSTRAINT). Zmusi to Hibernate do nieeksportowania fizycznych kluczy obcych, ale nadal będzie zachowywać się tak, jakby istniał, unikając wad @NotFound.

Biorąc pod uwagę następujące mapowania jednostek Miasto i Osoba:

@Entity(name = "Person")
@Table(name = "Person")
public static class Person {

	@Id
	private Integer id;
	private String name;

	@ManyToOne
	@NotFound(action = NotFoundAction.IGNORE)
	@JoinColumn(name = "city_fk", referencedColumnName = "id")
	private City city;

	//Getters and setters are omitted for brevity

}

@Entity(name = "City")
@Table(name = "City")
public static class City implements Serializable {

	@Id
	private Integer id;

	private String name;

	//Getters and setters are omitted for brevity

}

Jeżeli w naszej bazie danych znajdują się następujące podmioty:

City newYork = new City( 1, "New York" );
entityManager.persist( newYork );

Person person = new Person( 1, "John Doe", newYork );
entityManager.persist( person );

Podczas ładowania jednostki Person, Hibernate jest w stanie zlokalizować powiązaną jednostkę nadrzędną City:

Person person = entityManager.find( Person.class, 1 );
assertEquals( "New York", person.getCity().getName() );

Jeśli jednak złamiemy klucz obcy:

// the database allows this because there is no physical foreign-key
entityManager.createQuery( "delete City" ).executeUpdate();

Hibernate nie zgłosi żadnego wyjątku i przypisze wartość null do nieistniejącego odniesienia do encji City:

Person person = entityManager.find( Person.class, 1 );

assertNull( person.getCity(), "person.getCity() should be null" );

@NotFound wpływa również na sposób traktowania skojarzenia jako „dołączeń niejawnych” w HQL i kryteriach. W przypadku fizycznego klucza obcego, Hibernate może bezpiecznie założyć, że wartość w kolumnie (kolumnach) klucza obcego będzie zgodna z wartością w kolumnie (kolumnach) docelowej (docelowych), ponieważ baza danych upewnia się, że tak jest. Jednak @NotFound wymusza na Hibernate wykonanie dołączenia fizycznego dla dołączeń niejawnych, gdy w innym przypadku mogłoby to nie być konieczne.

Korzystając z modelu Person/City, rozważmy zapytanie z Person p, gdzie p.city.id ma wartość null.

Normalnie Hibernate nie potrzebowałby łączenia między tabelą Person a tabelą City, ponieważ fizyczny klucz obcy zapewniałby, że każda wartość różna od null w kolumnie Person.cityName miałaby odpowiadającą jej wartość różną od null w kolumnie City.name.

Jednak w przypadku mapowań @NotFound możliwe jest zerwanie skojarzenia, ponieważ nie ma fizycznego klucza obcego wymuszającego relację. Jak widać w sekcji „Zerwanie klucza obcego”, kolumna Person.cityName dla Johna Doe została zmieniona z „Nowy Jork” na „Atlantis”, mimo że w bazie danych nie ma miasta o nazwie „Atlantis”. Hibernate nie może zaufać wartości klucza obcego („Atlantis”), która ma odpowiadającą wartość docelową, więc musi łączyć się z tabelą City, aby rozwiązać wartość city.id.

final List<Person> nullResults = entityManager
		.createQuery( "from Person p where p.city.id is null", Person.class )
		.getResultList();
assertThat( nullResults ).isEmpty();

final List<Person> nonNullResults = entityManager
		.createQuery( "from Person p where p.city.id is not null", Person.class )
		.getResultList();
assertThat( nonNullResults ).isEmpty();

Żaden z wyników nie zawiera dopasowania dla „John Doe”, ponieważ sprzężenie wewnętrzne filtruje ten wiersz. Hibernate obsługuje metodę odwołania się bezpośrednio do kolumny klucza (Person.cityName) w zapytaniu za pomocą specjalnej funkcji fk(..). Np.

final List<String> nullResults = entityManager
		.createQuery( "select p.name from Person p where fk( p.city ) is null", String.class )
		.getResultList();

assertThat( nullResults ).isEmpty();

final List<String> nonNullResults = entityManager
		.createQuery( "select p.name from Person p left join p.city c where fk( c ) is not null", String.class )
		.getResultList();
assertThat( nonNullResults ).hasSize( 1 );
assertThat( nonNullResults.get( 0 ) ).isEqualTo( "John Doe" );