Front-End & Back-End Development 7
2.10 Relasi Data di MongoDB
Dalam dunia nyata, data di aplikasi jarang berdiri sendiri. Setiap entitas biasanya saling terhubung. Misalnya, satu produk memiliki banyak ulasan, seorang pengguna memiliki banyak pesanan, atau setiap pesanan berisi beberapa produk. Relasi seperti ini sangat penting untuk dipahami agar Anda dapat merancang struktur database yang efisien dan mudah di-query.
Di MongoDB, terdapat beberapa cara untuk membangun relasi antar-data. Dua pendekatan utama yang sering digunakan adalah One-to-Many dan Many-to-Many Relationship. Mari kita bahas satu per satu!
2.10.1 One-to-Many Relationship
Relasi one-to-many digunakan ketika satu dokumen memiliki banyak data terkait. Contohnya, satu produk (Product) bisa memiliki banyak ulasan (Reviews).
Ada dua pendekatan umum untuk membangun relasi jenis ini: Embedded Document dan Referenced Document.
Approach 1: Embedded (Reviews disimpan dalam Product)
Pendekatan ini berarti data reviews langsung disimpan di dalam dokumen product. Pendekatan ini cocok jika data relasinya kecil dan tidak terlalu sering berubah.
{
"name": "Vitamin C",
"price": 85000,
"reviews": [
{ "user": "Aiman", "rating": 5, "comment": "Bagus!" },
{ "user": "Aila", "rating": 4, "comment": "Oke" }
]
}
Penjelasan:
- Data reviews menjadi bagian langsung dari dokumen Product.
- Lebih cepat diakses (satu query cukup), tapi kurang fleksibel jika jumlah ulasan banyak atau sering berubah.
Approach 2: Referenced (Reviews disimpan terpisah)
Pada pendekatan ini, data reviews disimpan di collection terpisah, dan hanya menyimpan referensi ke productId. Ini lebih efisien untuk data besar dan memungkinkan query yang lebih fleksibel.
Contoh struktur dokumen:
{
"_id": ObjectId("product123"),
"name": "Vitamin C",
"price": 85000
}
// Reviews (collection terpisah)
{
"_id": ObjectId("review1"),
"productId": ObjectId("product123"), // Reference
"user": "Aiman",
"rating": 5
}
2.10.2 Schema Mongoose untuk Referenced
Kita akan membuat schema untuk Review yang menyimpan referensi ke model Product.
// models/Review.js
const mongoose = require("mongoose");
const reviewSchema = new mongoose.Schema(
{
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Product", // Reference ke Product model
required: true,
},
user: {
type: String,
required: true,
},
rating: {
type: Number,
required: true,
min: 1,
max: 5,
},
comment: String,
},
{
timestamps: true,
}
);
module.exports = mongoose.model("Review", reviewSchema);
Penjelasan:
- ref: "Product" menandakan bahwa field productId terhubung ke model Product.
- timestamps: true berguna untuk pencatatan waktu (audit data).
- Validasi rating membatasi nilai antara 1 hingga 5.
2.10.3 Populate: Mengambil Data dengan Relasi
Langkah berikutnya adalah menggabungkan data dari dua collection menggunakan populate().
const Review = require("./models/Review");
// Get reviews dengan product details
const reviews = await Review.find().populate("productId"); // Join with Product
// Populate dengan field tertentu (Select)
const reviewsSelect = await Review.find().populate("productId", "name price");
Penjelasan:
- populate("productId") berfungsi seperti JOIN di SQL — ia menampilkan detail produk berdasarkan referensi productId.
- Hasilnya akan memuat data lengkap dari Product yang terhubung.
Contoh output:
// Output:
[
{
_id: "review1",
productId: {
// Full product object!
_id: "product123",
name: "Vitamin C",
price: 85000,
},
user: "Aiman",
rating: 5,
},
];
2.10.4 Populate dengan Field Tertentu
Anda juga bisa memilih field yang ingin ditampilkan agar hasil query lebih efisien.
// Populate specific fields only
const reviews = await Review.find().populate("productId", "name price"); // Hanya name & price
Penjelasan:
- Hanya field name dan price dari Product yang akan ditampilkan.
- Field lain seperti _id atau description tidak akan dimuat.
2.10.5 Populate dengan Filter (Nested Populate)
Jika ingin menampilkan hanya produk tertentu, Anda bisa menambahkan filter menggunakan opsi match.
// Populate nested
const reviews = await Review.find().populate({
path: "productId",
select: "name price",
match: { price: { $gt: 50000 } }, // Filter products
});
Penjelasan:
- Query di atas hanya akan menampilkan review yang terhubung dengan produk berharga lebih dari Rp50.000.
2.10.6 Many-to-Many Relationship
Relasi many-to-many digunakan ketika dua entitas bisa saling memiliki banyak keterkaitan. Contohnya, sebuah produk bisa termasuk dalam beberapa kategori, dan satu kategori bisa memiliki banyak produk.
Pendekatan ini sering digunakan dalam sistem e-commerce, misalnya kategori “Vitamin” dan “Immune Boost” bisa sama-sama memiliki produk “Vitamin C”.
Schema dengan Array of References
Kita dapat menggunakan array berisi ObjectId untuk menghubungkan banyak data antar dua model.
// models/Category.js
const categorySchema = new mongoose.Schema({
name: String,
description: String,
});
// models/Product.js
const productSchema = new mongoose.Schema({
name: String,
price: Number,
categories: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Category",
},
],
});
Penjelasan:
- Setiap Product dapat memiliki beberapa Category melalui array referensi.
- Field ref: "Category" digunakan agar bisa melakukan populate() untuk mengambil detail kategori.
Contoh Penggunaan:
Step 1: Membuat Categories
Membuat dua kategori baru di database.
// Create categories
const vitaminCat = await Category.create({ name: "Vitamin" });
const immuneCat = await Category.create({ name: "Immune Boost" });
Step 2: Membuat Product dengan Beberapa Categories
Produk “Vitamin C” kini memiliki dua kategori yang saling terhubung.
// Create product dengan multiple categories
const product = await Product.create({
name: "Vitamin C",
price: 85000,
categories: [vitaminCat._id, immuneCat._id],
});
Step 3: Menampilkan Product dengan Categories
Mengambil produk sekaligus memuat detail kategorinya.
// Get product dengan categories populated
const productWithCats = await Product.findById(product._id).populate(
"categories"
);
console.log(productWithCats);
// {
// "name": "Vitamin C",
// "categories": [
// { "_id": "...", "name": "Vitamin" },
// { "_id": "...", "name": "Immune Boost" }
// ]
// }
Penjelasan:
- Dengan populate("categories"), Anda bisa langsung melihat daftar kategori lengkap tanpa query tambahan.
- Ini sangat berguna untuk menampilkan detail produk di halaman toko online atau katalog.
2.10.7 Implement Relasi Data (Practice)
Pada sesi ini, Anda akan membangun relasi one-to-many antara Product dan Review menggunakan fitur populate() di Mongoose. Tujuan utamanya adalah agar data produk dapat ditampilkan bersamaan dengan seluruh ulasan terkait dalam satu proses pengambilan data.
Dalam pengembangan aplikasi berbasis database, sering kali diperlukan untuk menampilkan data yang saling berhubungan antar koleksi. Salah satu contoh umum adalah sebuah produk yang memiliki banyak ulasan dari pengguna. Mongoose menyediakan mekanisme relasi menggunakan referensi antar dokumen dan populate() untuk mengambil data terkait secara efisien. Melalui latihan ini, Anda akan mempelajari cara membangun struktur relasi tersebut dan menampilkan data produk lengkap beserta ulasan pengguna.
Langkah-langkah Coding:
1. Buat file baru: models/Review.js.
Tempatkan file ini di folder models untuk menjaga struktur proyek tetap rapi.
2. Tambahkan struktur schema untuk Review.
const reviewSchema = new mongoose.Schema(
{
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Product",
required: true,
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
},
rating: {
type: Number,
required: true,
min: 1,
max: 5,
},
comment: String,
},
{ timestamps: true }
);
Penjelasan singkat tiap field:
- productId: referensi ke produk yang diulas.
- userId: referensi ke pengguna yang membuat ulasan.
- rating: nilai ulasan antara 1–5 sebagai indikator kualitas.
- comment: komentar pengguna, bersifat opsional.
- timestamps: menyimpan waktu pembuatan dan pembaruan ulasan secara otomatis.
3. Buat function untuk mengambil data produk beserta review-nya
Tambahkan function di file productService.js:
async function getProductWithReviews(productId) {
// Get product
const product = await Product.findById(productId);
// Get reviews untuk product ini
const reviews = await Review.find({ productId }).populate(
"userId",
"name email"
);
return {
...product.toObject(),
reviews: reviews,
};
}
Penjelasan:
- findById() digunakan untuk mengambil data produk.
- Review.find({ productId }) digunakan untuk mencari semua ulasan untuk produk tersebut.
- populate("userId", "name email") menampilkan data pengguna yang membuat ulasan, layaknya proses JOIN pada SQL.
4. Uji Function untuk memastikan relasi berjalan dengan baik
const result = await getProductWithReviews("product123");
console.log(result);
// Product dengan array reviews lengkap!
Hasil Akhir:
Anda berhasil membuat relasi one-to-many antara Product dan Review. Dengan struktur ini, Anda dapat menampilkan data produk sekaligus seluruh ulasan pelanggan hanya dalam satu pemanggilan fungsi.
Tips:
- populate() bekerja seperti JOIN pada SQL, namun dijalankan melalui beberapa query otomatis.
- Gunakan populate() secara selektif untuk menjaga performa.
- Relasi berbasis reference lebih fleksibel untuk data berukuran besar karena tidak menduplikasi dokumen seperti pada metode embedded.
Kesimpulan
Relasi data merupakan konsep penting dalam MongoDB karena sebagian besar entitas dalam aplikasi saling terhubung, seperti produk dengan ulasan atau produk dengan beberapa kategori. MongoDB menyediakan dua bentuk relasi utama, yaitu one-to-many dan many-to-many, yang dapat dibangun menggunakan pendekatan embedded document atau referenced document. Pendekatan referenced memungkinkan penggunaan populate() untuk mengambil data lintas koleksi, mirip seperti JOIN pada SQL.
Melalui penggunaan reference, schema yang lebih fleksibel dapat dibuat untuk menangani data besar dan kompleks. Operasi populate() juga membantu menampilkan data lengkap dalam satu query terstruktur. Dengan memahami teknik relasi ini, Anda dapat merancang database yang lebih efisien, scalable, dan mudah di-query, terutama dalam aplikasi e-commerce atau sistem yang memuat hubungan antar entitas.
2.11 Aggregation Pipeline
Dalam aplikasi nyata, kita tidak hanya menyimpan atau menampilkan data, tetapi juga perlu menganalisis dan mengolah data tersebut untuk mendapatkan insight yang lebih mendalam.
MongoDB menyediakan fitur powerful bernama Aggregation Pipeline yang berfungsi untuk melakukan operasi analisis data secara berurutan melalui beberapa tahap (stages).
Bayangkan aggregation seperti sebuah pabrik dengan jalur perakitan (conveyor belt):
- - Data mentah masuk di awal
- - Melewati berbagai tahapan seperti filter, group, atau calculate
- - Hingga akhirnya menghasilkan data yang sudah diolah sesuai kebutuhan
2.11.1 Basic Aggregation
Pada dasarnya, aggregation bekerja dengan menyusun serangkaian stages yang akan dijalankan secara berurutan. Setiap stage menerima data dari tahap sebelumnya dan menghasilkan output baru. Kita akan mulai dari contoh sederhana untuk memahami bagaimana cara kerja pipeline dasar.
Contoh: Menghitung rata-rata harga per kategori produk
const result = await Product.aggregate([
// Stage 1: Group by category
{
$group: {
_id: "$category", // Group by field category
avgPrice: { $avg: "$price" }, // Calculate average
count: { $sum: 1 }, // Count documents
total: { $sum: "$price" }, // Sum all prices
},
},
// Stage 2: Sort by avgPrice descending
{
$sort: { avgPrice: -1 },
},
]);
console.log(result);
// [
// { _id: "Supplement", avgPrice: 180000, count: 5, total: 900000 },
// { _id: "Vitamin", avgPrice: 95000, count: 10, total: 950000 }
// ]
Penjelasan:
- $group mengelompokkan data berdasarkan field category.
- Operator $avg, $sum, dan $count digunakan untuk menghitung statistik.
- $sort mengurutkan hasil berdasarkan rata-rata harga.
2.11.2 Aggregation Stages
Setiap stage dalam pipeline memiliki fungsi spesifik. Anda dapat menggabungkan beberapa stage untuk membuat analisis yang lebih kompleks. Berikut adalah stages yang paling umum digunakan dalam aggregation pipeline:
Contoh : Analisis Lengkap dengan Beberapa Stage
const stats = await Product.aggregate([
// Stage 1: Filter active products only
{
$match: { isActive: true },
},
// Stage 2: Group by category
{
$group: {
_id: "$category",
totalProducts: { $sum: 1 },
avgPrice: { $avg: "$price" },
minPrice: { $min: "$price" },
maxPrice: { $max: "$price" },
totalStock: { $sum: "$stock" },
},
},
// Stage 3: Add computed field
{
$addFields: {
category: "$_id",
priceRange: {
$concat: [
"Rp ",
{ $toString: "$minPrice" },
" - Rp ",
{ $toString: "$maxPrice" },
],
},
},
},
// Stage 4: Sort by avgPrice
{
$sort: { avgPrice: -1 },
},
]);
Penjelasan:
- $match: Memfilter produk yang aktif (isActive: true).
- $group: Mengelompokkan produk berdasarkan kategori dan menghitung statistik seperti total, rata-rata, minimum, maksimum, dan stok.
- $addFields: Menambahkan field baru seperti priceRange agar output lebih informatif.
- $sort: Mengurutkan hasil berdasarkan harga rata-rata tertinggi.
2.11.3 $lookup - Join Collections
Dalam beberapa kasus, kita perlu menggabungkan data dari dua koleksi berbeda, seperti Product dan Review. MongoDB menyediakan operator $lookup yang berfungsi seperti JOIN di SQL, untuk menampilkan data terkait dari dua koleksi.
Contoh: Menggabungkan Produk dengan Reviews-nya
const productsWithReviews = await Product.aggregate([
// Stage 1: Lookup reviews
{
$lookup: {
from: "reviews", // Collection name di DB
localField: "_id", // Product._id
foreignField: "productId", // Review.productId
as: "reviews", // Output field name
},
},
// Stage 2: Add review count & avg rating
{
$addFields: {
reviewCount: { $size: "$reviews" },
avgRating: { $avg: "$reviews.rating" },
},
},
// Stage 3: Filter products dengan rating > 4
{
$match: { avgRating: { $gt: 4 } },
},
]);
Penjelasan:
- $lookup menggabungkan data Product dengan Review berdasarkan ID produk.
- $addFields menambahkan dua informasi baru: total review dan rata-rata rating.
- $match memfilter hasil akhir agar hanya menampilkan produk dengan rating di atas 4.
2.11.4 Aggregation Pipeline
Pada sesi ini, Anda akan membangun sebuah aggregation pipeline untuk kebutuhan sales analytics yang menghasilkan statistik produk per kategori. Pipeline ini akan menghitung jumlah produk, rata-rata harga, total stok, serta rentang harga dalam setiap kategori, sehingga dapat langsung digunakan untuk kebutuhan dashboard maupun laporan analitik.
Aggregation Pipeline merupakan fitur analitik yang sangat kuat di MongoDB. Fitur ini memungkinkan Anda mengolah dan merangkum data melalui rangkaian tahapan seperti $match, $group, $sort, dan $project. Setiap tahap bekerja layaknya jalur pemrosesan yang membentuk hasil akhir secara bertahap. Dalam sesi ini, Anda akan menyusun pipeline analitik mulai dari tahap penyaringan data, pengelompokan, pengurutan, hingga pemformatan keluaran akhir agar siap digunakan dalam konteks bisnis.
Langkah-langkah :
1. Mulai dengan menyusun struktur pipeline
Pada tahap awal, susun kerangka dasar pipeline menggunakan beberapa stage utama seperti $match, $group, $sort, dan $project.
2. Lengkapi stage $match
Pastikan filter sesuai kebutuhan analisis, misalnya hanya mengambil produk yang aktif dengan { isActive: true }.
3. Lengkapi stage $group
Tambahkan akumulator untuk menghitung metrik yang diperlukan, seperti:
{ $sum: 1 } untuk menghitung jumlah produk
{ $avg: "$price" } untuk rata-rata harga
{ $sum: "$stock" } untuk total stok
{ $min, $max } untuk rentang harga
4. Tambahkan stage $sort
Urutkan hasil berdasarkan prioritas analitis, misalnya berdasarkan rata-rata harga tertinggi menggunakan { avgPrice: -1 }.
5. Lengkapi stage $project
Bentuk keluaran akhir yang rapi dan mudah dipahami. Pada tahap ini, Anda dapat:
- Mengubah _id menjadi category
- Membulatkan rata-rata harga
- Menggabungkan minPrice dan maxPrice menjadi priceRange
- Menyembunyikan _id jika tidak diperlukan
6. Refinement: pembulatan & format priceRange
Gunakan operator seperti $round, $toString, dan $concat untuk menghasilkan format angka dan rentang harga yang lebih mudah dibaca.
7. Uji pipeline secara bertahap
Jalankan pipeline per tahap mulai dari $match, kemudian $group, dan seterusnya untuk memastikan setiap bagian bekerja sesuai harapan.
Contoh hasil akhir (versi Aiman yang di-enchance)
async function getProductStats() {
const stats = await Product.aggregate([
{
$match: { isActive: true },
},
{
$group: {
_id: "$category",
productCount: { $sum: 1 },
avgPrice: { $avg: "$price" },
totalStock: { $sum: "$stock" },
maxPrice: { $max: "$price" },
minPrice: { $min: "$price" },
},
},
{
$sort: { avgPrice: -1 },
},
{
$project: {
category: "$_id",
productCount: 1,
avgPrice: { $round: ["$avgPrice", 0] }, // Round to integer
totalStock: 1,
priceRange: {
$concat: [
"Rp ",
{ $toString: "$minPrice" },
" - Rp ",
{ $toString: "$maxPrice" },
],
},
_id: 0,
},
},
]);
return stats;
}
Test dan Contoh Output:
const stats = await getProductStats();
console.log(stats);
// [
// {
// category: "Supplement",
// productCount: 5,
// avgPrice: 185000,
// totalStock: 125,
// priceRange: "Rp 150000 - Rp 220000"
// },
// ...
// ]
Hasil Akhir
Anda berhasil membangun sebuah aggregation pipeline yang ringkas, terstruktur, dan siap diterapkan untuk kebutuhan analitik. Pipeline ini dapat diintegrasikan ke dashboard admin atau laporan berkala, sekaligus mudah dimodifikasi sesuai perubahan kebutuhan bisnis.
Tips:
- - Mulailah dari struktur sederhana seperti $match, $group, dan $sort, kemudian tambahkan tahap lanjutan jika diperlukan.
- - Uji setiap tahap pipeline secara bertahap untuk memudahkan proses debugging.
- - Perhatikan performa, terutama pada koleksi besar; gunakan indeks yang relevan seperti pada isActive, category, atau price.
- - Gunakan stage $match sedini mungkin untuk mengurangi beban pemrosesan pada tahap berikutnya.
Kesimpulan
Relasi data di MongoDB memungkinkan kita untuk menghubungkan informasi antar koleksi layaknya hubungan antar tabel di database relasional. Namun, berbeda dari SQL, MongoDB menawarkan dua pendekatan utama: embedding (menyimpan data terkait langsung di dalam dokumen utama) dan referencing (menghubungkan data melalui ObjectId antar koleksi).
Embedding cocok untuk data yang sering diakses bersama dan tidak terlalu besar, sedangkan referencing lebih ideal untuk data kompleks atau dengan hubungan banyak-ke-banyak.
Dengan memahami kedua pendekatan ini, developer dapat merancang struktur database yang lebih efisien dan sesuai kebutuhan aplikasi. Pilihan antara embedding atau referencing sangat bergantung pada performa yang diinginkan, ukuran data, dan frekuensi akses antar entitas.
Komentar
Posting Komentar