Устойчивост на Java с JPA и Hibernate, Част 2: Взаимоотношения много към много

Първата половина на този урок представи основите на API за персистентност на Java и ви показа как да конфигурирате JPA приложение, използвайки Hibernate 5.3.6 и Java 8. Ако сте прочели този урок и сте проучили неговото примерно приложение, тогава знаете основите на моделиране на JPA обекти и взаимоотношения много към едно в JPA. Имали сте и практика за писане на поименни заявки с JPA Query Language (JPQL).

В тази втора половина на урока ще се задълбочим с JPA и Hibernate. Ще научите как да моделирате връзка много към много между Movieи SuperHeroобекти, да настроите отделни хранилища за тези обекти и да запазите обектите в базата данни H2 в паметта. Също така ще научите повече за ролята на каскадните операции в JPA и ще получите съвети за избор на CascadeTypeстратегия за обекти в базата данни. Накрая ще съберем работещо приложение, което можете да стартирате във вашата IDE или в командния ред.

Този урок се фокусира върху основите на JPA, но не забравяйте да разгледате тези съвети за Java, представящи по-напреднали теми в JPA:

  • Наследствени отношения в JPA и Hibernate
  • Композитни клавиши в JPA и Hibernate
изтегляне Вземете кода Изтеглете изходния код за примерни приложения, използвани в този урок. Създадено от Стивън Хейнс за JavaWorld.

Взаимоотношения много към много в JPA

Взаимоотношенията много към много дефинират обекти, за които и двете страни на връзката могат да имат множество препратки един към друг. За нашия пример ще моделираме филми и супергерои. За разлика от примера Authors & Books от Част 1, филмът може да има множество супергерои, а супергерой може да се появи в множество филми. Нашите супергерои, Ironman и Thor, се появяват в два филма - „Отмъстителите“ и „Отмъстителите: Война безкрайност“.

За да моделираме тази връзка много към много с помощта на JPA, ще са ни необходими три таблици:

  • ФИЛМ
  • SUPER_HERO
  • SUPERHERO_MOVIES

Фигура 1 показва модела на домейна с трите таблици.

Стивън Хейнс

Имайте предвид, че SuperHero_Moviesе присъединят маса между маркерите Movieи SuperHeroтаблици. В JPA таблицата за присъединяване е специален вид таблица, която улеснява връзката много към много.

Еднопосочни или двупосочни?

В JPA използваме @ManyToManyанотацията, за да моделираме връзки много към много. Този тип връзка може да бъде еднопосочна или двупосочна:

  • В еднопосочна връзка само една същност във връзката сочи другата.
  • В двупосочна връзка и двата обекта сочат един към друг.

Нашият пример е двупосочен, което означава, че филмът сочи към всичките си супергерои, а супергероят сочи към всички техни филми. В двупосочна връзка „много към много“ единият обект притежава връзката, а другият е съотнесен към връзката. Използваме mappedByатрибута на @ManyToManyанотацията, за да създадем това картографиране.

Листинг 1 показва изходния код за SuperHeroкласа.

Листинг 1. SuperHero.java

 package com.geekcap.javaworld.jpa.model; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.Table; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @Entity @Table(name = "SUPER_HERO") public class SuperHero { @Id @GeneratedValue private Integer id; private String name; @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) @JoinTable( name = "SuperHero_Movies", joinColumns = {@JoinColumn(name = "superhero_id")}, inverseJoinColumns = {@JoinColumn(name = "movie_id")} ) private Set movies = new HashSet(); public SuperHero() { } public SuperHero(Integer id, String name) { this.id = id; this.name = name; } public SuperHero(String name) { this.name = name; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Set getMovies() { return movies; } @Override public String toString() { return "SuperHero{" + "id=" + id + ", + name +"\'' + ", + movies.stream().map(Movie::getTitle).collect(Collectors.toList()) +"\'' + '}'; } } 

В SuperHeroклас има няколко пояснения, които трябва да бъдат запознати от Част 1:

  • @Entityидентифицира SuperHeroкато JPA обект.
  • @Tableкарти на SuperHeroпредприятието на масата "SUPER_HERO".

Също така обърнете внимание на Integeridполето, което указва, че първичният ключ на таблицата ще бъде генериран автоматично.

След това ще разгледаме @ManyToManyи @JoinTableпоясненията.

Стратегии за извличане

Нещото, което трябва да забележите в @ManyToManyанотацията, е как конфигурираме стратегията за извличане , която може да бъде мързелива или нетърпелива. В този случай, ние задали fetchда EAGER, така че, когато ние изтегли SuperHeroот базата данни, ще се извличат автоматично всички съответстващи му Movieите.

Ако LAZYвместо това избрахме да извършим извличане, щяхме да извлечем само всеки, Movieтъй като той беше специално достъпен. Мързеливо извличане е възможно само докато the SuperHeroе прикрепен към EntityManager; в противен случай достъпът до филми на супергерой ще доведе до изключение. Искаме да имаме достъп до филми на супергерой при поискване, така че в този случай избираме EAGERстратегията за извличане.

CascadeType.PERSIST

Каскадните операции дефинират как супергероите и съответните им филми се запазват в и от базата данни. Има няколко конфигурации от тип каскада, от които можете да избирате и ще говорим повече за тях по-късно в този урок. Засега просто обърнете внимание, че сме задали cascadeатрибута на CascadeType.PERSIST, което означава, че когато запазим супергерой, неговите филми също ще бъдат запазени.

