Bekerja dengan data saat membangun API berdasarkan GraphQL

Pembukaan


Pertama-tama, artikel ini dirancang untuk pembaca yang sudah terbiasa dengan GraphQL dan lebih banyak tentang seluk-beluk dan nuansa bekerja dengannya. Meskipun demikian, saya berharap ini akan bermanfaat bagi pemula.


GraphQL adalah alat yang hebat. Saya pikir banyak orang sudah tahu dan mengerti kelebihannya. Namun, ada beberapa nuansa yang harus diperhatikan ketika membangun API berbasis GraphQL Anda.


Misalnya, GraphQL memungkinkan Anda untuk kembali ke konsumen (pengguna atau program) yang meminta data hanya bagian yang diminati konsumen ini. Namun demikian, ketika membangun server cukup mudah untuk membuat kesalahan, yang mengarah pada fakta bahwa di dalam server (yang dapat, antara lain, didistribusikan), data akan berjalan dalam "bundel" penuh. Hal ini terutama disebabkan oleh kenyataan bahwa GraphQL sendiri tidak menyediakan alat yang mudah untuk mem-parsing permintaan yang masuk, dan antarmuka yang diletakkan di dalamnya tidak didokumentasikan dengan baik.


Sumber masalah


Mari kita lihat contoh khas dari implementasi yang tidak optimal (buka gambar di jendela terpisah jika kurang dibaca):


gambar


Misalkan konsumen kita adalah aplikasi atau komponen tertentu dari "buku telepon" yang meminta dari API kita hanya pengidentifikasi, nama, dan nomor telepon pengguna yang disimpan oleh kita. Pada saat yang sama, API kami jauh lebih luas, itu akan memungkinkan akses ke data lain, seperti alamat fisik tempat tinggal dan alamat email pengguna.


Pada titik pertukaran data antara konsumen dan API, GraphQL dengan sempurna melakukan semua pekerjaan yang kita butuhkan - hanya data yang diminta akan dikirim sebagai tanggapan atas permintaan tersebut. Masalah dalam hal ini adalah pada titik pengambilan sampel data dari database - yaitu dalam implementasi internal server kami, dan itu terdiri atas fakta bahwa untuk setiap permintaan yang masuk kami memilih semua data pengguna dari basis data, terlepas dari kenyataan bahwa kami tidak memerlukan sebagian dari mereka. Ini menghasilkan beban berlebihan pada database dan menyebabkan sirkulasi lalu lintas yang berlebihan dalam sistem. Dengan sejumlah besar kueri, Anda bisa mendapatkan optimasi yang signifikan dengan mengubah pendekatan pengambilan sampel data dan memilih hanya bidang yang diminta. Pada saat yang sama, sama sekali tidak masalah sumber data kami - basis data relasional, teknologi NoSQL, atau layanan lain (internal atau eksternal). Perilaku tidak optimal dapat dipengaruhi oleh implementasi apa pun. MySQL dalam hal ini dipilih hanya sebagai contoh.


Solusi


