Ketekunan Java dengan JPA dan Hibernate, Bagian 2: Hubungan banyak-ke-banyak

Paruh pertama tutorial ini memperkenalkan dasar-dasar Java Persistence API dan menunjukkan kepada Anda cara mengonfigurasi aplikasi JPA menggunakan Hibernate 5.3.6 dan Java 8. Jika Anda telah membaca tutorial itu dan mempelajari contoh aplikasinya, maka Anda mengetahui dasar-dasar dari pemodelan entitas JPA dan hubungan banyak-ke-satu di JPA. Anda juga telah berlatih menulis kueri bernama dengan JPA Query Language (JPQL).

Di paruh kedua tutorial ini, kita akan membahas JPA dan Hibernate lebih dalam. Anda akan mempelajari cara membuat model hubungan banyak-ke-banyak antara Moviedan SuperHeroentitas, menyiapkan repositori individual untuk entitas ini, dan mempertahankan entitas tersebut ke database dalam memori H2. Anda juga akan mempelajari lebih lanjut tentang peran operasi kaskade di JPA, dan mendapatkan tip untuk memilih CascadeTypestrategi untuk entitas di database. Terakhir, kami akan mengumpulkan aplikasi yang berfungsi yang dapat Anda jalankan di IDE atau di baris perintah.

Tutorial ini berfokus pada dasar-dasar JPA, tetapi pastikan untuk memeriksa tip Java berikut yang memperkenalkan topik lanjutan di JPA:

  • Hubungan pewarisan di JPA dan Hibernate
  • Kunci komposit di JPA dan Hibernate
download Dapatkan kode Download kode sumber untuk aplikasi contoh yang digunakan dalam tutorial ini. Dibuat oleh Steven Haines untuk JavaWorld.

Hubungan banyak ke banyak di JPA

Relasi banyak ke banyak menentukan entitas yang kedua sisi relasinya dapat memiliki banyak referensi satu sama lain. Sebagai contoh, kami akan membuat model film dan pahlawan super. Tidak seperti contoh Penulis & Buku dari Bagian 1, sebuah film dapat memiliki beberapa pahlawan super, dan seorang pahlawan super dapat muncul di beberapa film. Pahlawan super kita, Ironman dan Thor, keduanya muncul dalam dua film, "The Avengers" dan "Avengers: Infinity War."

Untuk memodelkan hubungan banyak-ke-banyak ini menggunakan JPA, kita memerlukan tiga tabel:

  • FILM
  • SUPER HERO
  • SUPERHERO_MOVIES

Gambar 1 menunjukkan model domain dengan tiga tabel.

Steven Haines

Perhatikan bahwa SuperHero_Moviesadalah bergabung meja antara Moviedan SuperHerotabel. Di JPA, tabel gabungan adalah jenis tabel khusus yang memfasilitasi hubungan banyak-ke-banyak.

Searah atau dua arah?

Di JPA kami menggunakan @ManyToManyanotasi untuk memodelkan hubungan banyak-ke-banyak. Jenis hubungan ini bisa searah atau dua arah:

  • Dalam hubungan searah, hanya satu entitas dalam hubungan yang menunjuk ke entitas lainnya.
  • Dalam hubungan dua arah, kedua entitas menunjuk satu sama lain.

Contoh kami adalah dua arah, yang berarti bahwa sebuah film menunjuk ke semua pahlawan supernya, dan seorang pahlawan super menunjuk ke semua film mereka. Dalam hubungan dua arah, banyak-ke-banyak, satu entitas memiliki hubungan dan entitas lainnya dipetakan ke hubungan. Kami menggunakan mappedByatribut @ManyToManyanotasi untuk membuat pemetaan ini.

Kode 1 menunjukkan kode sumber untuk SuperHerokelas.

Daftar 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()) +"\'' + '}'; } } 

The SuperHerokelas memiliki beberapa penjelasan yang harus akrab dari Bagian 1:

  • @Entitymengidentifikasi SuperHerosebagai entitas JPA.
  • @Tablememetakan SuperHeroentitas ke tabel "SUPER_HERO".

