Proyeksi JPA Data Musim Semi

1. Ikhtisar

Saat menggunakan Spring Data JPA untuk mengimplementasikan lapisan persistensi, repositori biasanya menampilkan satu atau beberapa instance dari kelas root. Namun, lebih sering daripada tidak, kita tidak membutuhkan semua properti dari objek yang dikembalikan.

Dalam kasus seperti itu, mungkin diinginkan untuk mengambil data sebagai objek dari tipe yang disesuaikan. Jenis ini mencerminkan tampilan parsial dari kelas root, yang hanya berisi properti yang kami pedulikan. Di sinilah proyeksi berguna.

2. Pengaturan Awal

Langkah pertama adalah menyiapkan proyek dan mengisi database.

2.1. Dependensi Maven

Untuk dependensi, lihat bagian 2 dari tutorial ini.

2.2. Kelas Entitas

Mari kita tentukan dua kelas entitas:

@Entity public class Address { @Id private Long id; @OneToOne private Person person; private String state; private String city; private String street; private String zipCode; // getters and setters }

Dan:

@Entity public class Person { @Id private Long id; private String firstName; private String lastName; @OneToOne(mappedBy = "person") private Address address; // getters and setters }

Hubungan antara entitas Person dan Address adalah dua arah satu-ke-satu: Alamat adalah sisi pemilik, dan Person adalah sisi terbalik.

Perhatikan dalam tutorial ini, kami menggunakan database tertanam - H2.

Saat database tertanam dikonfigurasi, Spring Boot secara otomatis membuat tabel yang mendasari untuk entitas yang kami tentukan.

2.3. Skrip SQL

Kami menggunakan skrip projection-insert-data.sql untuk mengisi kedua tabel pendukung:

INSERT INTO person(id,first_name,last_name) VALUES (1,'John','Doe'); INSERT INTO address(id,person_id,state,city,street,zip_code) VALUES (1,1,'CA', 'Los Angeles', 'Standford Ave', '90001');

Untuk membersihkan database setelah setiap pengujian dijalankan, kita dapat menggunakan skrip lain, bernama projection-clean-up-data.sql :

DELETE FROM address; DELETE FROM person;

2.4. Kelas Tes

Untuk mengonfirmasi bahwa proyeksi menghasilkan data yang benar, kita memerlukan kelas pengujian:

@DataJpaTest @RunWith(SpringRunner.class) @Sql(scripts = "/projection-insert-data.sql") @Sql(scripts = "/projection-clean-up-data.sql", executionPhase = AFTER_TEST_METHOD) public class JpaProjectionIntegrationTest { // injected fields and test methods }

Dengan anotasi yang diberikan, Spring Boot membuat database, memasukkan dependensi, dan mengisi serta membersihkan tabel sebelum dan setelah setiap eksekusi metode pengujian.

3. Proyeksi Berbasis Antarmuka

Saat memproyeksikan entitas, wajar jika mengandalkan antarmuka, karena kami tidak perlu menyediakan implementasi.

3.1. Proyeksi Tertutup

Melihat kembali kelas Alamat , kita dapat melihat itu memiliki banyak properti, namun tidak semuanya berguna. Misalnya, terkadang kode pos cukup untuk menunjukkan alamat.

Mari deklarasikan antarmuka proyeksi untuk kelas Alamat :

public interface AddressView { String getZipCode(); }

Kemudian gunakan di antarmuka repositori:

public interface AddressRepository extends Repository { List getAddressByState(String state); }

Sangat mudah untuk melihat bahwa mendefinisikan metode repositori dengan antarmuka proyeksi hampir sama dengan kelas entitas.

Satu-satunya perbedaan adalah bahwa antarmuka proyeksi, bukan kelas entitas, digunakan sebagai jenis elemen dalam koleksi yang dikembalikan.

Mari kita lakukan tes cepat proyeksi Alamat :

@Autowired private AddressRepository addressRepository; @Test public void whenUsingClosedProjections_thenViewWithRequiredPropertiesIsReturned() { AddressView addressView = addressRepository.getAddressByState("CA").get(0); assertThat(addressView.getZipCode()).isEqualTo("90001"); // ... }

Di balik layar, Spring membuat instance proxy dari antarmuka proyeksi untuk setiap objek entitas, dan semua panggilan ke proxy tersebut diteruskan ke objek itu.

Kita dapat menggunakan proyeksi secara rekursif. Misalnya, inilah antarmuka proyeksi untuk kelas Person :

public interface PersonView { String getFirstName(); String getLastName(); }

Sekarang, mari tambahkan metode dengan tipe kembalian PersonView - proyeksi bersarang - dalam proyeksi Alamat :

public interface AddressView { // ... PersonView getPerson(); }

Perhatikan metode yang mengembalikan proyeksi bersarang harus memiliki nama yang sama dengan metode di kelas root yang mengembalikan entitas terkait.

Mari verifikasi proyeksi bertingkat dengan menambahkan beberapa pernyataan ke metode pengujian yang baru saja kita tulis:

// ... PersonView personView = addressView.getPerson(); assertThat(personView.getFirstName()).isEqualTo("John"); assertThat(personView.getLastName()).isEqualTo("Doe");

Perhatikan bahwa proyeksi rekursif hanya berfungsi jika kita melintasi dari sisi pemilik ke sisi terbalik. Jika kita melakukannya dengan cara lain, proyeksi bersarang akan disetel ke nol .

3.2. Buka Proyeksi

Sampai tahap ini, kita telah melalui proyeksi tertutup, yang menunjukkan antarmuka proyeksi yang metodenya sama persis dengan nama properti entitas.

There's another sort of interface-based projections: open projections. These projections enable us to define interface methods with unmatched names and with return values computed at runtime.

Let's go back to the Person projection interface and add a new method:

public interface PersonView { // ... @Value("#{target.firstName + ' ' + target.lastName}") String getFullName(); }

The argument to the @Value annotation is a SpEL expression, in which the target designator indicates the backing entity object.

Now, we'll define another repository interface:

public interface PersonRepository extends Repository { PersonView findByLastName(String lastName); }

To make it simple, we only return a single projection object instead of a collection.

This test confirms open projections work as expected:

@Autowired private PersonRepository personRepository; @Testpublic void whenUsingOpenProjections_thenViewWithRequiredPropertiesIsReturned() { PersonView personView = personRepository.findByLastName("Doe"); assertThat(personView.getFullName()).isEqualTo("John Doe"); }

Open projections have a drawback: Spring Data cannot optimize query execution as it doesn't know in advance which properties will be used. Thus, we should only use open projections when closed projections aren't capable of handling our requirements.

4. Class-Based Projections

Instead of using proxies Spring Data creates for us from projection interfaces, we can define our own projection classes.

For example, here's a projection class for the Person entity:

public class PersonDto { private String firstName; private String lastName; public PersonDto(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } // getters, equals and hashCode }

For a projection class to work in tandem with a repository interface, the parameter names of its constructor must match properties of the root entity class.

We must also define equals and hashCode implementations – they allow Spring Data to process projection objects in a collection.

Now, let's add a method to the Person repository:

public interface PersonRepository extends Repository { // ... PersonDto findByFirstName(String firstName); }

This test verifies our class-based projection:

@Test public void whenUsingClassBasedProjections_thenDtoWithRequiredPropertiesIsReturned() { PersonDto personDto = personRepository.findByFirstName("John"); assertThat(personDto.getFirstName()).isEqualTo("John"); assertThat(personDto.getLastName()).isEqualTo("Doe"); }

Notice with the class-based approach, we cannot use nested projections.

5. Dynamic Projections

An entity class may have many projections. In some cases, we may use a certain type, but in other cases, we may need another type. Sometimes, we also need to use the entity class itself.

Defining separate repository interfaces or methods just to support multiple return types is cumbersome. To deal with this problem, Spring Data provides a better solution: dynamic projections.

Kita dapat menerapkan proyeksi dinamis hanya dengan mendeklarasikan metode repositori dengan parameter Class :

public interface PersonRepository extends Repository { // ...  T findByLastName(String lastName, Class type); }

Dengan meneruskan tipe proyeksi atau kelas entitas ke metode seperti itu, kita dapat mengambil objek dari tipe yang diinginkan:

@Test public void whenUsingDynamicProjections_thenObjectWithRequiredPropertiesIsReturned() { Person person = personRepository.findByLastName("Doe", Person.class); PersonView personView = personRepository.findByLastName("Doe", PersonView.class); PersonDto personDto = personRepository.findByLastName("Doe", PersonDto.class); assertThat(person.getFirstName()).isEqualTo("John"); assertThat(personView.getFirstName()).isEqualTo("John"); assertThat(personDto.getFirstName()).isEqualTo("John"); }

6. Kesimpulan

Pada artikel ini, kami membahas berbagai jenis proyeksi JPA Data Musim Semi.

Kode sumber untuk tutorial ini tersedia di GitHub. Ini adalah proyek Maven dan harus dapat berjalan sebagaimana adanya.