انجام عملیات join با استفاده از subquery ها

پیشینه

عملیات خط لوله یک رابط پرس‌وجوی جدید برای Cloud Firestore است. این رابط، قابلیت پرس‌وجوی پیشرفته‌ای را ارائه می‌دهد که شامل عبارات پیچیده است. نسخه سازمانی Firestore از طریق زیرپرس‌وجوهای هم‌بسته، از اتصال‌های رابطه‌ای پشتیبانی می‌کند. برخلاف بسیاری از پایگاه‌های داده NoSQL که اغلب نیاز به غیرنرمال‌سازی داده‌ها یا انجام چندین درخواست سمت کلاینت دارند، زیرپرس‌وجوها به شما امکان می‌دهند داده‌ها را از مجموعه‌ها یا زیرمجموعه‌های مرتبط مستقیماً روی سرور ترکیب و تجمیع کنید.

زیرپرس‌وجوها عباراتی هستند که یک خط لوله تودرتو را برای هر سندی که توسط پرس‌وجوی بیرونی پردازش می‌شود، اجرا می‌کنند. این امر الگوهای بازیابی داده‌های پیچیده، مانند واکشی یک سند در کنار اقلام زیرمجموعه مرتبط با آن یا اتصال داده‌های منطقی مرتبط در مجموعه‌های ریشه‌ای متفاوت را امکان‌پذیر می‌سازد.

مفاهیم

این بخش مفاهیم اصلی پشت استفاده از subqueryها برای انجام joinها در عملیات Pipeline را معرفی می‌کند.

زیرپرس‌وجوها به عنوان عبارات

یک subquery یک مرحله سطح بالا نیست؛ در عوض، عبارتی است که می‌تواند در هر مرحله‌ای که عباراتی مانند select(...) ، add_fields(...) ، where(...) یا sort(...) را می‌پذیرد، استفاده شود.

Cloud Firestore از سه نوع زیرپرس‌وجو پشتیبانی می‌کند:

  • زیرپرس‌وجوهای آرایه‌ای: کل مجموعه نتایج زیرپرس‌وجو را به صورت آرایه‌ای از اسناد ارائه می‌دهد.
  • زیرپرس‌وجوهای اسکالر: ارزیابی به یک مقدار واحد، مانند تعداد، میانگین یا یک فیلد خاص از یک سند مرتبط.
  • subcollection(...) زیرپرس‌وجوها: پیوندهای ساده‌شده برای رابطه‌ی والد-فرزندی یک به چند.

دامنه و متغیرها

هنگام نوشتن یک پیوند، زیرپرس‌وجوی تودرتو اغلب نیاز دارد که به فیلدهایی از سند "بیرونی" (والد) ارجاع دهد. برای ایجاد پل بین این حوزه‌ها، از مرحله let(...) (که در برخی SDKها به عنوان define(...) شناخته می‌شود) برای تعریف متغیرهایی در حوزه والد استفاده می‌کنید که سپس می‌توانند با استفاده از تابع variable(...) در زیرپرس‌وجوی به آنها ارجاع داده شوند.

نحو

بخش‌های زیر مروری بر سینتکس اجرای joinها ارائه می‌دهند.

مرحله let(...)

مرحله let(...) (که در برخی SDKها به آن define(...) گفته می‌شود) یک مرحله بدون فیلتر است که صریحاً داده‌ها را از محدوده والد به یک متغیر نامگذاری شده برای استفاده در محدوده‌های تو در تو بعدی می‌آورد.

Web

    async function defineStageData() {
      await setDoc(doc(collection(db, "Authors"), "author_123"), {
        "id": "author_123",
        "name": "Jane Austen"
      });
    }
    
سویفت
      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);
    }
  

زیرپرس‌وجوهای آرایه

یک زیرپرس‌وجوی آرایه‌ای (Array subquery) حالت خاصی از زیرپرس‌وجوی عبارت (expression subquery) است که کل مجموعه نتایج زیرپرس‌وجو را در یک آرایه پیاده‌سازی می‌کند. اگر زیرپرس‌وجو هیچ ردیفی را برنگرداند، به یک آرایه خالی ارزیابی می‌شود. هرگز آرایه null array) برنمی‌گرداند. چنین پرس‌وجوهایی زمانی مفید هستند که نتایج کامل در نتیجه نهایی مورد نیاز باشند، مانند زمانی که یک مجموعه تودرتو یا همبسته پیاده‌سازی می‌شود.

