GraphQL مقابل REST في Go: مبارزة العمالقة، أو كيف لا تخطئ في الاختيار
\n\nإذا كنت تكتب بلغة Go لمدة ستة أشهر على الأقل، فمن المحتمل أنك واجهت النقاش الأبدي: REST أم GraphQL؟ الأول هو "حصان العمل" القديم الجيد، والثاني هو "سيارة رياضية" عصرية مليئة بالخيارات. لكن في عالم Go، حيث الأداء والبساطة هما الدين، ليس الاختيار واضحًا دائمًا.
\n\nفي هذه المقالة، لن نقوم فقط بسرد الإيجابيات والسلبيات. سنكتب كودًا حقيقيًا بلغة Go، ونقيس وقت التنفيذ، ونحلل متى يكون GraphQL طلقة في القدم، ومتى يكون ممرًا غشًا إلى عالم واجهات API الفعالة. هيا بنا!
\n\nREST في Go: الاستقرار والقدرة على التنبؤ
\n\nREST يشبه المطرقة القديمة المختبرة. إنها لا تلمع، لكنها تدق المسامير بشكل مثالي. في Go، تُكتب واجهات REST API باستخدام net/http أو أطر عمل خفيفة مثل gin و chi. دعنا نلقي نظرة على مثال بسيط:
package main\n\nimport (\n "encoding/json"\n "net/http"\n)\n\ntype User struct {\n ID int `json:"id"`\n Name string `json:"name"`\n}\n\nfunc getUserHandler(w http.ResponseWriter, r *http.Request) {\n user := User{ID: 1, Name: "أليكسي"}\n w.Header().Set("Content-Type", "application/json")\n json.NewEncoder(w).Encode(user)\n}\n\nfunc main() {\n http.HandleFunc("/api/user", getUserHandler)\n http.ListenAndServe(":8080", nil)\n}\n\nجميل؟ نعم. بسيط؟ جدًا. لكن ماذا لو كان الواجهة الأمامية تحتاج فقط إلى اسم المستخدم، ونحن نسحب الكائن بأكمله؟ في REST، يُسمى هذا over-fetching - زيادة في البيانات. وفي Go، حيث اعتدنا التحكم في كل بايت، قد يكون هذا مزعجًا.
\n\nمشكلة N+1 في REST
\n\nلنفترض أن لدينا قائمة بالطلبات، ولكل طلب نحتاج اسم المستخدم. في REST، إما أن نقوم بعمل N من الطلبات (بطيء)، أو ننشئ نقطة نهاية منفصلة /api/orders-with-users (الكثير من نقاط النهاية). يتعامل Go مع هذا من خلال استعلامات join في قاعدة البيانات، ولكن على مستوى API، لا يزال يبدو غير أنيق.
GraphQL في Go: القوة والمرونة
\n\nGraphQL يشبه السكين السويسري: نقطة نهاية واحدة، والعميل هو من يقرر البيانات التي يحتاجها. في Go، يوجد إطار عمل gqlgen (من 99designs) - يقوم بتوليد الكود من المخطط، وهذا يتماشى جدًا مع روح Go (كود أقل، عمل أكثر).
// schema.graphql\ntype Query {\n user(id: Int!): User\n}\n\ntype User {\n id: Int!\n name: String!\n orders: [Order!]!\n}\n\ntype Order {\n id: Int!\n total: Float!\n}\n\nالآن التنفيذ بلغة Go:
\n\npackage graph\n\nimport (\n "context"\n "fmt"\n)\n\ntype Resolver struct{}\n\nfunc (r *Resolver) User(ctx context.Context, args struct{ ID int }) (*User, error) {\n // في الواقع - استعلام لقاعدة البيانات\n return &User{\n ID: args.ID,\n Name: "ماريا",\n Orders: []*Order{\n {ID: 1, Total: 250.50},\n },\n }, nil\n}\n\nالعميل يطلب فقط name - يحصل فقط على name. لا يوجد over-fetching. لكن هناك فارق بسيط: إذا طلب العميل orders لـ 10 مستخدمين، ولكل منهم 50 طلبًا - قد تنهار قاعدة البيانات. يُسمى هذا مشكلة N+1 في GraphQL، وفي Go يتم حلها باستخدام DataLoaders.
DataLoader - المنقذ من N+1
\n\nimport "github.com/graph-gophers/dataloader"\n\ntype UserLoader struct {\n *dataloader.Loader\n}\n\nfunc NewUserLoader() *UserLoader {\n return &UserLoader{\n Loader: dataloader.NewBatchedLoader(func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {\n // تجميع الاستعلامات في قاعدة البيانات\n userIDs := make([]int, len(keys))\n for i, key := range keys {\n userIDs[i] = key.Int()\n }\n users := batchFetchUsers(userIDs) // استعلام SQL واحد مع WHERE IN\n results := make([]*dataloader.Result, len(ke