MongoDB Advanced Data Modeling dengan Spring Boot
Sebelumnya kita sudah belajar tentang membuat applikasi spring boot sederhana dengan mongodb di artikel ini spring-dan-mongodb-user-login dan juga ini belajar-relasi-mongo-db-dengan-studi, selanjutnya mari buat studi kasus yang lebih komplek untuk memberikan pemahaman lebih lanjut tentang MongoDB Advanced Data Modeling dengan Spring Boot.
1. Embedded vs Reference dan Implementasinya di Spring Boot
Artikel ini merupakan lanjutan langsung dari pembahasan sistem rental mobil (Owner & Car) sebelumnya. Pada bagian ini, fokus kita bergeser dari sekadar CRUD dan relasi dasar menuju pemahaman mendalam tentang cara memodelkan data MongoDB secara fungsional, sekaligus bagaimana keputusan tersebut diimplementasikan dengan benar di Spring Boot.
Tujuan utama artikel ini adalah membantu kamu berpikir seperti system designer, bukan sekadar penulis kode.
3. Mengubah Cara Pandang: MongoDB Bukan SQL Tanpa JOIN
Kesalahan paling umum saat pertama kali menggunakan MongoDB adalah memperlakukannya seperti relational database tanpa JOIN. Padahal, MongoDB justru memberi kebebasan untuk menyesuaikan struktur data dengan pola akses aplikasi, bukan sebaliknya.
Pertanyaan utama yang harus dijawab sebelum menentukan embedded atau reference bukanlah:
“Apakah ini relasi one-to-many?”
melainkan:
“Bagaimana data ini digunakan, dibaca, dan bertumbuh?”
4. Embedded Data: Ketika Data Selalu Berjalan Bersama
Studi Kasus: Riwayat Status Mobil
Dalam sistem rental mobil, setiap mobil memiliki status yang dapat berubah seiring waktu, seperti AVAILABLE, RENTED, atau MAINTENANCE. Riwayat perubahan ini selalu berkaitan langsung dengan mobil dan tidak pernah diakses secara terpisah.
5. Alasan Teknis Memilih Embedded
Data selalu dibaca bersama parent (Car)
Tidak memiliki makna bisnis tanpa parent
Jumlah data relatif kecil dan terkendali
Performa read lebih cepat tanpa query tambahan
6. Spring Data dengan Menggunakan record
Ya, mendukung record secara resmi.
Spring Data MongoDB:
-
bisa melakukan mapping constructor
-
mengenali
@Iddi parameter record -
bisa serialize / deserialize embedded object di dalam record
package com.yusuf.springmongoclean.domain.rental;
import com.yusuf.springmongoclean.enumerator.CarColor;
import com.yusuf.springmongoclean.enumerator.CarStatus;
import com.yusuf.springmongoclean.enumerator.CarType;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.List;
@Document(collection = "cars")
public record Car(
@Id String id,
String ownerId,
CarType type,
String brand,
String model,
int year,
int totalSeats,
CarColor color,
String plateNumber,
int dailyPrice,
CarStatus status,
List<CarStatusHistory> statusHistory
) {}
package com.yusuf.springmongoclean.domain.rental;
import com.yusuf.springmongoclean.enumerator.CarStatus;
import java.time.Instant;
public record CarStatusHistory(
CarStatus status,
Instant changedAt
) {}
MongoDB driver dan Spring Data akan memperlakukannya seperti immutable value object.
7. Kapan record Sangat Cocok untuk Entity MongoDB?
Cocok jika:
- MongoDB (bukan JPA)
- Tidak pakai lazy loading
- Tidak pakai dirty checking
- Update dilakukan dengan replace document
- Domain bersifat state snapshot, bukan entity lifecycle berat
MongoDB tidak butuh:
-
proxy
-
setter
-
no-arg constructor
Jadi record = aman.
8. Embedded + Record = Kombinasi Ideal
Embedded document secara konsep memang value object.
CarStatusHistory:
-
tidak punya identity
-
tidak hidup sendiri
-
tidak berubah (append-only)
Secara DDD, ini textbook:
Entity → record
Embedded / Value Object → record
Record lebih konsisten secara model dibanding pakai class mutable.
9. Kapan record TIDAK Disarankan?
Jujur saja, ada case di mana class lebih cocok.
Hindari record jika:
-
pakai JPA / Hibernate
-
butuh partial update kompleks dengan mutation
-
domain punya banyak behavior internal
-
entity lifecycle kompleks (aggregate root berat)
Tapi kita tidak sedang berada di situ sekarang.
10. CRUD dengan Embeded Data CarStatusHistory di CARS
Kita lanjut rapi, bertahap, dan konsisten dengan desain record + embedded yang sudah kita bangun.
Fokus kita: CRUD Car yang benar-benar supportList<CarStatusHistory>tanpa melanggar prinsip MongoDB + immutability.
Prinsip Dasar (Pegang Ini Dulu)
Sebelum ke kode, ini aturan mainnya:
-
Cartetap record (immutable) -
CarStatusHistoryadalah embedded value object -
CRUD tidak pernah mutate list
-
Setiap perubahan status → append history
-
Tidak ada repository untuk
CarStatusHistory
11. CREATE Car (Init Status History)
Saat mobil dibuat:
-
status default =
AVAILABLE -
history diisi 1 entry awal
Service: Create di CarService
public Car create(CarRequest request) {
ownerRepository.findById(request.ownerId())
.orElseThrow(() -> new IllegalArgumentException("Owner not found"));
CarStatus initialStatus = CarStatus.AVAILABLE;
List<CarStatusHistory> statusHistories = List.of(
new CarStatusHistory(initialStatus, Instant.now())
);
return carRepository.save(new Car(
null,
request.ownerId(),
request.type(),
request.brand(),
request.model(),
request.year(),
request.totalSeats(),
request.color(),
request.plateNumber(),
request.dailyPrice(),
CarStatus.AVAILABLE,
statusHistories
));
}
12. READ Car di Car Service (Dengan History)
a. Find by ID di CarService
public Car findById(String id) {
return carRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Car not found"));
}
b. Find Cars by Owner di CarService
public List<Car> findByOwner(String ownerId) {
return carRepository.findByOwnerId(ownerId);
}
c. Delete Cars by ID di CarService
@DeleteMapping("/{id}")
public void delete(@PathVariable String id) {
service.delete(id);
}
13. UPDATE Car DTO
package com.yusuf.springmongoclean.dto.rental;
import com.yusuf.springmongoclean.enumerator.CarColor;
public record UpdateCarRequest(
String brand,
String model,
CarColor color,
Integer dailyPrice
) {}
14. UPDATE di Car Service (Core Use Case)
Ini CRUD paling penting karena melibatkan embedded list.
Service: UpdateCarStatus di CarService
public Car updateCarStatus(String carId, CarStatus newStatus) {
Car car = carRepository.findById(carId)
.orElseThrow(() -> new IllegalArgumentException("Car not found"));
if (car.status() == newStatus) {
return car; // no-op
}
List<CarStatusHistory> updatedHistory =
new ArrayList<>(car.statusHistory());
updatedHistory.add(
new CarStatusHistory(newStatus, Instant.now())
);
Car updatedCar = new Car(
car.id(),
car.ownerId(),
car.type(),
car.brand(),
car.model(),
car.year(),
car.totalSeats(),
car.color(),
car.plateNumber(),
car.dailyPrice(),
newStatus,
updatedHistory
);
return carRepository.save(updatedCar);
}
Service: UpdateCarInfo di CarService
public Car updateCarInfo(String carId, UpdateCarRequest request) {
Car car = findById(carId);
return carRepository.save(new Car(
car.id(),
car.ownerId(),
car.type(),
request.brand() != null ? request.brand() : car.brand(),
request.model() != null ? request.model() : car.model(),
car.year(),
car.totalSeats(),
request.color() != null ? request.color() : car.color(),
car.plateNumber(),
request.dailyPrice() > 0 ? request.dailyPrice() : car.dailyPrice(),
car.status(),
car.statusHistory()
));
}
Kenapa ini oke?
-
immutable
-
append-only history
-
MongoDB replace document secara atomic
15. UPDATE Car Controller (Rest Endpoints)
package com.yusuf.springmongoclean.controller.rental;
import com.yusuf.springmongoclean.domain.rental.Car;
import com.yusuf.springmongoclean.dto.rental.CarRequest;
import com.yusuf.springmongoclean.dto.rental.UpdateCarRequest;
import com.yusuf.springmongoclean.enumerator.CarStatus;
import com.yusuf.springmongoclean.service.rental.CarService;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/cars")
public class CarController {
private final CarService service;
public CarController(CarService service) {
this.service = service;
}
@PostMapping
public Car create(@RequestBody CarRequest request) {
return service.create(request);
}
@GetMapping("/owner/{ownerId}")
public List<Car> findByOwner(@PathVariable String ownerId) {
return service.findByOwner(ownerId);
}
@GetMapping("/{id}")
public Car findById(@PathVariable String id) {
return service.findById(id);
}
@PatchMapping("/{id}/status")
public Car updateCarStatus(
@PathVariable String id,
@RequestParam CarStatus status
) {
return service.updateCarStatus(id, status);
}
@PutMapping("/{id}/info")
public Car updateCarInfo(
@PathVariable String id,
@RequestBody UpdateCarRequest updateCarRequest
) {
return service.updateCarInfo(id, updateCarRequest);
}
@DeleteMapping("/{id}")
public void delete(@PathVariable String id) {
service.delete(id);
}
}
16. Endpoint - Create Car
17. Update Status Car (From Available to Rented)
Pada tahap ini, kita tidak hanya membangun CRUD yang “berjalan”, tetapi sebuah fondasi sistem yang sadar akan struktur data, alur bisnis, dan karakteristik MongoDB sebagai document database. Dengan memanfaatkan embedded document melalui CarStatusHistory, kita melihat bagaimana MongoDB memungkinkan data dimodelkan secara lebih natural, mengikuti cara aplikasi benar-benar membaca dan menggunakan informasi.
Pendekatan menggunakan record yang immutable mendorong desain yang eksplisit dan aman, di mana setiap perubahan state tercermin sebagai objek baru, bukan mutasi tersembunyi. Hal ini membuat alur perubahan data lebih mudah dipahami, diuji, dan diaudit, sekaligus selaras dengan cara MongoDB menangani update dokumen secara atomik.
Lebih dari sekadar implementasi teknis, pembahasan ini menekankan pentingnya memilih struktur data berdasarkan konteks penggunaan, bukan kebiasaan dari dunia relational database. Embedded dan reference bukanlah kompetitor, melainkan alat yang saling melengkapi ketika digunakan pada tempat yang tepat.
Comments
Post a Comment