پرس‌وجوها می‌توانند در زیرپرس‌وجو فیلتر، مرتب‌سازی و تجمیع شوند تا میزان داده‌هایی که باید واکشی و بازگردانده شوند نیز کاهش یابد و به کاهش هزینه پرس‌وجو کمک کند. ترتیب زیرپرس‌وجو رعایت می‌شود، به این معنی که مرحله 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"
      ]
    }
    
سویفت
    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")) به پایان می‌رسد که در آن طرحواره زیرپرس‌وجو فقط یک فیلد است. در غیر این صورت، وقتی یک زیرپرس‌وجو می‌تواند چندین فیلد تولید کند، آنها در یک map قرار می‌گیرند.

از بسته‌بندی 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
        }
      
سویفت
        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(...) امکان انجام joinها را بر روی مدل داده سلسله مراتبی Cloud Firestore فراهم می‌کند. در یک مدل سلسله مراتبی، کوئری‌ها اغلب نیاز به بازیابی یک سند در کنار داده‌های زیرمجموع‌های خود دارند. در حالی که می‌توانید با استفاده از مرحله ورودی collection_group(...) و به دنبال آن یک فیلتر روی مرجع والد، به این هدف دست یابید، subcollection(...) سینتکس بسیار مختصرتری ارائه می‌دهد.

به غیر از شرط اتصال ضمنی، این تابع مشابه یک زیرپرس و جوی آرایه عمل می‌کند و در صورت عدم تطابق اسناد، حتی اگر مجموعه تو در تو وجود نداشته باشد، نتیجه خالی را برمی‌گرداند.

اساساً یک دستور زبان شیرین است: به طور خودکار از __name__ سند در محدوده بیرونی به عنوان کلید اتصال برای حل رابطه سلسله مراتبی استفاده می‌کند. این امر آن را به روش ترجیحی برای انجام جستجو در مجموعه‌های مرتبط در یک رابطه والد-فرزندی تبدیل می‌کند.

بهترین شیوه‌ها

  • مدیریت حافظه با toArrayExpression() : در مورد زیرپرس‌وجوهای toArrayExpression() محتاط باشید، زیرا ارائه تعداد زیادی سند می‌تواند محدودیت حافظه پرس‌وجو (128 مگابایت) را به پایان برساند. برای کاهش این مشکل، از select(...) در زیرپرس‌وجو استفاده کنید تا فقط فیلدهای ضروری را برگردانید و فیلترهای where(...) را برای محدود کردن تعداد اسناد برگردانده شده اعمال کنید. در صورت لزوم، استفاده از limit(...) را برای محدود کردن تعداد اسناد برگردانده شده توسط زیرپرس‌وجو در نظر بگیرید.
  • فهرست‌بندی: اطمینان حاصل کنید که فیلدهای استفاده شده در عبارت where(...) از یک subquery فهرست‌بندی شده‌اند. پیوندهای Performant به توانایی انجام جستجوی فهرست به جای اسکن کامل جدول متکی هستند.

برای بهترین شیوه‌های پرس‌وجوی بیشتر، به راهنمای ما که شامل بهینه‌سازی پرس‌وجو می‌شود ، مراجعه کنید.

محدودیت‌ها

  • دامنه subcollection(...) : مرحله ورودی subcollection(...) فقط در subqueryها پشتیبانی می‌شود، زیرا برای حل رابطه سلسله مراتبی و انجام اتصال، به چارچوب یک سند والد نیاز دارد.
  • عمق تودرتو: زیرپرس‌وجوها می‌توانند تا عمق ۲۰ لایه تودرتو شوند.
  • میزان استفاده از حافظه: محدودیت ۱۲۸ مگابایت برای داده‌های مادی در کل پرس‌وجو، شامل تمام اسناد پیوست‌شده، اعمال می‌شود.