تنفيذ عمليات الربط باستخدام الاستعلامات الفرعية

الخلفية

عمليات Pipeline هي واجهة طلب بحث جديدة لـ Cloud Firestore. توفّر هذه الواجهة وظائف طلب بحث متقدّمة تتضمّن عبارات معقّدة. يتيح إصدار Firestore Enterprise عمليات الربط بأسلوب علائقي من خلال الاستعلامات الفرعية المرتبطة. بخلاف العديد من قواعد بيانات NoSQL التي تتطلّب غالبًا إلغاء تسوية البيانات أو تنفيذ طلبات متعددة من جهة العميل، تتيح لك الاستعلامات الفرعية دمج البيانات وتجميعها من مجموعات أو مجموعات فرعية ذات صلة مباشرةً على الخادم.

الاستعلامات الفرعية هي عبارات تنفّذ مسارًا متداخلاً لكل مستند تتم معالجته بواسطة الاستعلام الخارجي. يتيح ذلك أنماطًا معقّدة لاسترداد البيانات، مثل جلب مستند مع عناصر المجموعة الفرعية ذات الصلة أو ربط البيانات المرتبطة منطقيًا من مجموعات جذرية مختلفة.

المفاهيم

يعرض هذا القسم المفاهيم الأساسية المتعلقة باستخدام طلبات البحث الفرعية لتنفيذ عمليات الربط في عمليات Pipeline.

الاستعلامات الفرعية كتعبيرات

الاستعلام الفرعي ليس مرحلة من المستوى الأعلى، بل هو تعبير يمكن استخدامه في أي مرحلة تقبل التعبيرات، مثل select(...) أو add_fields(...) أو where(...) أو sort(...).

تتيح Cloud Firestore ثلاثة أنواع من الاستعلامات الفرعية:

  • الطلبات الفرعية للمصفوفات: يتم إنشاء مجموعة النتائج الكاملة للطلب الفرعي كمصفوفة من المستندات.
  • الطلبات الفرعية العددية: يتم تقييمها إلى قيمة واحدة، مثل عدد أو متوسط أو حقل معيّن من مستند ذي صلة.
  • subcollection(...) طلبات البحث الفرعية: عمليات ربط مبسطة لعلاقة العنصر الرئيسي بالعناصر التابعة من نوع واحد إلى متعدد.

النطاق والمتغيرات

عند كتابة عملية ربط، يجب غالبًا أن يشير الاستعلام الفرعي المتداخل إلى حقول من المستند "الخارجي" (العنصر الرئيسي). لربط هذه النطاقات، يمكنك استخدام مرحلة let(...) (يُشار إليها باسم define(...) في بعض حِزم تطوير البرامج (SDK)) لتحديد المتغيرات في النطاق الرئيسي التي يمكن الرجوع إليها بعد ذلك في الاستعلام الفرعي باستخدام الدالة variable(...).

البنية

تقدّم الأقسام التالية نظرة عامة على صيغة تنفيذ عمليات الربط.

المرحلة let(...)

المرحلة let(...) (المشار إليها باسم define(...) في بعض حِزم SDK) هي مرحلة لا يتم فيها الفلترة، وتعمل على جلب البيانات بشكل صريح من النطاق الرئيسي إلى متغيّر مسمّى لاستخدامه في النطاقات المتداخلة اللاحقة.

Web

    async function defineStageData() {
      await setDoc(doc(collection(db, "Authors"), "author_123"), {
        "id": "author_123",
        "name": "Jane Austen"
      });
    }
    
Swift
      func defineStageData() async throws {
      try await db.collection("authors").document("author_123").setData([
        "id": "author_123",
        "name": "Jane Austen"
      ])
    }
    

Kotlin

    fun defineStageData() {
        val author = hashMapOf(
            "id" to "author_123",
            "name" to "Jane Austen",
        )

        db.collection("Authors").document("author_123").set(author)
    }
  

Java

    public void defineStageData() {
        Map<String, Object> author = new HashMap<>();
        author.put("id", "author_123");
        author.put("name", "Jane Austen");

        db.collection("Authors").document("author_123").set(author);
    }
  

الاستعلامات الفرعية للمصفوفات

استعلام فرعي للمصفوفة هو حالة خاصة من استعلام فرعي للتعبير، ويحوّل مجموعة النتائج الكاملة للاستعلام الفرعي إلى مصفوفة. إذا عرضت الاستعلام الفرعي صفر صفوف، سيتم تقييمه إلى مصفوفة فارغة. ولا تعرض أبدًا مصفوفة null. تكون هذه الاستعلامات مفيدة عندما تكون النتائج الكاملة مطلوبة في النتيجة النهائية، مثلما يحدث عند إنشاء مجموعة متداخلة أو مرتبطة.

يمكن لطلبات البحث فلترة البيانات وترتيبها وتجميعها في طلب البحث الفرعي للحدّ أيضًا من كمية البيانات التي يجب جلبها وعرضها، ما يساعد في تقليل تكلفة طلب البحث. يتم احترام ترتيب الاستعلام الفرعي، ما يعني أنّ مرحلة sort(...) في الاستعلام الفرعي تتحكّم في ترتيب النتائج في المصفوفة النهائية.

