Kursor pada driver

Mengiterasi kursor dari aplikasi Node.js dan Python, termasuk metode kursor yang tersedia.

Daftar isi

Hasil query dari kode aplikasi tidak tiba sebagai array yang berisi seluruh dokumen sekaligus. Driver MongoDB, yaitu pustaka yang menghubungkan aplikasi dengan server, mengemas hasil query dalam kursor (cursor) yang mengalir melalui batch. Batch adalah kelompok dokumen yang dikirim dalam satu respons jaringan. Topik ini membahas cara mengiterasi kursor dari aplikasi Node.js dan Python dengan driver resminya, mulai dari pola iterasi, mekanisme pembagian batch, sampai metode kursor yang tersedia pada kedua driver. Contohnya memakai dua program lengkap pada basis data sensor dengan koleksi pembacaan yang sama dengan topik lain di modul ini. Setelah topik ini diikuti, hasil query dapat dibaca dari kode aplikasi secara efisien untuk hasil kecil maupun besar.

Kursor yang dikembalikan find#

Metode find pada kedua driver tidak mengembalikan dokumen, melainkan objek kursor. PyMongo menamainya Cursor, sedangkan driver Node.js menamainya FindCursor. Keduanya tidak langsung mengirim query ke server. Perintah find baru dikirim ketika dokumen pertama diminta, sehingga kursor yang dibuat tetapi tidak pernah diiterasi tidak pernah menyentuh jaringan. Penundaan inilah yang membuat metode seperti sort dan limit dapat dirangkai setelah find, karena rangkaian metode itu baru membentuk satu query utuh pada saat iterasi dimulai.

Bentuk perulangan mengikuti bahasa yang dipakai. Pada Python, kursor adalah objek yang dapat diiterasi secara biasa, sehingga perulangan for d in kursor sudah cukup. Pada Node.js, kursor hanya mendukung iterasi asinkron karena setiap langkahnya menunggu data dari jaringan, sehingga perulangannya memakai bentuk for await (const d of kursor). Selama perulangan berjalan, driver meminta hasil ke server bagian demi bagian, dan posisi pembacaan tersimpan sebagai cursor id di sisi server hingga hasil habis atau kursor ditutup.

Pengiriman hasil dalam batch#

Server mengirim hasil query bukan dokumen demi dokumen, melainkan dalam batch. Batch pertama memuat paling banyak 101 dokumen, sedangkan batch berikutnya dapat memuat lebih banyak dokumen dengan batas ukuran 16 MiB per batch. Apabila dokumen di sisi klien sudah habis sedangkan kursor masih menyimpan sisa hasil, driver meminta batch berikutnya melalui perintah getMore secara otomatis. Dari sisi aplikasi, iterasi terasa mengalir tanpa putus, padahal pertukaran jaringan terjadi sekali per batch.

mongosh menyembunyikan pembagian ini karena metode seperti forEach dan toArray selalu menguras kursor sampai habis. Pembagian batch dapat dilihat langsung melalui runCommand, yaitu cara mengirim perintah ke server tanpa metode pembungkus shell. Blok berikut menyiapkan data contoh berisi dua belas dokumen, kemudian meminta hasil dengan batchSize bernilai 3 agar isi batch pertamanya terlihat jelas.

mongosh
use sensor
const hasil = db.pembacaan.insertMany(Array.from({ length: 12 }, (_, m) => ({ menit: m, sensor: "S-0" + (m % 3 + 1), suhu: (220 + m * 4) / 10 })))
print("Dokumen tersimpan: " + db.pembacaan.countDocuments())
text Keluaran
switched to db sensor

Dokumen tersimpan: 12
mongosh
const balasan = db.runCommand({ find: "pembacaan", batchSize: 3, projection: { _id: 0, menit: 1, suhu: 1 } })
balasan.cursor.firstBatch
balasan.cursor.id.greaterThan(Long.ZERO) ? "kursor masih terbuka" : "kursor sudah habis"
text Keluaran
[
  { menit: 0, suhu: 22 },
  { menit: 1, suhu: 22.4 },
  { menit: 2, suhu: 22.8 }
]
kursor masih terbuka

