Belajar Spring Data JPA - Relasi One To Many Bidirectional - Java Community

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.1.5 versi terbaru saat artikel ini dibuat
  • Java 21
  • JPostgreSQL 16

Membuat Database

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

pom.xml
<?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.1.5</version>
        <relativePath/>
    </parent>

    <groupId>id.aban</groupId>
    <artifactId>belajar-spring</artifactId>
    <version>1.0.0</version>
    <name>belajar-spring</name>
    <description>belajar-spring</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>
properties
server.port=8080
server.servlet.context-path=/belajar-spring

spring.jpa.open-in-view=false

#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
ReponseData
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

PenjualanRequest
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;
}

PenjualanDetailRequest
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

Penjualan
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;
}

PenjualanDetail
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")
}

Repository

PenjualanRepository
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

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 {
    public ResponseData<List<Penjualan>> findAll();
    public ResponseData<Penjualan> findById(long id);
    public ResponseData<Penjualan> create(PenjualanRequest obj);
    public ResponseData<Penjualan> update(long id, PenjualanRequest obj);
    public ResponseData delete(long id);
}

Service Impl
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

Rest API
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
  • 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.

  • Manajemen Cascade
  • 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.

  • Optimasi Query
  • 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.

  • Kejelasan Model
  • Dalam beberapa kasus, model yang bidirectional dapat memberikan representasi yang lebih jelas dari hubungan antar entitas dalam aplikasi kalian.

Kekurangan

  • Kompleksitas
  • Penggunaan hubungan bidirectional dapat meningkatkan kompleksitas kode dan konfigurasi. Perlu memastikan bahwa kedua sisi hubungan dikelola dengan benar untuk menghindari masalah seperti ketidaksesuaian data.

  • Potensi Kesalahan
  • 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.

  • Pemakaian Memori
  • 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.

  • Kemungkinan Deadlock
  • 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.

Subscribe to receive free email updates:

0 Response to "Belajar Spring Data JPA - Relasi One To Many Bidirectional"

Posting Komentar