Apa yang salah dengan GraphQL

Baru-baru ini, GraphQL semakin populer. Sintaksis permintaan, tipifikasi, dan langganan yang anggun.


Tampaknya: "ini dia - kami telah menemukan bahasa yang ideal untuk pertukaran data!" ...


Saya telah mengembangkan menggunakan bahasa ini selama lebih dari satu tahun, dan saya akan memberi tahu Anda: semuanya jauh dari mulus. GraphQL memiliki momen tidak nyaman dan masalah mendasar dalam desain bahasa itu sendiri.


Di sisi lain, sebagian besar "langkah desain" ini dibuat karena suatu alasan - ini karena berbagai pertimbangan. Faktanya, GraphQL bukan untuk semua orang, dan mungkin bukan alat yang Anda butuhkan sama sekali. Tetapi hal pertama yang pertama.


Saya pikir perlu memberi komentar kecil tentang di mana saya menggunakan bahasa ini. Ini adalah panel admin SPA yang agak rumit, sebagian besar operasi di mana CRUDs nontrivial (entitas kompleks). Bagian penting dari argumentasi dalam materi ini dihubungkan dengan sifat aplikasi dan sifat data yang diproses. Dalam aplikasi dengan tipe yang berbeda (atau dengan sifat data yang berbeda), masalah tersebut mungkin tidak muncul secara prinsip.

1. NON_NULL


Ini bukan masalah serius. Sebaliknya, ini adalah serangkaian ketidaknyamanan terkait dengan bagaimana pekerjaan dengan nullable di GraphQL diatur.


Ada bahasa pemrograman fungsional (dan tidak hanya), paradigma seperti itu adalah monad. Jadi, ada yang namanya monad Maybe (Haskel) atau Option (Scala), intinya adalah bahwa nilai yang terkandung dalam monad tersebut mungkin ada atau mungkin tidak ada (yaitu, null). Baik, atau dapat diimplementasikan melalui enum, seperti di Rust.


Dengan satu atau lain cara, dan dalam sebagian besar bahasa, nilai ini, yang "membungkus" aslinya, menjadikan null opsi tambahan ke yang utama. Dan secara sintaksis - selalu merupakan tambahan untuk tipe utama. Ini tidak selalu hanya tipe kelas yang terpisah - dalam beberapa bahasa, apakah ini hanya tambahan dalam bentuk akhiran atau awalan ? .


Dalam GraqhQL, yang terjadi adalah sebaliknya. Semua jenis dapat dibatalkan secara default - dan ini bukan hanya menandai jenis tersebut dapat dibatalkan, tetapi Maybe monad.


Dan jika kita mempertimbangkan bagian introspeksi bidang name untuk skema semacam itu:


 #       schema -  ,    schema { query: Query } type Query { #       NonNull name: String! } 

kami menemukan:


gambar


Jenis String dibungkus NON_NULL


1.1. OUTPUT


Kenapa begitu? Singkatnya, ini terhubung dengan desain bahasa "toleran" default (antara lain, ramah untuk arsitektur layanan mikro).