Balasan find hanya memuat tiga dokumen pada field firstBatch, sedangkan kursor masih terbuka di server. Status terbuka tersebut terlihat dari cursor id yang bukan nol, yaitu penanda posisi yang disimpan server selama kursor masih menyimpan sisa hasil. Sembilan dokumen lainnya menunggu batch berikutnya, dan permintaan getMore berikut mengambilnya secara eksplisit.

mongosh
db.runCommand({ getMore: balasan.cursor.id, collection: "pembacaan", batchSize: 3 }).cursor.nextBatch
text Keluaran
[
  { menit: 3, suhu: 23.2 },
  { menit: 4, suhu: 23.6 },
  { menit: 5, suhu: 24 }
]

Alur pengiriman batch dari server ke aplikasi digambarkan pada diagram berikut. Driver meminta batch berikutnya tepat ketika dokumen yang tersimpan di sisi klien habis, sehingga kode aplikasi tidak perlu mengetahui batas batch sama sekali.

ServerMongoDBAplikasiNode.js / Pythonbatch 1, paling banyak 101 dokumenbatch 2batch berikutnya diambil otomatis saat hasil di klien habisposisi hasil disimpan pada kursor di sisi server
Pengiriman hasil query dalam batch dari server ke aplikasi

Pembagian batch menjaga memori aplikasi tetap hemat karena hanya sebagian hasil yang berada di klien pada satu waktu. Pola ini juga menjaga jaringan tetap efisien karena dokumen tidak dikirim satu per satu. Ukuran batch diubah melalui metode batchSize pada kursor bila pola bawaannya tidak sesuai kebutuhan aplikasi.

Mengiterasi kursor dengan PyMongo#

Program app.py berikut mengiterasi kursor hasil query menggunakan PyMongo. Koneksi dibuat lewat MongoClient dengan alamat server lokal, lalu koleksi ditunjuk melalui client["sensor"]["pembacaan"] tanpa pernyataan pembuatan eksplisit karena MongoDB membuat basis data dan koleksi tersebut otomatis saat dokumen pertama disisipkan. Program membersihkan koleksi pembacaan, mengisi ulang dua belas dokumen pengukuran dengan field menit, sensor, dan suhu, lalu menjalankan dua pola pembacaan. Pola pertama mengiterasi kursor dokumen demi dokumen dengan for, sedangkan pola kedua merangkai sort dan limit sebelum seluruh hasil diambil dengan list.

python app.py
# Mengiterasi kursor hasil query pada PyMongo
from pymongo import MongoClient

client = MongoClient("mongodb://127.0.0.1:27017")
koleksi = client["sensor"]["pembacaan"]
koleksi.delete_many({})

data = [{"menit": m, "sensor": "S-0" + str(m % 3 + 1), "suhu": (220 + m * 4) / 10} for m in range(12)]
koleksi.insert_many(data)

# Pola 1: kursor diiterasi dokumen demi dokumen dengan for
kursor = koleksi.find({"suhu": {"$gte": 23}}).sort("menit", 1)
for d in kursor:
    print(d["menit"], d["sensor"], d["suhu"])

# Pola 2: metode kursor dirangkai, lalu seluruh hasil diambil dengan list()
teratas = list(koleksi.find().sort("suhu", -1).limit(3))
for d in teratas:
    print("teratas: menit", d["menit"], "suhu", d["suhu"])

client.close()
text Keluaran
3 S-01 23.2
4 S-02 23.6
5 S-03 24.0
6 S-01 24.4
7 S-02 24.8
8 S-03 25.2
9 S-01 25.6
10 S-02 26.0
11 S-03 26.4
teratas: menit 11 suhu 26.4
teratas: menit 10 suhu 26.0
teratas: menit 9 suhu 25.6

Pola pertama menyaring dokumen dengan suhu minimal 23, mengurutkannya menaik berdasarkan menit, lalu mencetak field dari setiap dokumen saat perulangan berjalan. Sembilan baris pertama keluaran berasal dari pola tersebut, yaitu dokumen dengan menit 3 sampai 11. Pola kedua tidak beriterasi manual, melainkan mengambil tiga dokumen dengan suhu tertinggi ke dalam daftar, sehingga tiga baris terakhir keluaran menampilkan menit 11, 10, dan 9.

