Tahun petualangan dengan graphene-python

Hai semuanya, saya adalah pengembang python. Tahun lalu saya bekerja dengan graphene-python + django ORM dan selama ini saya mencoba membuat beberapa jenis alat untuk membuat bekerja dengan graphene lebih nyaman. Sebagai hasilnya, saya mendapat basis kode graphene-framework
kecil dan seperangkat aturan, yang ingin saya bagikan.

Apa itu graphene-python?
Menurut graphene-python.org , maka:
Graphene-Python adalah perpustakaan untuk membuat API GraphQL dengan mudah menggunakan Python. Tugas utamanya adalah untuk menyediakan API yang sederhana tetapi pada saat yang sama dapat diperpanjang untuk membuat kehidupan programmer lebih mudah.
Tugas utamanya adalah untuk menyediakan API yang sederhana tetapi pada saat yang sama dapat diperpanjang untuk membuat kehidupan programmer lebih mudah.
Ya, pada kenyataannya graphene sederhana dan dapat dikembangkan, tetapi bagi saya sepertinya terlalu sederhana untuk aplikasi besar dan cepat berkembang. Dokumentasi pendek (saya menggunakan kode sumber sebagai gantinya - itu jauh lebih verbose), serta kurangnya standar untuk menulis kode membuat perpustakaan ini bukan pilihan terbaik untuk API Anda berikutnya.
Bagaimanapun, saya memutuskan untuk menggunakannya dalam proyek dan mengalami sejumlah masalah, untungnya, setelah memecahkan sebagian besar dari mereka (berkat fitur graphene yang tidak terdokumentasi). Beberapa solusi saya murni arsitektur dan dapat digunakan di luar kotak, tanpa kerangka kerja saya. Namun, sisanya masih memerlukan beberapa basis kode.
Artikel ini bukan dokumentasi, tetapi dalam arti deskripsi singkat tentang jalan yang saya tempuh dan masalah yang saya selesaikan dengan satu atau lain cara dengan alasan singkat untuk pilihan saya. Pada bagian ini, saya memperhatikan mutasi dan hal-hal yang berkaitan dengannya.
Tujuan artikel ini adalah untuk mendapatkan umpan balik yang berarti , jadi saya akan menunggu kritik di komentar!
Catatan: sebelum melanjutkan membaca artikel, saya sangat menyarankan Anda membiasakan diri dengan apa itu GraphQL.
Mutasi
Sebagian besar diskusi tentang GraphQL fokus pada mendapatkan data, tetapi platform apa pun yang menghargai diri sendiri juga membutuhkan cara untuk memodifikasi data yang disimpan di server.
Mari kita mulai dengan mutasi.
Pertimbangkan kode berikut:
class UpdatePostMutation(graphene.Mutation): class Arguments: post_id = graphene.ID(required=True) title = graphene.String(required=True) content = graphene.String(required=True) image_urls = graphene.List(graphene.String, required=False) allow_comments = graphene.Boolean(required=True) contact_email = graphene.String(required=True) ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) def mutate(_, info, post_id, title, content, image_urls, allow_comments, contact_email): errors = [] try: post = get_post_by_id(post_id) except PostNotFound: return UpdatePostMutation(ok=False, errors=['post_not_found']) if not info.context.user.is_authenticated: errors.append('not_authenticated') if len(title) < TITLE_MIN_LENGTH: errors.append('title_too_short') if not is_email(contact_email): errors.append('contact_email_not_valid') if post.owner != info.context.user: errors.append('not_post_owner') if Post.objects.filter(title=title).exists(): errors.append('title_already_taken') if not errors: post = Utils.update_post(post, title, content, image_urls, allow_comments, contact_email) return UpdatePostMutation(ok=bool(errors), errors=errors)
UpdatePostMutation
mengubah pos dengan id
diberikan, menggunakan data yang ditransfer dan mengembalikan kesalahan jika beberapa kondisi tidak terpenuhi.
Satu hanya perlu melihat kode ini, karena menjadi non-ekstensibilitas dan tidak didukung karena:
- Terlalu banyak argumen dari fungsi yang
mutate
, yang jumlahnya mungkin bertambah bahkan jika kita ingin menambahkan lebih banyak bidang yang akan diedit. - Agar mutasi terlihat sama di sisi klien, mereka harus mengembalikan
errors
dan ok
, sehingga status mereka dan apa yang selalu mungkin untuk dipahami. - Cari dan ambil objek dalam fungsi
mutate
. Fungsi mutasi beroperasi dengan puasa, dan jika tidak ada, maka mutasi tidak boleh terjadi. - Memeriksa izin dalam mutasi. Mutasi tidak boleh terjadi jika pengguna tidak memiliki hak untuk melakukan ini (edit beberapa posting).
- Argumen pertama yang tidak berguna ( root yang selalu
None
untuk bidang tingkat atas, yang merupakan mutasi kami). - Serangkaian kesalahan yang tidak dapat diprediksi: jika Anda tidak memiliki kode sumber atau dokumentasi, maka Anda tidak akan tahu kesalahan apa yang dapat dikembalikan mutasi ini, karena mereka tidak tercermin dalam skema.
- Ada terlalu banyak pengecekan kesalahan templat yang dilakukan secara langsung dalam metode
mutate
, yang melibatkan perubahan data, dan bukannya berbagai pengecekan. mutate
ideal harus terdiri dari satu baris - panggilan ke fungsi pengeditan posting.
Singkatnya, mutate
harus memodifikasi data , daripada menangani tugas pihak ketiga seperti mengakses objek dan memvalidasi input. Tujuan kami adalah mencapai sesuatu seperti:
def mutate(post, info, input): post = Utils.update_post(post, **input) return UpdatePostMutation(post=post)
Sekarang mari kita lihat poin-poin di atas.
Jenis khusus
Bidang email
dilewatkan sebagai string, sementara itu adalah string format tertentu . Setiap kali API menerima email, itu harus memeriksa kebenarannya. Jadi solusi terbaik adalah membuat tipe kustom.
class Email(graphene.String):
Ini mungkin tampak jelas, tetapi perlu disebutkan.
Jenis input
Gunakan tipe input untuk mutasi Anda. Bahkan jika mereka tidak dapat digunakan kembali di tempat lain. Berkat tipe input, kueri menjadi lebih kecil, yang membuatnya lebih mudah dibaca dan lebih cepat untuk menulis.
class UpdatePostInput(graphene.InputObjectType): title = graphene.String(required=True) content = graphene.String(required=True) image_urls = graphene.List(graphene.String, required=False) allow_comments = graphene.Boolean(required=True) contact_email = graphene.String(required=True)
Kepada:
mutation( $post_id: ID!, $title: String!, $content: String!, $image_urls: String!, $allow_comments: Boolean!, $contact_email: Email! ) { updatePost( post_id: $post_id, title: $title, content: $content, image_urls: $image_urls, allow_comments: $allow_comments, contact_email: $contact_email, ) { ok } }
Setelah:
mutation($id: ID!, $input: UpdatePostInput!) { updatePost(id: $id, input: $input) { ok } }
Kode mutasi berubah menjadi:
class UpdatePostMutation(graphene.Mutation): class Arguments: input = UpdatePostInput(required=True) id = graphene.ID(required=True) ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) def mutate(_, info, input, id):
Kelas mutasi dasar
Seperti disebutkan dalam paragraf 2, mutasi harus mengembalikan errors
dan ok
sehingga status mereka dan apa penyebabnya selalu dapat dipahami . Cukup sederhana, kami membuat kelas dasar:
class MutationPayload(graphene.ObjectType): ok = graphene.Boolean(required=True) errors = graphene.List(graphene.String, required=True) query = graphene.Field('main.schema.Query', required=True) def resolve_ok(self, info): return len(self.errors or []) == 0 def resolve_errors(self, info): return self.errors or [] def resolve_query(self, info): return {}
Beberapa catatan:
Ini sangat nyaman ketika klien memperbarui beberapa data setelah mutasi selesai dan tidak ingin meminta pendukung untuk mengembalikan seluruh rangkaian ini. Semakin sedikit kode yang Anda tulis, semakin mudah untuk mempertahankannya. Saya mengambil ide ini dari sini .
Dengan kelas mutasi dasar, kode berubah menjadi:
class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput(required=True) id = graphene.ID(required=True) def mutate(_, info, input, id):
Mutasi akar
Permintaan mutasi kami sekarang terlihat seperti ini:
mutation($id: ID!, $input: PostUpdateInput!) { updatePost(id: $id, input: $input) { ok } }
Mengandung semua mutasi dalam lingkup global bukanlah praktik yang baik. Berikut beberapa alasannya:
- Dengan semakin banyaknya mutasi, semakin sulit menemukan mutasi yang Anda butuhkan.
- Karena satu namespace, perlu untuk memasukkan "nama modulnya" dalam nama mutasi, misalnya
update
Post
. - Anda harus memberikan
id
sebagai argumen untuk mutasi.
Saya sarankan menggunakan mutasi root . Tujuan mereka adalah untuk memecahkan masalah-masalah ini dengan memisahkan mutasi ke dalam lingkup yang terpisah dan membebaskan mutasi dari logika akses ke objek dan hak akses kepada mereka.
Permintaan baru terlihat seperti ini:
mutation($id: ID!, $input: PostUpdateInput!) { post(id: $id) { update(input: $input) { ok } } }
Argumen permintaan tetap sama. Sekarang fungsi perubahan "dipanggil" di dalam post
, yang memungkinkan logika berikut untuk diimplementasikan:
- Jika
id
tidak diteruskan ke post
, maka ia mengembalikan {}
. Ini memungkinkan Anda untuk terus melakukan mutasi di dalam. Digunakan untuk mutasi yang tidak memerlukan elemen root (misalnya, untuk membuat objek). - Jika
id
dilewatkan, elemen yang sesuai diambil. - Jika objek tidak ditemukan,
None
dikembalikan, dan ini melengkapi permintaan, mutasi tidak dipanggil. - Jika objek ditemukan, maka periksa hak pengguna untuk memanipulasinya.
- Jika pengguna tidak memiliki hak,
None
dikembalikan dan permintaan selesai, mutasi tidak dipanggil. - Jika pengguna memiliki hak, maka objek yang ditemukan dikembalikan dan mutasi menerimanya sebagai root - argumen pertama.
Dengan demikian, kode mutasi berubah menjadi:
class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput() def mutate(post, info, input): if post is None: return None errors = [] if not info.context.user.is_authenticated: errors.append('not_authenticated') if len(title) < TITLE_MIN_LENGTH: errors.append('title_too_short') if Post.objects.filter(title=title).exists(): errors.append('title_already_taken') if not errors: post = Utils.update_post(post, **input.__dict__) return UpdatePostMutation(errors=errors)
- Akar dari mutasi - argumen pertama - sekarang menjadi objek tipe
Post
, di mana mutasi dilakukan. - Pemeriksaan otorisasi dipindahkan ke root mutation code.
Kode Mutasi Root:
class PostMutationRoot(MutationRoot): class Meta: model = Post has_permission = lambda post, user: post.owner == user update = UpdatePostMutation.Field()
Antarmuka galat
Untuk membuat satu set kesalahan dapat diprediksi, mereka harus tercermin dalam desain.
- Karena mutasi dapat mengembalikan beberapa kesalahan, kesalahan harus menjadi daftar.
- Karena kesalahan diwakili oleh tipe yang berbeda,
Union
kesalahan tertentu harus ada untuk mutasi tertentu. - Agar kesalahan tetap serupa satu sama lain, mereka harus mengimplementasikan antarmuka, sebut saja itu
ErrorInterface
. Biarkan berisi dua bidang: ok
dan message
.
Jadi, kesalahan harus bertipe [SomeMutationErrorsUnion]!
. Semua subtipe SomeMutationErrorsUnion
harus menerapkan ErrorInterface
.
Kami mendapatkan:
class NotAuthenticated(graphene.ObjectType): message = graphene.String(required=True, default_value='not_authenticated') class Meta: interfaces = [ErrorInterface, ] class TitleTooShort(graphene.ObjectType): message = graphene.String(required=True, default_value='title_too_short') class Meta: interfaces = [ErrorInterface, ] class TitleAlreadyTaken(graphene.ObjectType): message = graphene.String(required=True, default_value='title_already_taken') class Meta: interfaces = [ErrorInterface, ] class UpdatePostMutationErrors(graphene.Union): class Meta: types = [NotAuthenticated, TitleIsTooShort, TitleAlreadyTaken, ]
Kelihatannya bagus, tetapi ada terlalu banyak kode. Kami menggunakan metaclass untuk menghasilkan kesalahan ini dengan cepat:
class PostErrors(metaclass=ErrorMetaclass): errors = [ 'not_authenticated', 'title_too_short', 'title_already_taken', ] class UpdatePostMutationErrors(graphene.Union): class Meta: types = [PostErrors.not_authenticated, PostErrors.title_too_short, PostErrors.title_already_taken, ]
Tambahkan deklarasi kesalahan yang dikembalikan ke mutasi:
class UpdatePostMutation(MutationPayload, graphene.Mutation): class Arguments: input = UpdatePostInput() errors = graphene.List(UpdatePostMutationErrors, required=True) def mutate(post, info, input):
Periksa kesalahan
Sepertinya saya bahwa metode mutate
tidak boleh peduli tentang apa pun selain bermutasi data . Untuk mencapai ini, Anda perlu memeriksa kesalahan dalam kode mereka untuk fungsi ini.
Menghilangkan implementasi, berikut hasilnya:
class UpdatePostMutation(DefaultMutation): class Arguments: input = UpdatePostInput() class Meta: root_required = True authentication_required = True
Sebelum memulai fungsi mutate
, setiap pemeriksa (elemen kedua dari anggota array checks
) dipanggil. Jika True
dikembalikan, kesalahan yang sesuai ditemukan. Jika tidak ada kesalahan ditemukan, fungsi mutate
dipanggil.
Saya akan menjelaskan:
- Periksa fungsi, ambil argumen yang sama dengan fungsi yang
mutate
. - Fungsi validasi harus mengembalikan
True
jika ditemukan kesalahan. - Pemeriksaan otorisasi dan keberadaan elemen root cukup umum dan terdaftar di bendera
Meta
. authentication_required
menambahkan pemeriksaan otorisasi jika True
.root_required
menambahkan root_required
" root is not None
".UpdatePostMutationErrors
tidak lagi diperlukan. Gabungan kemungkinan kesalahan dibuat dengan cepat tergantung pada kelas kesalahan array checks
.
Generik
DefaultMutation
digunakan di bagian terakhir menambahkan metode pre_mutate
, yang memungkinkan Anda untuk mengubah argumen input sebelum memeriksa kesalahan, dan, dengan demikian, memanggil mutasi.
Ada juga kit starter generik yang membuat kode lebih pendek dan membuat hidup lebih mudah.
Catatan: saat ini kode generik khusus untuk django ORM
Penciptaan
Membutuhkan salah satu create_function
model
atau create_function
. Secara default, create_function
terlihat seperti ini:
model._default_manager.create(**data, owner=user)
Ini mungkin terlihat tidak aman, tetapi jangan lupa bahwa ada tipe bawaan memeriksa graphql, serta memeriksa mutasi.
Ini juga menyediakan metode post_mutate
yang dipanggil setelah create_function
dengan argumen (instance_created, user)
, yang hasilnya akan dikembalikan ke klien.
Pembaruan pembaruan
Memungkinkan Anda untuk mengatur update_function
. Secara default:
def default_update_function(instance, user=None, **data): instance.__dict__.update(data) instance.save() return instance
root_required
True
secara default.
Ini juga menyediakan metode post_mutate
, yang disebut after update_function
dengan argumen (instance_updated, user)
, yang hasilnya akan dikembalikan ke klien.
Dan inilah yang kami butuhkan!
Kode terakhir:
class UpdatePostMutation(UpdateMutation): class Arguments: input = UpdatePostInput() class Meta: checks = [ ( PostErrors.title_too_short, lambda post, input: len(input.title) < TITLE_MIN_LENGTH ), ( PostErrors.title_already_taken, lambda post, input: Post.objects.filter(title=input.title).exists() ), ]
DeleteMutation
Memungkinkan Anda menyetel delete_function
. Secara default:
def default_delete_function(instance, user=None, **data): instance.delete()
Kesimpulan
Artikel ini hanya mempertimbangkan satu aspek, meskipun menurut saya itu yang paling rumit. Saya memiliki beberapa pemikiran tentang resolvers dan jenis, serta hal-hal umum dalam graphene-python.
Sulit bagi saya untuk menyebut diri saya seorang pengembang yang berpengalaman, jadi saya akan sangat senang atas umpan balik, serta saran.
Kode sumber dapat ditemukan di sini .