Untuk memahami esensi dari "toleransi" ini, pertimbangkan contoh yang sedikit lebih kompleks, di mana semua nilai yang dikembalikan secara ketat dibungkus dengan NON_NULL:


 type User { name: String! #  :       . friends: [User!]! } type Query { #  :       . users(ids: [ID!]!): [User!]! } 

Misalkan kita memiliki layanan yang mengembalikan daftar pengguna, dan "persahabatan" layanan mikro terpisah yang mengembalikan kecocokan untuk teman-teman pengguna. Kemudian, jika terjadi kegagalan layanan "persahabatan", kami tidak akan dapat membuat daftar pengguna sama sekali. Perlu memperbaiki situasi:


 type User { name: String! #    -  null   . #    ""  -      ,    . friends: [User!] } 

Ini adalah toleransi terhadap kesalahan internal. Contoh, tentu saja, dibuat-buat. Tapi saya harap Anda memahami esensinya.


Selain itu, Anda dapat membuat hidup Anda sedikit lebih mudah dalam situasi lain. Misalkan ada pengguna jarak jauh, dan pengidentifikasi teman dapat disimpan dalam beberapa struktur eksternal yang tidak terkait. Kami hanya bisa membuang dan mengembalikan hanya apa yang kita miliki, tetapi kemudian kita tidak akan bisa memahami apa yang sebenarnya dibuang.


 type Query { #  null   . #                . users(ids: [ID!]!): [User]! } 

Semuanya baik-baik saja. Dan apa masalahnya?
Secara umum, bukan masalah yang sangat besar - begitu rasanya. Tetapi jika Anda memiliki aplikasi monolitik dengan basis data relasional, maka kemungkinan besar kesalahannya benar-benar kesalahan, dan api harus seketat mungkin. Halo, tanda seru! Dimanapun kamu bisa.


Saya ingin dapat "membalikkan" perilaku ini, dan menempatkan tanda tanya alih-alih poin seru) Akan lebih akrab entah bagaimana.


1.2. INPUT


Tetapi ketika masuk, nullable adalah cerita yang sama sekali berbeda. Ini adalah ambang tingkat kotak centang dalam HTML (saya pikir semua orang mengingat ketidakjelasan ini ketika bidang kotak centang tidak dicentang sama sekali tidak dikirim ke belakang).


Pertimbangkan sebuah contoh:


 type Post { id: ID! title: String! #  :     null description: String content: String! } input PostInput { title: String! #  :     ,   description: String content: String! } type Mutation { createPost(post: PostInput!): Post! } 

Sejauh ini, sangat bagus. Tambahkan pembaruan:


 type Mutation { createPost(post: PostInput!): Post! updatePost(id: ID!, post: PostInput!): Post! } 

Dan sekarang pertanyaannya adalah: apa yang bisa kita harapkan dari bidang deskripsi saat memperbarui posting? Bidang mungkin nol, atau mungkin tidak ada sama sekali.


Jika bidangnya hilang, lalu apa yang perlu dilakukan? Jangan perbarui itu? Atau set ke nol? Intinya adalah bahwa memungkinkan nol dan membiarkan tidak adanya bidang adalah dua hal yang berbeda. Namun, GraphQL adalah hal yang sama.


2. Pemisahan input dan output


Ini hanya rasa sakit. Dalam model kerja CRUD, Anda mendapatkan objek dari belakang "memelintir" itu, dan mengirimkannya kembali. Secara kasar, ini adalah satu dan objek yang sama. Tetapi Anda hanya perlu menggambarkannya dua kali - untuk input dan output. Dan tidak ada yang bisa dilakukan dengan ini, kecuali menulis generator kode untuk bisnis ini. Saya lebih suka memisahkan ke "input dan output" bukan objek itu sendiri, tetapi bidang objek. Misalnya, pengubah:


 type Post { input output text: String! output updatedAt(format: DateFormat = W3C): Date! } 

atau menggunakan arahan:


 type Post { text: String! @input @output updatedAt(format: DateFormat = W3C): Date! @output } 

3. Polimorfisme


Masalah memisahkan jenis menjadi input dan output tidak terbatas pada deskripsi ganda. Pada saat yang sama, untuk tipe generik Anda dapat mendefinisikan antarmuka umum:


 interface Commentable { comments: [Comment!]! } type Post implements Commentable { text: String! comments: [Comment!]! } type Photo implements Commentable { src: URL! comments: [Comment!]! } 

atau serikat pekerja


 type Person { firstName: String, lastName: String, } type Organiation { title: String } union Subject = Organiation | Person type Account { login: String subject: Subject } 

Anda tidak dapat melakukan hal yang sama untuk tipe input. Ada sejumlah prasyarat untuk ini, tetapi ini sebagian karena fakta bahwa json digunakan sebagai format data untuk transportasi. Namun, dalam output, bidang __typename digunakan untuk menentukan tipe. Mengapa tidak mungkin untuk melakukan hal yang sama ketika masuk - tidak terlalu jelas. Tampak bagi saya bahwa masalah ini dapat diselesaikan sedikit lebih elegan dengan meninggalkan json selama transportasi dan memasukkan formatnya sendiri. Sesuatu dalam roh:


 union Subject = OrganiationInput | PersonInput input AccountInput { login: String! password: String! subject: Subject! } 

 #     { account: AccountInput { login: "Acme", password: "***", subject: OrganiationInput { title: "Acme Inc" } } } 

 #      { account: AccountInput { login: "Acme", password: "***", subject: PersonInput { firstName: "Vasya", lastName: "Pupkin", } } } 

Tetapi ini akan membuatnya perlu untuk menulis parser tambahan untuk bisnis ini.


4. Generik


Apa yang salah dengan GraphQL dengan obat generik? Dan semuanya sederhana - mereka tidak. Mari kita ambil kueri indeks CRUD khas dengan pagination atau kursor - itu tidak penting. Saya akan memberi contoh dengan pagination.


 input Pagination { page: UInt, perPage: UInt, } type Query { users(pagination: Pagination): PageOfUsers! } type PageOfUsers { total: UInt items: [User!]! } 

dan sekarang untuk organisasi


 type Query { organizations(pagination: Pagination): PageOfOrganizations! } type PageOfOrganizations { total: UInt items: [Organization!]! } 

dan seterusnya ... bagaimana saya ingin memiliki obat generik untuk ini


 type PageOf<T> { total: UInt items: [T!]! } 

maka saya hanya akan menulis


 type Query { users(page: UInt, perPage: UInt): PageOf<User>! } 

Ya banyak aplikasi! Haruskah saya memberi tahu Anda tentang obat generik?


5. Ruang nama


Mereka juga tidak ada di sana. Ketika jumlah jenis dalam sistem lebih dari satu setengah ratus, kemungkinan tabrakan nama cenderung seratus persen.


Dan segala macam Service_GuideNDriving_Standard_Model_Input muncul. Saya tidak berbicara tentang ruang nama lengkap di titik akhir yang berbeda, seperti di SOAP (ya, ya, itu mengerikan, tetapi ruang nama dibuat di sana dengan sempurna). Dan setidaknya beberapa skema pada satu titik akhir dengan kemampuan untuk "meraba-raba" jenis antar skema.


Total


GraphQL adalah alat yang bagus. Ini sangat cocok dengan arsitektur layanan mikro yang toleran, yang berorientasi, pertama-tama, pada keluaran informasi, dan input sederhana dan deterministik.


Jika Anda memiliki entitas polimorfik untuk dimasukkan, Anda mungkin memiliki masalah.
Pemisahan tipe input dan output, serta kurangnya obat generik - menghasilkan banyak coretan dari awal.


Graphql tidak benar-benar (dan kadang-kadang tidak sama sekali ) tentang CRUD.


Tetapi ini tidak berarti bahwa Anda tidak dapat memakannya :)


Pada artikel selanjutnya, saya ingin berbicara tentang bagaimana saya bertarung (dan terkadang berhasil) dengan beberapa masalah yang dijelaskan di atas.

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


All Articles