Perulangan for pada Python meminta hasil ke driver secara diam-diam setiap kali dokumen berikutnya dibutuhkan, sehingga seluruh mekanisme batch tetap bekerja tanpa kode tambahan. Koneksi ditutup dengan client.close() setelah pembacaan selesai. Program dijalankan dari folder topik dengan perintah berikut.

bash
# menjalankan program PyMongo dari folder topik
python3 app.py
text Keluaran
3 S-01 23.2
4 S-02 23.6
5 S-03 24.0
6 S-01 24.4
7 S-02 24.8
8 S-03 25.2
9 S-01 25.6
10 S-02 26.0
11 S-03 26.4
teratas: menit 11 suhu 26.4
teratas: menit 10 suhu 26.0
teratas: menit 9 suhu 25.6

Mengiterasi kursor dengan driver Node.js#

Program app.js melakukan alur yang sama menggunakan driver Node.js. Alamat server pada new MongoClient(...) juga 127.0.0.1 dengan port bawaan 27017, sama seperti yang dipakai mongosh. Karena seluruh operasi jaringan bersifat asinkron, program membungkus alurnya dalam fungsi async dan mengiterasi kursor dengan for await. Pola kedua mengambil seluruh hasil sekaligus dengan toArray, yaitu padanan list pada PyMongo. Di bagian bawah berkas, main().catch(...) menangkap error koneksi maupun query agar program berakhir dengan kode keluar yang jelas.

js app.js
// Mengiterasi kursor hasil query pada driver Node.js
const { MongoClient } = require("mongodb");

const client = new MongoClient("mongodb://127.0.0.1:27017");

async function main() {
  await client.connect();
  const koleksi = client.db("sensor").collection("pembacaan");
  await koleksi.deleteMany({});

  const data = Array.from({ length: 12 }, (_, m) => ({ menit: m, sensor: "S-0" + (m % 3 + 1), suhu: (220 + m * 4) / 10 }));
  await koleksi.insertMany(data);

  // Pola 1: kursor diiterasi dokumen demi dokumen dengan for await
  const kursor = koleksi.find({ suhu: { $gte: 23 } }).sort({ menit: 1 });
  for await (const d of kursor) {
    console.log(d.menit + " " + d.sensor + " " + d.suhu);
  }

  // Pola 2: metode kursor dirangkai, lalu seluruh hasil diambil dengan toArray
  const teratas = await koleksi.find().sort({ suhu: -1 }).limit(3).toArray();
  for (const d of teratas) {
    console.log("teratas: menit " + d.menit + " suhu " + d.suhu);
  }

  await client.close();
}

main().catch((e) => { console.error(e); process.exitCode = 1; });
text Keluaran
3 S-01 23.2
4 S-02 23.6
5 S-03 24
6 S-01 24.4
7 S-02 24.8
8 S-03 25.2
9 S-01 25.6
10 S-02 26
11 S-03 26.4
teratas: menit 11 suhu 26.4
teratas: menit 10 suhu 26
teratas: menit 9 suhu 25.6

Keluaran kedua program berisi nilai yang sama, tetapi tampilan bilangan pecahan bernilai bulat berbeda. Python mencetak 24.0 dan 26.0 lengkap dengan satu desimal, sedangkan JavaScript mencetak 24 dan 26 tanpa desimal. Keduanya membaca tipe double yang sama dari BSON, sehingga perbedaan itu murni cara pencetakan masing-masing bahasa dan tidak memengaruhi isi data.

Package mongodb untuk Node.js pada lingkungan latihan jalur ini terpasang di folder tersendiri, sehingga perintah eksekusinya menetapkan NODE_PATH terlebih dahulu agar package dapat ditemukan. Program dijalankan dari folder topik dengan perintah berikut.

bash
# menetapkan lokasi package driver, lalu menjalankan program Node.js
export NODE_PATH=/home/kal/tools/node-driver/node_modules
node app.js
text Keluaran
3 S-01 23.2
4 S-02 23.6
5 S-03 24
6 S-01 24.4
7 S-02 24.8
8 S-03 25.2
9 S-01 25.6
10 S-02 26
11 S-03 26.4
teratas: menit 11 suhu 26.4
teratas: menit 10 suhu 26
teratas: menit 9 suhu 25.6