Perhatikan juga Integeridbidangnya, yang menentukan bahwa kunci utama tabel akan dibuat secara otomatis.

Selanjutnya kita akan melihat anotasi @ManyToManydan @JoinTable.

Strategi pengambilan

Hal yang perlu diperhatikan dalam @ManyToManyanotasi adalah cara kami mengonfigurasi strategi pengambilan , yang bisa malas atau bersemangat. Dalam hal ini, kita telah menyetel fetchke EAGER, sehingga ketika kita mengambil SuperHerodari database, kita juga akan secara otomatis mengambil semua yang sesuai Movie.

Jika kami memilih untuk melakukan LAZYpengambilan, kami hanya akan mengambil masing Movie- masing seperti yang diakses secara khusus. Pengambilan malas hanya mungkin jika SuperHerodilampirkan ke EntityManager; jika tidak mengakses film pahlawan super akan menimbulkan pengecualian. Kami ingin dapat mengakses film pahlawan super sesuai permintaan, jadi dalam hal ini kami memilih EAGERstrategi pengambilan.

CascadeType.PERSIST

Operasi bertingkat menentukan bagaimana pahlawan super dan filmnya yang terkait disimpan ke dan dari database. Ada sejumlah konfigurasi tipe kaskade yang dapat dipilih, dan kita akan membicarakannya lebih banyak nanti di tutorial ini. Untuk saat ini, perhatikan saja bahwa kami telah menyetel cascadeatribut ke CascadeType.PERSIST, yang berarti bahwa ketika kami menyimpan pahlawan super, filmnya juga akan disimpan.

Bergabunglah dengan tabel

JoinTableadalah kelas yang memfasilitasi hubungan banyak-ke-banyak antara SuperHerodan Movie. Di kelas ini, kami mendefinisikan tabel yang akan menyimpan kunci primer untuk entitas SuperHerodan Movieentitas.

Kode 1 menentukan bahwa nama tabel akan SuperHero_Movies. The bergabung kolom akan superhero_id, dan inverse bergabung kolom akan movie_id. The SuperHeroentitas memiliki hubungan, sehingga bergabung kolom akan diisi dengan SuperHero's kunci utama. Kolom gabungan terbalik kemudian mereferensikan entitas di sisi lain hubungan, yaitu Movie.

Berdasarkan definisi ini di Listing 1, kami mengharapkan sebuah tabel baru dibuat, bernama SuperHero_Movies. Tabel akan memiliki dua kolom:, superhero_idyang mereferensikan idkolom SUPERHEROtabel, dan movie_id, yang mereferensikan idkolom MOVIEtabel.

Kelas Film

Kode 2 menunjukkan kode sumber untuk Moviekelas. Ingatlah bahwa dalam hubungan dua arah, satu entitas memiliki hubungan (dalam kasus ini, SuperHero) sementara entitas lainnya dipetakan ke hubungan tersebut. Kode dalam Listing 2 mencakup pemetaan hubungan yang diterapkan ke Moviekelas.

Daftar 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 +"\'' + '}'; } }

Properti berikut ini diterapkan ke @ManyToManypenjelasan di Listing 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

Kita bisa mengimplementasikan semua kode persistensi kita langsung di aplikasi contoh, tetapi membuat kelas repositori memungkinkan kita memisahkan kode persistensi dari kode aplikasi. Sama seperti yang kami lakukan dengan aplikasi Buku & Penulis di Bagian 1, kami akan membuat EntityManagerdan menggunakannya untuk menginisialisasi dua repositori, satu untuk setiap entitas yang kami pertahankan.

Kode 3 menunjukkan kode sumber untuk MovieRepositorykelas.

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

Ini MovieRepositorydiinisialisasi dengan EntityManager, lalu menyimpannya ke variabel anggota untuk digunakan dalam metode persistensi. Kami akan mempertimbangkan masing-masing metode ini.

Metode persistensi

Mari kita tinjau MovieRepositorymetode persistensi dan lihat bagaimana mereka berinteraksi dengan EntityManagermetode persistensi.