بناء واجهات REST مع Spring Boot

تعيين الطلبات

18 دقيقة الدرس 2 من 13

تعيين الطلبات

في الدرس السابق تعلّمت أن @RestController يُعلّم فئةً لتكون نقطة دخول حركة HTTP. لكن المتحكم وحده لا يفعل شيئًا — تحتاج إلى إخبار Spring بأيّ طريقة HTTP وأيّ مسار URL يجب أن يُشغّل كل دالة معالج. هذا هو ما تفعله تعليمات تعيين الطلبات، واختيارها بصورة صحيحة هو أحد أهم قرارات التصميم التي ستتخذها عند بناء واجهة برمجية REST.

أفعال HTTP الأربعة الرئيسية

تُعيّن REST عمليات CRUD على أربع طرق HTTP، لكل منها عقد دلالي مميز:

  • GET — استرداد مورد؛ يجب أن يكون آمنًا (بلا آثار جانبية) وإيدمبوتنتًا.
  • POST — إنشاء مورد جديد أو تشغيل إجراء؛ ليس إيدمبوتنتًا.
  • PUT — استبدال مورد بالكامل؛ إيدمبوتنت (استدعاؤه مرتين يُعطي النتيجة ذاتها).
  • DELETE — حذف مورد؛ إيدمبوتنت.

يوفر Spring تعليمًا اختصاريًا مخصصًا لكل فعل. التعليم العام القديم @RequestMapping لا يزال يعمل وهو مفيد حين تحتاج إلى مشاركة مسار أساسي بين جميع الدوال في متحكم، لكن للمعالجات الفردية تكون الاختصارات أنظف وأوضح.

@GetMapping

استخدم @GetMapping لأي عملية تقرأ البيانات دون تعديل حالة الخادم. يمكن تخزين معالج GET المصمم جيدًا في ذاكرة التخزين المؤقت وإعادة المحاولة بأمان واستدعاؤه مرارًا دون قلق من آثار جانبية مكررة.

import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/products") // المسار الأساسي المشترك لجميع دوال هذا المتحكم public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService = productService; } // GET /api/products — قائمة بجميع المنتجات @GetMapping public List<Product> listAll() { return productService.findAll(); } // GET /api/products/{id} — جلب منتج واحد @GetMapping("/{id}") public Product getOne(@PathVariable Long id) { return productService.findById(id); } }
@RequestMapping على مستوى الفئة هو بادئة. حين تضع @RequestMapping("/api/products") على الفئة، تُضاف كل تعليمة على مستوى الدالة إليها. فـ@GetMapping("/{id}") يُحلَّل إلى GET /api/products/{id}. هذا يُجنّبك تكرار المسار الأساسي في كل دالة.

@PostMapping

POST هو الفعل الصحيح حين تطلب من الخادم إنشاء شيء جديد. النمط المعياري هو: يُرسل العميل جسم JSON ببيانات المورد، يُنشئ الخادم المورد، ويحتوي الرد على المورد المُنشأ حديثًا (أو على أقل تقدير معرّفه وحالة 201 Created).

// POST /api/products @PostMapping @ResponseStatus(HttpStatus.CREATED) // إرجاع 201 بدلًا من الافتراضي 200 public Product create(@RequestBody ProductRequest request) { return productService.create(request); }

يُخبر التعليم @RequestBody Spring بإلغاء تسلسل JSON الوارد إلى كائن Java باستخدام Jackson. ستتعمق في Jackson وResponseEntity في دروس لاحقة؛ لكن تذكّر الآن أن POST يجب أن يُرجع 201 Created، لا 200 OK.

@PutMapping

يستبدل PUT المورد المستهدف بالكامل. إذا أرسل العميل كائنًا جزئيًا، فعلى الخادم معالجة الحقول المفقودة باعتبارها قيم فارغة متعمدة (أو الحالة الخالية للمورد). إذا أردت التحديثات الجزئية، استخدم PATCH — لكن هذا يُغطى في درس أفضل الممارسات.

// PUT /api/products/{id} @PutMapping("/{id}") public Product replace(@PathVariable Long id, @RequestBody ProductRequest request) { return productService.replace(id, request); }
PUT مقابل POST للإنشاء. تسمح بعض الواجهات البرمجية لـ PUT /resources/{id} بإنشاء مورد إن لم يكن موجودًا (upsert). هذا صحيح في REST لكنه يتطلب أن يُوفّر العميل المعرّف. وهو مناسب حين تكون المعرّفات مفاتيح طبيعية (مثل اسم المستخدم). حين تكون المعرّفات من توليد الخادم (مثل المفاتيح التلقائية في قاعدة البيانات)، POST-للإنشاء هو النمط المعياري.