Metode kursor yang tersedia#

Kedua driver menyediakan metode kursor yang saling setara, dengan beberapa nama yang berbeda. Metode sort, limit, dan skip dipakai dengan nama yang sama seperti di mongosh, sehingga query yang dipraktikkan pada modul ini dapat dipindahkan ke kode aplikasi tanpa perubahan konsep. Metode pengambilan hasil menyesuaikan konvensi masing-masing bahasa. Tabel berikut merangkum metode yang paling sering dipakai sehari-hari.

KebutuhanDriver Node.jsPyMongo
mengiterasi dokumen satu per satufor awaitfor
mengambil seluruh hasil sekaligustoArray()list()
dokumen berikutnya satu per satunext()next()
mengecek sisa dokumenhasNext()alive
membatasi jumlah dokumenlimit()limit()
mengurutkan hasilsort()sort()
melewatkan dokumenskip()skip()
mengatur ukuran batchbatchSize()batch_size()
menutup kursor sebelum habisclose()close()

Pemilihan antara memuat seluruh hasil sekaligus (materialisasi) dan iterasi langsung ditentukan oleh ukuran hasil. toArray() dan list() mengumpulkan seluruh dokumen ke memori sebelum program melanjutkan, sehingga nyaman untuk hasil kecil seperti daftar teratas, tetapi membebani memori pada hasil besar. Iterasi langsung mengolah satu dokumen pada satu waktu, sehingga penggunaan memorinya tetap rendah berapa pun jumlah hasilnya.

Kursor yang diiterasi sampai habis ditutup secara otomatis oleh kedua driver, sehingga sumber daya di server langsung dilepas. Metode close() diperlukan hanya apabila iterasi berhenti di tengah jalan, misalnya karena jumlah dokumen yang dibutuhkan sudah tercapai. Metode next() memberi kendali penuh atas langkah iterasi dan menandakan kursor habis dengan cara yang berbeda pada tiap bahasa, yaitu null pada Node.js dan StopIteration pada Python.

Kesalahan umum#

Kesalahan pertama adalah memperlakukan hasil find sebagai array. Pada PyMongo, len(koleksi.find()) melemparkan TypeError karena objek Cursor tidak memiliki panjang. Di Node.js, FindCursor juga bukan array, sehingga kode yang memperlakukannya seperti array tidak berjalan dengan benar. Perbaikannya adalah mematerialisasi kursor terlebih dahulu dengan list() atau toArray(), atau mengolah hasilnya melalui iterasi.

Kesalahan kedua adalah mengiterasi kursor Node.js dengan for biasa. Objek FindCursor hanya menyediakan iterasi asinkron, sehingga perulangan tanpa await melemparkan error sebelum satu dokumen pun terbaca. Perbaikannya adalah memakai for await atau mengambil seluruh hasil dengan toArray() yang selalu disertai await.

Kesalahan ketiga adalah menunda iterasi terlalu lama di tengah jalan. Server menghapus kursor yang tidak pernah meminta batch berikutnya selama sepuluh menit, sesuai pengaturan bawaan cursorTimeoutMillis yang bernilai 600000 milidetik, sehingga permintaan batch berikutnya melemparkan error CursorNotFound. Perbaikannya adalah menyelesaikan iterasi tanpa jeda panjang atau mengulangi query dari awal apabila error itu terjadi.

Ringkasan#

  • find pada driver mengembalikan kursor, bukan dokumen, dan query baru dikirim ke server ketika iterasi dimulai.
  • Server mengirim hasil dalam batch, yaitu batch pertama paling banyak 101 dokumen dan batch berikutnya hingga 16 MiB, yang diminta driver secara otomatis melalui getMore.
  • Kursor diiterasi dengan for pada PyMongo dan for await pada driver Node.js, sedangkan list() dan toArray() memuat seluruh hasil ke memori.
  • Metode sort, limit, skip, dan batchSize tersedia pada kedua driver, dengan batch_size sebagai penamaan versi PyMongo.

Referensi#