Thực hiện các thao tác kết hợp bằng truy vấn phụ

Thông tin khái quát

Các thao tác trong quy trình là một giao diện truy vấn mới cho Cloud Firestore. Giao diện này cung cấp chức năng truy vấn nâng cao, bao gồm cả các biểu thức phức tạp. Phiên bản Firestore Enterprise hỗ trợ các phép kết hợp theo kiểu quan hệ thông qua truy vấn con tương quan. Không giống như nhiều cơ sở dữ liệu NoSQL thường yêu cầu chuẩn hoá dữ liệu hoặc thực hiện nhiều yêu cầu phía máy khách, truy vấn con cho phép bạn kết hợp và tổng hợp dữ liệu từ các bộ sưu tập hoặc bộ sưu tập con có liên quan ngay trên máy chủ.

Truy vấn con là các biểu thức thực thi một quy trình lồng nhau cho mọi tài liệu được truy vấn bên ngoài xử lý. Điều này cho phép các mẫu truy xuất dữ liệu phức tạp, chẳng hạn như tìm nạp một tài liệu cùng với các mục trong bộ sưu tập con có liên quan hoặc kết hợp dữ liệu được liên kết một cách logic trên các bộ sưu tập gốc riêng biệt.

Khái niệm

Phần này giới thiệu các khái niệm cốt lõi đằng sau việc sử dụng truy vấn con để thực hiện các phép kết hợp trong các thao tác trong quy trình.

Truy vấn con dưới dạng biểu thức

Truy vấn con không phải là giai đoạn cấp cao nhất; thay vào đó, đây là một biểu thức có thể được sử dụng trong mọi giai đoạn chấp nhận biểu thức, chẳng hạn như select(...), add_fields(...), where(...), hoặc sort(...).

Cloud Firestore hỗ trợ 3 loại truy vấn con:

  • Truy vấn con dạng mảng: Hiện thực hoá toàn bộ tập kết quả của truy vấn con dưới dạng một mảng tài liệu.
  • Truy vấn con dạng vô hướng: Đánh giá thành một giá trị duy nhất, chẳng hạn như số lượng, giá trị trung bình hoặc một trường cụ thể từ một tài liệu có liên quan.
  • Truy vấn con subcollection(...): các phép kết hợp đơn giản hoá cho mối quan hệ một-nhiều giữa cha mẹ và con.

Phạm vi và biến

Khi viết một phép kết hợp, truy vấn con lồng nhau thường cần tham chiếu đến các trường từ tài liệu "bên ngoài" (tài liệu mẹ). Để kết nối các phạm vi này, bạn sử dụng giai đoạn let(...) (được gọi là define(...) trong một số SDK) để xác định các biến trong phạm vi mẹ mà sau đó có thể được tham chiếu trong truy vấn con bằng hàm variable(...).

Cú pháp

Các phần sau đây cung cấp thông tin tổng quan về cú pháp để thực hiện các phép kết hợp.

Giai đoạn let(...)

Giai đoạn let(...) (được gọi là define(...) trong một số SDK) là giai đoạn không lọc, giúp đưa dữ liệu một cách rõ ràng từ phạm vi mẹ vào một biến có tên để sử dụng trong các phạm vi lồng nhau tiếp theo.

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);
    }
  

Truy vấn con dạng mảng

Truy vấn con dạng mảng là một trường hợp đặc biệt của truy vấn con dạng biểu thức, giúp hiện thực hoá toàn bộ tập kết quả của truy vấn con thành một mảng. Nếu truy vấn con trả về 0 hàng, thì truy vấn con đó sẽ đánh giá thành một mảng trống. Truy vấn con này không bao giờ trả về một mảng null. Các truy vấn như vậy rất hữu ích khi bạn cần kết quả đầy đủ trong kết quả cuối cùng, chẳng hạn như khi hiện thực hoá một bộ sưu tập lồng nhau hoặc tương quan.

Các truy vấn có thể lọc, sắp xếp và tổng hợp trong truy vấn con để giảm lượng dữ liệu cần tìm nạp và trả về, giúp giảm chi phí truy vấn. Thứ tự của truy vấn con được tuân thủ, nghĩa là giai đoạn sort(...) trong truy vấn con sẽ kiểm soát thứ tự của kết quả trong mảng cuối cùng.

Sử dụng trình bao bọc SDK toArrayExpression() để chuyển đổi một truy vấn thành một mảng.

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"
      });
    }
  

Đáp

    {
        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"
      });
    }
    

Đáp

    {
      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)
    }
  

Đáp

    {
      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);
    }
  