استخدِم برنامج تضمين حزمة تطوير البرامج (SDK) toArrayExpression() لتحويل طلب بحث إلى مصفوفة.

Web

    async function toArrayExpressionStageData() {
      await setDoc(doc(collection(db, "Projects"), "project_1"), {
        "id": "project_1",
        "name": "Alpha Build"
      });
      await addDoc(collection(db, "Tasks"), {
        "project_id": "project_1",
        "title": "System Architecture"
      });
      await addDoc(collection(db, "Tasks"), {
        "project_id": "project_1",
        "title": "Database Schema Design"
      });
    }
  

الردّ

    {
        id: "project_1",
        name: "Alpha Build",
        taskTitles: [
          "System Architecture", "Database Schema Design"
      ]
    }
    
Swift
    async function toArrayExpressionStageData() {
      await setDoc(doc(collection(db, "Projects"), "project_1"), {
        "id": "project_1",
        "name": "Alpha Build"
      });
      await addDoc(collection(db, "Tasks"), {
        "project_id": "project_1",
        "title": "System Architecture"
      });
      await addDoc(collection(db, "Tasks"), {
        "project_id": "project_1",
        "title": "Database Schema Design"
      });
    }
    

الردّ

    {
      id: "project_1",
      name: "Alpha Build",
      taskTitles: [
        "System Architecture", "Database Schema Design"
      ]
    }
    

Kotlin

    fun toArrayExpressionData() {
        val project = hashMapOf(
            "id" to "project_1",
            "name" to "Alpha Build",
        )
        db.collection("Projects").document("project_1").set(project)

        val task1 = hashMapOf(
            "project_id" to "project_1",
            "title" to "System Architecture",
        )
        db.collection("Tasks").add(task1)

        val task2 = hashMapOf(
            "project_id" to "project_1",
            "title" to "Database Schema Design",
        )
        db.collection("Tasks").add(task2)
    }
  

الردّ

    {
      id: "project_1",
      name: "Alpha Build",
      taskTitles: [
        "System Architecture", "Database Schema Design"
      ]
    }
    

Java

      public void toArrayExpressionData() {
        Map<String, Object> project = new HashMap<>();
        project.put("id", "project_1");
        project.put("name", "Alpha Build");
        db.collection("Projects").document("project_1").set(project);

        Map<String, Object> task1 = new HashMap<>();
        task1.put("project_id", "project_1");
        task1.put("title", "System Architecture");
        db.collection("Tasks").add(task1);

        Map<String, Object> task2 = new HashMap<>();
        task2.put("project_id", "project_1");
        task2.put("title", "Database Schema Design");
        db.collection("Tasks").add(task2);
    }
  

الردّ

    {
        id: "project_1",
        name: "Alpha Build",
        taskTitles: [
          "System Architecture", "Database Schema Design"
      ]
    }
    

الاستعلامات الفرعية العددية

يتم غالبًا استخدام الاستعلامات الفرعية العددية في مرحلة select(...) أو where(...) للسماح بالفلترة أو عرض نتيجة استعلام فرعي بدون تنفيذ الاستعلام الكامل مباشرةً.

سيتم تقييم طلب البحث الفرعي العددي الذي يعرض صفر نتائج إلى null نفسه، بينما سيؤدي طلب البحث الفرعي الذي يتم تقييمه إلى عناصر متعددة إلى حدوث خطأ في وقت التشغيل.

عندما ينتج طلب فرعي عددي حقلاً واحدًا فقط لكل نتيجة، يتم رفع الحقل ليكون النتيجة ذات المستوى الأعلى للطلب الفرعي. يحدث ذلك عادةً عندما ينتهي الاستعلام الفرعي بـ select(field("user_name")) أو aggregate(countAll().as("total")) حيث يكون مخطط الاستعلام الفرعي عبارة عن حقل واحد فقط. بخلاف ذلك، عندما يمكن أن ينتج طلب البحث الفرعي حقولاً متعددة، يتم تضمينها في خريطة.

استخدِم برنامج تضمين حزمة تطوير البرامج (SDK) toScalarExpression() لتحويل طلب بحث إلى تعبير عددي.

Web

            async function toScalarExpressionStageData() {
              await setDoc(doc(collection(db, "Authors"), "author_202"), {
                "id": "author_202",
                "name": "Charles Dickens"
              });
              await addDoc(collection(db, "Books"), {
                "author_id": "author_202",
                "title": "Great Expectations",
                "rating": 4.8
              });
              await addDoc(collection(db, "Books"), {
                "author_id": "author_202",
                "title": "Oliver Twist",
                "rating": 4.5
              });
            }
        

الردّ

        {
            "id": "author_202",
            "name": "Charles Dickens",
            "averageBookRating": 4.65
        }
      
