Belajar Spring Data JPA - Relasi One To Many Bidirectional
Relasi dalam pengembangan aplikasi adalah fondasi dari hubungan antar objek dalam database. Salah satu jenis relasi yang umum digunakan adalah relasi One-to-Many, yang menciptakan koneksi antara satu entitas dengan banyak entitas terkait. Dalam artikel ini, kita akan membahas relasi One-to-Many bidireksional dan konsep dasarnya.
Apa itu Relasi One-to-Many Bidirectional?
Relasi One-to-Many bidirectional mengacu pada hubungan di antara dua entitas, di mana satu entitas dapat terhubung dengan banyak entitas terkait, dan sebaliknya. Dalam konteks ini, "One" mengacu pada entitas yang memiliki hubungan, dan "Many" mengacu pada entitas terkait. Bidirectional berarti bahwa kedua entitas mengetahui tentang keberadaan satu sama lain.
Dalam kasus relasi @OneToMany, kita dapat mengimplementasikannya secara bidirectional, yang berarti setiap entitas terkait memiliki pengetahuan tentang entitas lainnya. Dalam konteks Spring Data JPA, kita dapat menggunakan anotasi @OneToMany untuk menentukan hubungan tersebut.
Persiapan
Berikut ini merupakan tools yang saya gunakan saat membuat artikel ini :
- Apache Maven 3.9.5
- Spring Boot 3.3.2
- Java 21
- PostgreSQL 16
Membuat Database
--Membuat Database db_retail CREATE DATABASE db_retail; --Membuat Tabel Penjualan CREATE TABLE penjualan( id bigserial not null, kode_penjualan varchar(255) not null, tanggal_penjualan date not null, CONSTRAINT penjualan_pk PRIMARY KEY (id), CONSTRAINT kode_penjualan_unique UNIQUE (kode_penjualan) ); --Membuat Tabel Penjualan Detail CREATE TABLE penjualan_detail( id bigserial not null, kode_produk varchar(10) not null, nama_produk varchar(255) not null, jumlah_produk int not null, harga_produk double precision not null, penjualan_id bigint not null, CONSTRAINT penjualan_detail_pk PRIMARY KEY (id), CONSTRAINT penjualan_id_fk FOREIGN KEY (penjualan_id) REFERENCES penjualan (id) ON DELETE CASCADE ON UPDATE CASCADE );
Contoh Program One To Many Bidirectional
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.2</version> <relativePath/> </parent> <groupId>id.aban</groupId> <artifactId>belajar-spring</artifactId> <version>1.0.0</version> <name>belajar-spring</name> <description>Belajar OneToMany Spring Data JPA</description> <properties> <java.version>21</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <argLine>-XX:+EnableDynamicAgentLoading</argLine> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
server.port=8080 server.servlet.context-path=/belajar-spring #Konfigurasi Postgres spring.datasource.url=jdbc:postgresql://localhost:5432/db_retail spring.datasource.username=postgres spring.datasource.password=postgres #Konfigurasi Hibernate spring.jpa.hibernate.ddl-auto=none
package id.aban.belajarspring.response; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor @Builder public class ResponseData<T> { private boolean status; private String pesan; private T data; }
Request Class
package id.aban.belajarspring.request; import lombok.Getter; import lombok.Setter; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.Builder; import java.sql.Date; import java.util.List; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class PenjualanRequest { private String kodePenjualan; private Date tanggalPenjualan; private List<PenjualanDetailRequest> details; }
package id.aban.belajarspring.request; import lombok.Getter; import lombok.Setter; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.Builder; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class PenjualanDetailRequest { private String kodeProduk; private String namaProduk; private int jumlahProduk; private double hargaProduk; }
Entity Class
package id.aban.belajarspring.entities; import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.Builder; import java.sql.Date; import java.util.List; @Getter @Setter @AllArgsConstructor @NoArgsConstructor @Builder @Entity @Table(name = "penjualan") public class Penjualan { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(name = "kode_penjualan") private String kodePenjualan; @Column(name = "tanggal_penjualan") private Date tanggalPenjualan; @JsonManagedReference @OneToMany(mappedBy = "penjualan", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) private List<PenjualanDetail> details; }
package id.aban.belajarspring.entities; import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import lombok.Builder; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder @Entity @Table(name = "penjualan_detail") public class PenjualanDetail { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(name = "kode_produk") private String kodeProduk; @Column(name = "nama_produk") private String namaProduk; @Column(name = "jumlah_produk") private int jumlahProduk; @Column(name = "harga_produk") private double hargaProduk; @JsonBackReference @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "penjualan_id", referencedColumnName = "id") private Penjualan penjualan; }
Repository
package id.aban.belajarspring.repositories; import id.aban.belajarspring.entities.Penjualan; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface PenjualanRepository extends JpaRepository<Penjualan, Long> { }
Service
package id.aban.belajarspring.service; import id.aban.belajarspring.entities.Penjualan; import id.aban.belajarspring.request.PenjualanRequest; import id.aban.belajarspring.response.ResponseData; import java.util.List; public interface PenjualanService { ResponseData<List<Penjualan>> findAll(); ResponseData<Penjualan> findById(long id); ResponseData<Penjualan> create(PenjualanRequest obj); ResponseData<Penjualan> update(long id, PenjualanRequest obj); ResponseData delete(long id); }
package id.aban.belajarspring.service; import id.aban.belajarspring.entities.Penjualan; import id.aban.belajarspring.entities.PenjualanDetail; import id.aban.belajarspring.repositories.PenjualanRepository; import id.aban.belajarspring.request.PenjualanRequest; import id.aban.belajarspring.response.ResponseData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; @Service public class PenjualanServiceImpl implements PenjualanService { @Autowired private PenjualanRepository repository; @Override public ResponseData<List<Penjualan>> findAll() { ResponseData response = ResponseData.builder() .status(false) .pesan("Data Penjualan Tidak Ditemukan") .build(); List<Penjualan> list = repository.findAll(); if(!list.isEmpty()){ response.setStatus(true); response.setPesan("Data Berhasil Ditemukan"); response.setData(list); } return response; } @Override public ResponseData<Penjualan> findById(long id) { ResponseData response = ResponseData.builder() .status(false) .pesan("Data Penjualan Tidak Ditemukan") .build(); Penjualan penjualan = repository.findById(id).orElse(new Penjualan()); if(penjualan.getId() > 0){ response.setStatus(true); response.setPesan("Data Berhasil Ditemukan"); response.setData(penjualan); } return response; } @Transactional @Override public ResponseData<Penjualan> create(PenjualanRequest obj) { ResponseData response = ResponseData.builder() .status(false) .pesan("Data Gagal Disimpan") .build(); Penjualan penjualan = Penjualan.builder() .kodePenjualan(obj.getKodePenjualan()) .tanggalPenjualan(obj.getTanggalPenjualan()) .build(); penjualan.setDetails( obj.getDetails() .stream() .map(detail -> PenjualanDetail.builder() .kodeProduk(detail.getKodeProduk()) .namaProduk(detail.getNamaProduk()) .jumlahProduk(detail.getJumlahProduk()) .hargaProduk(detail.getHargaProduk()) .penjualan(penjualan) .build()).collect(Collectors.toList() ) ); Penjualan p = repository.save(penjualan); response.setStatus(true); response.setPesan("Data Berhasil Disimpan"); response.setData(p); return response; } @Transactional @Override public ResponseData<Penjualan> update(long id, PenjualanRequest obj) { ResponseData response = ResponseData.builder() .status(false) .pesan("Data Penjualan Tidak Ditemukan") .build(); Penjualan penjualan = repository.findById(id) .orElse(Penjualan.builder().id(0).build()); if(penjualan.getId() > 0) { penjualan.setKodePenjualan(obj.getKodePenjualan()); penjualan.setTanggalPenjualan(obj.getTanggalPenjualan()); penjualan.getDetails().clear(); penjualan.getDetails().addAll( obj.getDetails() .stream() .map(detail -> PenjualanDetail.builder() .kodeProduk(detail.getKodeProduk()) .namaProduk(detail.getNamaProduk()) .jumlahProduk(detail.getJumlahProduk()) .hargaProduk(detail.getHargaProduk()) .penjualan(penjualan) .build()).collect(Collectors.toList() ) ); Penjualan p = repository.save(penjualan); response.setStatus(true); response.setPesan("Data Berhasil Diubah"); response.setData(p); } return response; } @Override public ResponseData delete(long id) { ResponseData response = ResponseData.builder() .status(false) .pesan("Data Penjualan Tidak Ditemukan") .build(); Penjualan penjualan = repository.findById(id).orElse(Penjualan.builder().build()); if(penjualan.getId() > 0) { repository.deleteById(id); response.setStatus(true); response.setPesan("Data Berhasil Dihapus"); } return response; } }
Rest Controller
package id.aban.belajarspring.api; import id.aban.belajarspring.entities.Penjualan; import id.aban.belajarspring.request.PenjualanRequest; import id.aban.belajarspring.response.ResponseData; import id.aban.belajarspring.service.PenjualanService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("penjualan") public class PenjualanApi { @Autowired private PenjualanService service; @GetMapping("/list") public ResponseEntity findAll(){ ResponseData<List<Penjualan>> response = service.findAll(); if(response.isStatus()){ return ResponseEntity.ok().body(response.getData()); } return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } @GetMapping("") public ResponseEntity findById(@RequestParam(name = "id", required = true) long id){ ResponseData<Penjualan> response = service.findById(id); if(response.isStatus()){ return ResponseEntity.ok().body(response.getData()); } return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } @PostMapping("/create") public ResponseEntity create(@RequestBody PenjualanRequest request){ ResponseData<Penjualan> response = service.create(request); if(response.isStatus()){ return ResponseEntity.ok(response.getData()); } return ResponseEntity.status(HttpStatus.NOT_ACCEPTABLE).build(); } @PutMapping("/update") public ResponseEntity update(@RequestParam(name = "id", required = true) long id, @RequestBody PenjualanRequest request){ ResponseData<Penjualan> response = service.update(id, request); if(response.isStatus()){ return ResponseEntity.ok().body(response.getData()); } return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } @DeleteMapping("/delete") public ResponseEntity delete(@RequestParam(name = "id", required = true) long id){ ResponseData response = service.delete(id); if(response.isStatus()){ return ResponseEntity.status(HttpStatus.OK).build(); } return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } }
Kelebihan
- Navigasi Mudah
- Manajemen Cascade
- Optimasi Query
- Kejelasan Model
Dengan hubungan bidireksional, Anda dapat dengan mudah melakukan navigasi dari satu entitas ke entitas terkait. Misalnya, dari objek Parent, Anda dapat mengakses semua objek Child, dan sebaliknya.
Penggunaan atribut cascade pada anotasi @OneToMany memungkinkan operasi (seperti persist, merge, remove) untuk di-cascade dari entitas parent ke entitas child. Ini mengurangi boilerplate code dan membuat manajemen entitas menjadi lebih mudah.
Dengan bidirectional, kalian dapat mengoptimalkan query basis data dengan menggunakan fetch dan join untuk mengambil entitas terkait dalam satu query, menghindari pemanggilan ekstra ke database.
Dalam beberapa kasus, model yang bidirectional dapat memberikan representasi yang lebih jelas dari hubungan antar entitas dalam aplikasi kalian.
Kekurangan
- Kompleksitas
- Potensi Kesalahan
- Pemakaian Memori
- Kemungkinan Deadlock
Penggunaan hubungan bidirectional dapat meningkatkan kompleksitas kode dan konfigurasi. Perlu memastikan bahwa kedua sisi hubungan dikelola dengan benar untuk menghindari masalah seperti ketidaksesuaian data.
Jika tidak dikelola dengan benar, dapat terjadi potensi kesalahan seperti inkonsistensi data. Misalnya, jika satu sisi relasi tidak diperbarui, maka sisi lainnya mungkin tidak merefleksikan perubahan.
Bidirectional relationship dapat memakan lebih banyak memori karena masing-masing entitas harus menyimpan referensi ke entitas terkaitnya. Hal ini terutama penting jika koleksi entitas terkait cukup besar.
Pada beberapa kasus, penggunaan transaksi bersamaan dengan hubungan bidirectional dapat menyebabkan potensi deadlock jika tidak dikelola dengan hati-hati.
Kesimpulan
Pilihan antara menggunakan hubungan @OneToMany bidirectional atau tidak tergantung pada kebutuhan dan kompleksitas aplikasi Anda. Jika navigasi mudah dan manajemen kaskad diperlukan, bidirectional dapat menjadi pilihan yang baik, tetapi perlu dikelola dengan hati-hati untuk menghindari potensi kesalahan dan kompleksitas yang tidak perlu.
0 Response to "Belajar Spring Data JPA - Relasi One To Many Bidirectional"
Posting Komentar