Saat bekerja dengan database, ada masalah yang disebut "SELECT N +1" - ketika aplikasi, alih-alih satu permintaan ke database, yang memilih semua data yang diperlukan dari beberapa tabel terkait, koleksi, membuat subquery tambahan untuk setiap baris hasil dari hasil query pertama, untuk mendapatkan data terkait. Sebagai contoh, pertama kita mendapatkan daftar mahasiswa di mana spesialisasinya diidentifikasi oleh pengidentifikasi, dan kemudian untuk masing-masing siswa kita membuat subquery tambahan ke tabel atau kumpulan spesialisasi untuk mendapatkan nama spesialisasi dengan pengidentifikasi spesialisasi. Karena setiap subkueri mungkin memerlukan subkueri lain, dan subkueri lain - jumlah kueri ke basis data mulai tumbuh secara eksponensial.
Ketika bekerja dengan graphql, sangat mudah untuk menghasilkan masalah "SELECT N +1" jika Anda membuat subquery pada tabel tertaut dalam fungsi resolver. Hal pertama yang terlintas dalam pikiran adalah membuat permintaan segera dengan mempertimbangkan semua data terkait, tetapi ini, Anda harus setuju, tidak rasional jika data terkait tidak diminta oleh klien.
Salah satu solusi untuk masalah "SELECT N +1" untuk graphql akan dipertimbangkan dalam posting ini.
Misalnya, ambil dua koleksi: "Penulis" (Penulis) dan "Buku" (Buku). Hubungan itu, seperti yang orang duga, banyak-ke-banyak. Satu Penulis dapat memiliki beberapa Buku, dan satu Buku dapat ditulis oleh beberapa Penulis. Untuk menyimpan informasi, kami akan menggunakan database mongodb dan perpustakaan mongoose.js
Kami menyadari hubungan antara banyak-ke-banyak koleksi menggunakan koleksi BookAuthor tambahan dan bidang virtual.
Sekarang tentukan jenis Penulis dan Buku di graphql. Ada sedikit masalah dengan fakta bahwa jenis-jenis ini saling dirujuk. Oleh karena itu, untuk akses bersama mereka, pengikatan tautan ke objek modul ekspor digunakan, dan bukan pengikatan objek baru ke modul.ekspor (yang menggantikan objek asli), dan bidang bidang diimplementasikan sebagai fungsi, yang memungkinkan Anda untuk "menunda" membaca tautan ke objek saat dibuat sampai semua referensi melingkar tersedia:
Sekarang kami mendefinisikan permintaan Penulis, mungkin dengan daftar buku-buku mereka, dan, mungkin, dengan daftar penulis (rekan penulis) dari buku-buku ini.
const graphql = require('graphql'); const getFieldNames = require('graphql-list-fields'); const graphqlType = require('../graphqlType'); const mongoSchema = require('../mongoSchema'); module.exports = { type: new graphql.GraphQLList(graphqlType.Author), args: { _id: { type: graphql.GraphQLString } }, resolve: (_, {_id}, context, info) => { const fields = getFieldNames(info); const where = _id ? {_id} : {}; const authors = mongoSchema.Author.find(where) if (fields.indexOf('books.authors.name') > -1 ) { authors.populate({ path: 'books', populate: { path: 'book', populate: {path: 'authors', populate: {path: 'author'}} } }) } else if (fields.indexOf('books.title') > -1 ) { authors.populate({path: 'books', populate: {path: 'book'}}) } return authors.exec(); } };
Untuk menentukan permintaan bidang mana yang berasal dari klien, perpustakaan graphql-list-fields digunakan. Dan jika permintaan datang dengan objek bersarang, maka metode populate () dari perpustakaan luwak dipanggil.
Sekarang mari kita bereksperimen dengan kueri. Permintaan maksimum yang mungkin untuk implementasi kami:
{ author { _id name books { _id title authors { _id name } } } }
akan dilakukan oleh 5 panggilan ke database:
authors.find({}, { fields: {} }) bookauthors.find({ author: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcabd05b15d38f672357e"), ObjectId("5b0fcac405b15d38f672357f"), ObjectId("5b0fcad705b15d38f6723580"), ObjectId("5b0fcae305b15d38f6723581"), ObjectId("5b0fedb94ad5435896079cf1"), ObjectId("5b0fedbd4ad5435896079cf2") ] } }, { fields: {} }) books.find({ _id: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) bookauthors.find({ book: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) authors.find({ _id: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcad705b15d38f6723580") ] } }, { fields: {} })
di': [ObjectId ( "5b0fcab305b15d38f672357d"), ObjectId ( "5b0fcabd05b15d38f672357e"), ObjectId ( "5b0fcac405b15d38f672357f"), ObjectId ( "5b0fcad705b15d38f6723580"), ObjectId ( "5b0fcae305b15d38f6723581"), ObjectId authors.find({}, { fields: {} }) bookauthors.find({ author: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcabd05b15d38f672357e"), ObjectId("5b0fcac405b15d38f672357f"), ObjectId("5b0fcad705b15d38f6723580"), ObjectId("5b0fcae305b15d38f6723581"), ObjectId("5b0fedb94ad5435896079cf1"), ObjectId("5b0fedbd4ad5435896079cf2") ] } }, { fields: {} }) books.find({ _id: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) bookauthors.find({ book: { '$in': [ ObjectId("5b0fcb7105b15d38f6723582") ] } }, { fields: {} }) authors.find({ _id: { '$in': [ ObjectId("5b0fcab305b15d38f672357d"), ObjectId("5b0fcad705b15d38f6723580") ] } }, { fields: {} })
Seperti yang Anda lihat, fungsi mongoose.js - populate () - tidak menggunakan fitur mongodb yang relatif baru - $ lookup, tetapi membuat permintaan tambahan. Tapi ini bukan masalah "SELECT N +1" sejak itu Kueri baru tidak dibuat untuk setiap baris, tetapi untuk seluruh koleksi. (Keinginan untuk memeriksa bagaimana fungsi mongoose.js populate () benar-benar berfungsi - dengan satu permintaan atau beberapa - adalah salah satu motif untuk memilih database non-relasional untuk contoh ini).
Jika kami menggunakan permintaan minimalis:
{ author { _id name } }
maka itu hanya akan membentuk satu panggilan ke database:
authors.find({}, { fields: {} })
Ini, sebenarnya, saya cari. Sebagai kesimpulan, saya akan mengatakan bahwa ketika saya mulai mencari solusi untuk masalah ini, saya menemukan perpustakaan yang sangat nyaman dan canggih yang memecahkan masalah ini. Salah satunya, misalnya, yang sangat saya sukai, berdasarkan pada struktur basis data relasional, membentuk skema graphql dengan semua operasi yang diperlukan. Namun, pendekatan ini dapat diterima jika graphql digunakan di sisi belakang aplikasi. Jika Anda membuka akses ke layanan tersebut dari front-end aplikasi (yang saya butuhkan), maka ini mirip dengan menempatkan panel admin ke server database dalam akses terbuka, seperti semua tabel tersedia di luar kotak
Untuk kenyamanan pembaca, contoh kerja terletak di
repositori .
Tambahan oleh
komentar joniksPengguna joniks di umpan telah merujuk ke perpustakaan
https://github.com/facebook/dataloader . Mari kita lihat bagaimana perpustakaan ini memungkinkan Anda untuk mengatasi masalah "SELECT N +1"
Dengan perpustakaan ini, definisi tipe graphql Penulis akan terlihat seperti ini:
Apa gunanya menggunakan pustaka ini: permintaan single bookLoader.load (id) diakumulasikan dan dikirim untuk diproses dengan array pengidentifikasi const bookLoader = DataLoader baru (id async = = {...
Pada output, kita harus mengembalikan janji array atau array janji yang terletak dalam urutan yang sama dengan array input id.
Sekarang kami dapat menulis ulang permintaan kami untuk Penulis sebagai berikut:
Sebagai hasilnya, kami dapat meminta objek terkait dari tingkat bersarang yang sewenang-wenang tanpa khawatir tentang masalah SELECT N + 1 (meskipun dengan biaya panggilan tak bersyarat untuk mengisi () meskipun tidak diperlukan):
{ authors { _id name books { _id title authors { _id name books { _id title authors { _id name } } } } } }
Tapi di sini Anda harus benar-benar mengerti bahwa jika kita beralih ke bekerja dengan server SQL, maka pada setiap tingkat objek bersarang akan ada satu permintaan agregat. Pada saat yang sama, kadang-kadang diperlukan bahwa ini masih persis satu permintaan. Tetapi Anda tidak dapat mencapai ini secara langsung menggunakan pustaka dataloader. Contoh perubahan tersedia di cabang repositori data-loader.
apapacy@gmail.com
31 Mei 2018