Swift
        try await db.collection("authors").document("author_202").setData([
          "id": "author_202",
          "name": "Charles Dickens"
        ])
        try await db.collection("books").document().setData([
          "author_id": "author_202",
          "title": "Great Expectations",
          "rating": 4.8
        ])
        try await db.collection("books").document().setData([
          "author_id": "author_202",
          "title": "Oliver Twist",
          "rating": 4.5
        ])
        

الردّ

        {
            "id": "author_202",
            "name": "Charles Dickens",
            "averageBookRating": 4.65
        }
      

Kotlin

        fun toScalarExpressionData() {
        val author = hashMapOf(
            "id" to "author_202",
            "name" to "Charles Dickens",
        )
        db.collection("Authors").document("author_202").set(author)

        val book1 = hashMapOf(
            "author_id" to "author_202",
            "title" to "Great Expectations",
            "rating" to 4.8,
        )
        db.collection("Books").add(book1)

        val book2 = hashMapOf(
            "author_id" to "author_202",
            "title" to "Oliver Twist",
            "rating" to 4.5,
        )
        db.collection("Books").add(book2)
    }
  

الردّ

        {
            "id": "author_202",
            "name": "Charles Dickens",
            "averageBookRating": 4.65
        }
      

Java

       public void toScalarExpressionData() {
        Map<String, Object> author = new HashMap<>();
        author.put("id", "author_202");
        author.put("name", "Charles Dickens");
        db.collection("Authors").document("author_202").set(author);

        Map<String, Object> book1 = new HashMap<>();
        book1.put("author_id", "author_202");
        book1.put("title", "Great Expectations");
        book1.put("rating", 4.8);
        db.collection("Books").add(book1);

        Map<String, Object> book2 = new HashMap<>();
        book2.put("author_id", "author_202");
        book2.put("title", "Oliver Twist");
        book2.put("rating", 4.5);
        db.collection("Books").add(book2);
    }
  

الردّ

        {
            "id": "author_202",
            "name": "Charles Dickens",
            "averageBookRating": 4.65
        }
      

subcollection(...) الاستعلامات الفرعية

على الرغم من أنّ مرحلة إدخال subcollection(...) تُقدَّم كمرحلة، فإنّها تتيح إجراء عمليات ربط على نموذج البيانات الهرمي الخاص بـ Cloud Firestore. في النموذج الهرمي، غالبًا ما تحتاج طلبات البحث إلى استرداد مستند إلى جانب البيانات من مجموعاته الفرعية. على الرغم من أنّه يمكنك تحقيق ذلك باستخدام مرحلة إدخال collection_group(...) متبوعة بفلتر على المرجع الرئيسي، إلا أنّ subcollection(...) يوفّر صيغة أكثر اختصارًا.

بخلاف شرط الربط الضمني، يعمل هذا الإجراء بشكل مشابه لاستعلام فرعي عن مصفوفة، ويعرض نتيجة فارغة إذا لم تتم مطابقة أي مستندات، حتى إذا لم تكن المجموعة المتداخلة متوفّرة.

وهي في الأساس تجميل لغوي: تستخدم تلقائيًا __name__ للمستند في النطاق الخارجي كمفتاح ربط لحل العلاقة الهرمية. وهذا يجعلها الطريقة المفضّلة لإجراء عمليات البحث في المجموعات المرتبطة بعلاقة العنصر الرئيسي والعنصر الثانوي.

أفضل الممارسات

  • إدارة الذاكرة باستخدام toArrayExpression(): يجب توخّي الحذر عند استخدام طلبات البحث الفرعية toArrayExpression()، لأنّ إنشاء عدد كبير من المستندات قد يؤدي إلى استنفاد الحد الأقصى المسموح به لذاكرة طلب البحث (128 ميغابايت). لحلّ هذه المشكلة، استخدِم select(...) ضمن طلب البحث الفرعي لعرض الحقول الضرورية فقط، وطبِّق فلاتر where(...) للحدّ من عدد المستندات المعروضة. ننصحك باستخدام limit(...) إذا كان ذلك مناسبًا للحدّ من عدد المستندات التي تعرضها الاستعلامات الفرعية.
  • الفهرسة: تأكَّد من فهرسة الحقول المستخدَمة في عبارة where(...) الخاصة باستعلام فرعي. تعتمد عمليات الربط ذات الأداء الجيد على إمكانية إجراء عمليات بحث في الفهرس بدلاً من عمليات البحث الكاملة في الجدول.

لمزيد من أفضل الممارسات المتعلّقة بالطلبات، يُرجى الرجوع إلى الدليل الذي يغطّي تحسين الطلبات.

القيود

  • subcollection(...) النطاق: لا تتوفّر مرحلة الإدخال subcollection(...) إلا ضمن الاستعلامات الفرعية، لأنّها تتطلّب سياق مستند رئيسي لحل العلاقة الهرمية وإجراء عملية الربط.
  • عمق التضمين: يمكن تضمين الاستعلامات الفرعية بما يصل إلى 20 طبقة.
  • استخدام الذاكرة: ينطبق الحدّ الأقصى البالغ 128 ميغابايت على البيانات المادية في طلب البحث بأكمله، بما في ذلك جميع المستندات التي تم ربطها.