Đáp

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

Truy vấn con dạng vô hướng

Truy vấn con dạng vô hướng thường được sử dụng trong giai đoạn select(...) hoặc where(...) vì cho phép lọc hoặc tạo kết quả của truy vấn con mà không hiện thực hoá trực tiếp toàn bộ truy vấn.

Truy vấn con dạng vô hướng tạo ra 0 kết quả sẽ tự đánh giá thành null, trong khi truy vấn con đánh giá thành nhiều phần tử sẽ dẫn đến lỗi thời gian chạy.

Khi truy vấn con dạng vô hướng chỉ tạo ra một trường duy nhất cho mỗi kết quả, trường đó sẽ được nâng cao để trở thành kết quả cấp cao nhất cho truy vấn con. Trường hợp này thường thấy nhất khi truy vấn con kết thúc bằng select(field("user_name")) hoặc aggregate(countAll().as("total")) trong đó giản đồ của truy vấn con chỉ là một trường duy nhất. Nếu không, khi truy vấn con có thể tạo ra nhiều trường, các trường đó sẽ được gói trong một bản đồ.

Sử dụng trình bao bọc SDK toScalarExpression() để chuyển đổi một truy vấn thành một biểu thức vô hướng.

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
              });
            }
        

Đáp

        {
            "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
        ])
        

Đáp

        {
            "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)
    }
  

Đáp

        {
            "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);
    }
  

Đáp

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

Truy vấn con subcollection(...)

Mặc dù được cung cấp dưới dạng một giai đoạn, nhưng giai đoạn đầu vào subcollection(...) cho phép thực hiện các phép kết hợp trên mô hình dữ liệu phân cấp của Cloud Firestore's. Trong mô hình phân cấp, các truy vấn thường cần truy xuất một tài liệu cùng với dữ liệu từ các bộ sưu tập con của tài liệu đó. Mặc dù bạn có thể thực hiện việc này bằng cách sử dụng giai đoạn đầu vào collection_group(...) rồi áp dụng bộ lọc cho tham chiếu mẹ, subcollection(...) cung cấp cú pháp ngắn gọn hơn nhiều.

Ngoài điều kiện kết hợp ngầm ẩn, điều này hoạt động tương tự như truy vấn con dạng mảng, trả về kết quả trống nếu không có tài liệu nào được so khớp, ngay cả khi bộ sưu tập lồng nhau không tồn tại.

Về cơ bản, đây là cú pháp dễ hiểu: nó tự động sử dụng __name__ của tài liệu trong phạm vi bên ngoài làm khoá kết hợp để giải quyết mối quan hệ phân cấp. Điều này khiến đây trở thành cách ưu tiên để thực hiện tra cứu trên các bộ sưu tập được liên kết trong mối quan hệ cha mẹ-con.

Các phương pháp hay nhất

  • Quản lý bộ nhớ bằng toArrayExpression(): Hãy thận trọng với toArrayExpression() các truy vấn con, vì việc hiện thực hoá một số lượng lớn tài liệu có thể làm cạn kiệt giới hạn bộ nhớ truy vấn (128 MiB). Để giảm thiểu vấn đề này, hãy sử dụng select(...) trong truy vấn con để chỉ trả về các trường cần thiết và áp dụng bộ lọc where(...) để giới hạn số lượng tài liệu được trả về. Cân nhắc sử dụng limit(...) nếu phù hợp để giới hạn số lượng tài liệu được truy vấn con trả về.
  • Lập chỉ mục: Đảm bảo rằng các trường được sử dụng trong mệnh đề where(...) của truy vấn con được lập chỉ mục. Các phép kết hợp hiệu suất cao dựa vào khả năng thực hiện tìm kiếm chỉ mục thay vì quét toàn bộ bảng.

Để biết thêm các phương pháp hay nhất về truy vấn, hãy tham khảo hướng dẫn của chúng tôi về việc tối ưu hoá truy vấn.

Hạn chế

  • subcollection(...) phạm vi: Giai đoạn đầu vào subcollection(...) chỉ được hỗ trợ trong các truy vấn con, vì giai đoạn này yêu cầu ngữ cảnh của tài tài liệu mẹ để giải quyết mối quan hệ phân cấp và thực hiện phép kết hợp.
  • Độ sâu lồng nhau: Bạn có thể lồng các truy vấn con tối đa 20 lớp.
  • Mức sử dụng bộ nhớ: Giới hạn 128 MiB đối với dữ liệu được hiện thực hoá áp dụng cho toàn bộ truy vấn, bao gồm cả tất cả tài liệu được kết hợp.