@DeleteMapping

يحذف DELETE المورد المُحدَّد. يُرجع الحذف الناجح تقليديًا 204 No Content — نجحت العملية لكن لا يوجد جسم للإرجاع.

// DELETE /api/products/{id} @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) // 204 public void delete(@PathVariable Long id) { productService.delete(id); }

تصميم المسارات — قواعد عملية

بنية URL التي تختارها هي جزء من العقد العام للواجهة البرمجية. تغييرها لاحقًا هو تغيير جذري لكل عميل. هذه القواعد توفّر عليك الكثير من المتاعب:

  1. استخدم الأسماء لا الأفعال. طريقة HTTP هي الفعل. اكتب /api/products، لا /api/getProducts أو /api/createProduct.
  2. استخدم الأسماء الجمع للمجموعات. /api/products للمجموعة، و/api/products/{id} لعنصر واحد. خلط المفرد والجمع يُربك المستدعين.
  3. عبّر عن العلاقات في المسار. إذا كانت المراجعة تنتمي إلى منتج، فالمسار /api/products/{productId}/reviews يُوضّح الملكية.
  4. اجعل المسارات بحروف صغيرة مع شرطات. /api/product-categories، لا /api/productCategories أو /api/ProductCategories. عناوين URL حساسة لحالة الأحرف على معظم الخوادم، وcamelCase يبدو غريبًا فيها.
  5. لا تُصدر نسخة في المسار الآن. الإصدار يُغطى في الدرس التاسع؛ إضافة /v1/ في اليوم الأول بدون استراتيجية يُولّد فوضى.
// جيد — موجّه نحو الموارد، أسماء جمع، حروف صغيرة @RestController @RequestMapping("/api/orders") public class OrderController { @GetMapping // GET /api/orders public List<Order> list() { ... } @GetMapping("/{orderId}") // GET /api/orders/{orderId} public Order get(@PathVariable Long orderId) { ... } @GetMapping("/{orderId}/items") // GET /api/orders/{orderId}/items — مورد متداخل public List<OrderItem> items(@PathVariable Long orderId) { ... } @PostMapping // POST /api/orders @ResponseStatus(HttpStatus.CREATED) public Order place(@RequestBody OrderRequest req) { ... } @PutMapping("/{orderId}") // PUT /api/orders/{orderId} public Order update(@PathVariable Long orderId, @RequestBody OrderRequest req) { ... } @DeleteMapping("/{orderId}") // DELETE /api/orders/{orderId} @ResponseStatus(HttpStatus.NO_CONTENT) public void cancel(@PathVariable Long orderId) { ... } }

تحت الغطاء: كيف تُعيَّن التعليمات إلى @RequestMapping

التعليمات الاختصارية هي أغلفة رفيعة. فمثلًا @GetMapping("/path") مكافئة تمامًا لـ @RequestMapping(value = "/path", method = RequestMethod.GET). معرفة هذا يُهمّك حين تحتاج إلى ضبط سمات إضافية مثل produces (التفاوض على المحتوى) أو consumes — يمكنك استخدام الاختصار وإضافة تلك السمات مباشرةً:

@GetMapping(value = "/{id}", produces = "application/json") public Product getOne(@PathVariable Long id) { ... } @PostMapping(value = "", consumes = "application/json", produces = "application/json") @ResponseStatus(HttpStatus.CREATED) public Product create(@RequestBody ProductRequest req) { ... }
التعيينات الغامضة تُوقف التشغيل. إذا حُلَّت دالتا معالج في التطبيق ذاته إلى مجموعة طريقة HTTP + مسار مطابقة، يرمي Spring استثناء IllegalStateException: Ambiguous mapping عند بدء التشغيل. هذا أمر جيد — فهو انتهاك عقد في وقت البدء لا خطأ صامت في وقت التشغيل. صحّحه بتمييز المسارات أو الطرق أو قيود consumes/produces.

الخلاصة

تربط @GetMapping و@PostMapping و@PutMapping و@DeleteMapping دوالَّ معالجيك بأفعال HTTP ومسارات محددة. اجمعها مع @RequestMapping على مستوى الفئة كبادئة للإبقاء على متحكمك نظيفًا. صمّم مساراتك حول أسماء الموارد، استخدم أسماء الجمع للمجموعات، وعبّر عن الملكية في المسارات الفرعية المتداخلة. في الدرس التالي ستتعلم كيفية استخراج أجزاء ديناميكية من تلك المسارات باستخدام @PathVariable و@RequestParam.