Dimungkinkan untuk mengoptimalkan perilaku server ini jika kami menganalisis argumen yang datang ke fungsi resolve() :


 async resolve(source, args, context, info) { // ... } 

Ini adalah argumen terakhir, info , yang menarik bagi kami, dalam hal ini. Kami beralih ke dokumentasi dan menganalisis secara detail apa fungsi resolve() dan argumen yang kami minati terdiri dari:


 type GraphQLFieldResolveFn = ( source?: any, args?: {[argName: string]: any}, context?: any, info?: GraphQLResolveInfo ) => any type GraphQLResolveInfo = { fieldName: string, fieldNodes: Array<Field>, returnType: GraphQLOutputType, parentType: GraphQLCompositeType, schema: GraphQLSchema, fragments: { [fragmentName: string]: FragmentDefinition }, rootValue: any, operation: OperationDefinition, variableValues: { [variableName: string]: any }, } 

Jadi, tiga argumen pertama yang diteruskan ke resolver adalah source - data yang dikirimkan dari simpul induk dalam pohon GraphQL skema, args - argumen permintaan (yang berasal dari permintaan), dan context - objek konteks eksekusi yang ditentukan pengembang, sering dipanggil untuk mengirimkan beberapa data global di "resolvers." Dan akhirnya, argumen keempat adalah informasi meta tentang permintaan tersebut.


Apa yang bisa kita ekstrak dari GraphQLResolveInfo untuk menyelesaikan masalah kita?


Bagian yang paling menarik adalah:


  • fieldName adalah nama bidang saat ini dari skema GraphQL mereka. Yaitu itu sesuai dengan nama bidang yang ditentukan dalam skema untuk resolver ini. Jika kami menangkap objek info di bidang users , seperti dalam contoh kami di atas, maka "pengguna" yang akan dimuat sebagai nilai fieldName
  • fieldNodes - koleksi (array) node yang DIMINTA dalam kueri. Apa yang dibutuhkan!
  • fragments - kumpulan fragmen permintaan (jika permintaan itu terpecah-pecah). Juga informasi penting untuk mengambil bidang data akhir.

Jadi, sebagai solusi, kita perlu mengurai alat info dan memilih daftar bidang yang datang kepada kita dari kueri, dan kemudian meneruskannya ke kueri SQL. Sayangnya, paket GraphQL dari Facebook "out of the box" tidak memberi kami apa pun untuk menyederhanakan tugas ini. Secara keseluruhan, seperti yang telah ditunjukkan oleh praktik, tugas ini tidak sepele, mengingat fakta bahwa permintaan dapat terpecah-pecah. Dan selain itu, analisis semacam itu memiliki solusi universal, yang kemudian disalin dari proyek ke proyek.


Jadi saya memutuskan untuk menulisnya sebagai pustaka sumber terbuka di bawah lisensi ISC . Dengan bantuannya, solusi untuk mem-parsing bidang permintaan masuk diselesaikan dengan cukup sederhana, misalnya, dalam kasus kami seperti ini:


 const { fieldsList } = require('graphql-fields-list'); // ... async resolve(source, args, context, info) { const requestedFields = fieldsList(info); return await database.query(`SELECT ${requestedFields.join(',')} FROM users`) } 

fieldsList(info) dalam hal ini melakukan semua pekerjaan untuk kami dan mengembalikan array "flat" bidang anak untuk resolver ini, mis. Kueri SQL final kami akan terlihat seperti ini:


 SELECT id, name, phone FROM users; 

Jika kami mengubah permintaan masuk ke:


 query UserListQuery { users { id name phone email } } 

maka query SQL akan berubah menjadi:


 SELECT id, name, phone, email FROM users; 

Namun, itu tidak selalu mungkin dilakukan dengan tantangan sederhana. Seringkali, aplikasi nyata dalam struktur jauh lebih kompleks. Dalam beberapa implementasi, kita mungkin perlu menggambarkan resolver di tingkat atas sehubungan dengan data dalam skema GraphQL akhir. Misalnya, jika kami memutuskan untuk menggunakan pustaka Relay , kami ingin menggunakan mekanisme yang sudah jadi untuk memecah koleksi objek data ke halaman, yang mengarah pada fakta bahwa skema GraphQL kami akan dibangun sesuai dengan aturan tertentu. Misalnya, kami mengerjakan ulang skema kami dengan cara ini (TypeScript):


 import { GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; import { connectionDefinitions, connectionArgs, nodeDefinitions, fromGlobalId, globalIdField, connectionFromArray, GraphQLResolveInfo, } from 'graphql-relay'; import { fieldsList } from 'graphql-fields-list'; export const { nodeInterface, nodeField } = nodeDefinitions(async (globalId: string) => { const { type, id } = fromGlobalId(globalId); let node: any = null; if (type === 'User') { node = await database.select(`SELECT id FROM user WHERE id="${id}"`); } return node; }); const User = new GraphQLObjectType({ name: 'User', interfaces: [nodeInterface], fields: { id: globalIdField('User', (user: any) => user.id), name: { type: GraphQLString }, email: { type: GraphQLString }, phone: { type: GraphQLString }, address: { type: GraphQLString }, } }); export const { connectionType: userConnection } = connectionDefinitions({ nodeType: User }); const Query = new GraphQLObjectType({ name: 'Query', fields: { node: nodeField, users: { type: userConnection, args: { ...connectionArgs }, async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { // TODO: implement }, }, }); export const schema = new GraphQLSchema({ query: Query }); 

Dalam kasus ini, connectionDefinition dari Relay akan menambahkan edges , node , pageInfo dan cursor nodes ke dalam skema, mis. kita sekarang perlu membangun kembali kueri kita secara berbeda (kita tidak akan membahas pagination sekarang):


 query UserListQuery { users { edges { node { id name phone email } } } } 

Jadi, resolve() fungsi yang diterapkan pada simpul users sekarang harus menentukan bidang mana yang diminta bukan untuk dirinya sendiri, tetapi untuk simpul node anak bersarangnya, yang, seperti yang kita lihat, relatif terhadap users sepanjang edges.node path.


fieldsList dari graphql-fields-list akan membantu menyelesaikan masalah ini, untuk ini Anda harus meneruskan opsi path sesuai dengannya. Sebagai contoh, berikut adalah implementasinya dalam kasus kami:


 async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { const fields = fieldsList(info, { path: 'edges.node' }); return connectionFromArray( await database.query(`SELECT ${fields.join(',')} FROM users`), args ); } 

Juga di dunia nyata, mungkin dalam skema GraphQL kami hanya menetapkan satu nama bidang, dan dalam skema database, nama bidang lainnya sesuai dengan mereka. Misalnya, anggap tabel pengguna dalam database didefinisikan secara berbeda:


 CREATE TABLE users ( id BIGINT PRIMARY KEY AUTO_INCREMENT, fullName VARCHAR(255), email VARCHAR(255), phoneNumber VARCHAR(15), address VARCHAR(255) ); 

Dalam kasus ini, bidang dari kueri GraphQL harus diganti namanya sebelum disematkan dalam kueri SQL. fieldsList akan membantu dalam hal ini jika Anda melewatinya peta terjemahan nama dalam opsi transform sesuai:


 async resolve( source: any, args: {[argName: string]: any}, context: any, info: GraphQLResolveInfo, ) { const fields = fieldsList(info, { path: 'edges.node', transform: { phone: 'phoneNumber', name: 'fullName' }, }); return connectionFromArray( await database.query(`SELECT ${fields.join(',')} FROM users`), args ); } 

Namun, kadang-kadang, mengonversi ke array bidang datar tidak cukup (misalnya, jika sumber data mengembalikan struktur kompleks dengan bersarang). Dalam hal ini, fungsi fieldsMap dari graphql-fields-list akan datang untuk menyelamatkan, yang mengembalikan seluruh pohon bidang yang diminta sebagai objek:


 const { fieldsMap } = require(`graphql-fields-list`); // ... some resolver implementation on `users`: resolve(arc, args, ctx, info) { const map = fieldsMap(info); /* RESULT: { edges: { node: { id: false, name: false, phone: false, } } } */ } 

Jika kita menganggap bahwa pengguna dideskripsikan oleh struktur yang kompleks, kita akan mendapatkan semuanya. Metode ini juga dapat mengambil argumen path opsional, yang memungkinkan Anda untuk mendapatkan peta hanya cabang yang diperlukan dari seluruh pohon, misalnya:


 const { fieldsMap } = require(`graphql-fields-list`); // ... some resolver implementation on `users`: resolve(arc, args, ctx, info) { const map = fieldsMap(info, 'edges.node'); /* RESULT: { id: false, name: false, phone: false, } */ } 

Transformasi nama pada kartu saat ini tidak didukung dan tetap berada di tangan pengembang.


Minta Fragmentasi


GraphQL mendukung fragmentasi kueri, misalnya, kami dapat berharap konsumen mengirimkan permintaan seperti itu (di sini kami merujuk pada skema asli, sedikit dibuat-buat, tetapi secara sintaksis benar):


 query UsersFragmentedQuery { users { id ...NamesFramgment ...ContactsFragment } } fragment NamesFragment on User { name } fragment AddressFragment on User { address } fragment ContactsFragment on User { phone email ...AddressFragment } 

Anda tidak perlu khawatir dalam hal ini, dan fieldsList(info) , dan fieldsMap(info) dalam kasus ini akan mengembalikan hasil yang diharapkan, karena mereka mempertimbangkan kemungkinan memecah permintaan. Jadi, fieldsList(info) akan mengembalikan ['id', 'name', 'phone', 'email', 'address'] , dan fieldsMap(info) , masing-masing, akan kembali:


 { id: false, name: false, phone: false, email: false, address: false } 

PS


Saya harap artikel ini membantu menjelaskan beberapa nuansa bekerja dengan GraphQL di server, dan pustaka graphql-bidang-daftar dapat membantu Anda membuat solusi optimal di masa depan.


UPD 1


Versi 1.1.0 perpustakaan telah dirilis - dukungan untuk @skip dan @include dalam permintaan telah ditambahkan. Secara default, opsi ini diaktifkan, jika perlu, nonaktifkan seperti ini:


 fieldsList(info, { withDirectives: false }) fieldsMap(info, null, false); 

Source: https://habr.com/ru/post/id427399/


All Articles