Присъединете се към таблици

JoinTableе клас, който улеснява връзката много към много между SuperHeroи Movie. В този клас дефинираме таблицата, която ще съхранява първичните ключове както за, така SuperHeroи за Movieобектите.

Листинг 1 указва, че името на таблицата ще бъде SuperHero_Movies. В присъединят колона ще бъде superhero_idи обратното присъединят колона ще бъде movie_id. Обектът SuperHeroпритежава връзката, така че колоната за присъединяване ще бъде попълнена с SuperHeroпървичен ключ на. След това колоната за обратно свързване се позовава на обекта от другата страна на връзката, което е Movie.

Въз основа на тези дефиниции в Листинг 1, бихме очаквали създадена нова таблица, наречена SuperHero_Movies. Таблицата ще има две колони:, superhero_idкоято препраща към idколоната на SUPERHEROтаблицата и movie_id, която препраща към idколоната на MOVIEтаблицата.

Класът по филм

Листинг 2 показва изходния код за Movieкласа. Припомнете си, че при двупосочна връзка, единият обект притежава връзката (в случая SuperHero), докато другият е картографиран към връзката. Кодът в Листинг 2 включва съпоставяне на връзки, приложено към Movieкласа.

Листинг 2. Movie.java

 package com.geekcap.javaworld.jpa.model; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.ManyToMany; import javax.persistence.Table; import java.util.HashSet; import java.util.Set; @Entity @Table(name = "MOVIE") public class Movie { @Id @GeneratedValue private Integer id; private String title; @ManyToMany(mappedBy = "movies", cascade = CascadeType.PERSIST, fetch = FetchType.EAGER) private Set superHeroes = new HashSet(); public Movie() { } public Movie(Integer id, String title) { this.id = id; this.title = title; } public Movie(String title) { this.title = title; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Set getSuperHeroes() { return superHeroes; } public void addSuperHero(SuperHero superHero) { superHeroes.add(superHero); superHero.getMovies().add(this); } @Override public String toString() { return "Movie{" + "id=" + id + ", + title +"\'' + '}'; } }

Следните свойства се прилагат към @ManyToManyанотацията в Листинг 2:

  • mappedBy references the field name on the SuperHero class that manages the many-to-many relationship. In this case, it references the movies field, which we defined in Listing 1 with the corresponding JoinTable.
  • cascade is configured to CascadeType.PERSIST, which means that when a Movie is saved its corresponding SuperHero entities should also be saved.
  • fetch tells the EntityManager that it should retrieve a movie's superheroes eagerly: when it loads a Movie, it should also load all corresponding SuperHero entities.

Something else to note about the Movie class is its addSuperHero() method.

When configuring entities for persistence, it isn't enough to simply add a superhero to a movie; we also need to update the other side of the relationship. This means we need to add the movie to the superhero. When both sides of the relationship are configured properly, so that the movie has a reference to the superhero and the superhero has a reference to the movie, then the join table will also be properly populated.

We've defined our two entities. Now let's look at the repositories we'll use to persist them to and from the database.

Tip! Set both sides of the table

It's a common mistake to only set one side of the relationship, persist the entity, and then observe that the join table is empty. Setting both sides of the relationship will fix this.

JPA repositories

Можем да приложим целия си код за постоянство директно в примерното приложение, но създаването на класове на хранилището ни позволява да отделим кода за постоянство от кода на приложението. Точно както направихме с приложението Books & Authors в Част 1, ще създадем EntityManagerи след това ще го използваме, за да инициализираме две хранилища, по едно за всеки обект, който продължаваме.

Листинг 3 показва изходния код за MovieRepositoryкласа.

Листинг 3. MovieRepository.java

 package com.geekcap.javaworld.jpa.repository; import com.geekcap.javaworld.jpa.model.Movie; import javax.persistence.EntityManager; import java.util.List; import java.util.Optional; public class MovieRepository { private EntityManager entityManager; public MovieRepository(EntityManager entityManager) { this.entityManager = entityManager; } public Optional save(Movie movie) { try { entityManager.getTransaction().begin(); entityManager.persist(movie); entityManager.getTransaction().commit(); return Optional.of(movie); } catch (Exception e) { e.printStackTrace(); } return Optional.empty(); } public Optional findById(Integer id) { Movie movie = entityManager.find(Movie.class, id); return movie != null ? Optional.of(movie) : Optional.empty(); } public List findAll() { return entityManager.createQuery("from Movie").getResultList(); } public void deleteById(Integer id) { // Retrieve the movie with this ID Movie movie = entityManager.find(Movie.class, id); if (movie != null) { try { // Start a transaction because we're going to change the database entityManager.getTransaction().begin(); // Remove all references to this movie by superheroes movie.getSuperHeroes().forEach(superHero -> { superHero.getMovies().remove(movie); }); // Now remove the movie entityManager.remove(movie); // Commit the transaction entityManager.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); } } } } 

The MovieRepositoryсе инициализира с EntityManager, след което го записва в променлива член, за да се използва в методите на постоянство. Ще разгледаме всеки от тези методи.

Методи за постоянство

Нека да прегледаме MovieRepositoryметодите за персистиране и да видим как те взаимодействат с EntityManagerметодите за персистиране.