انجام پیوندها با زیرپرس‌وجوها

نمای کلی

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

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

مفاهیم

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

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

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

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

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

حوزه و متغیرها

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

نحو

بخش‌های زیر نمای کلی از نحو اجرای پیوندها ارائه می‌دهند.

مرحله let(...)

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

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

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

پُرسمان‌ها می‌توانند در زیرپُرسمان فیلتر، مرتب‌سازی، و جمع‌آوری کنند تا مقدار داده‌هایی را که باید واکشی و برگردانده شوند نیز کاهش دهند و به کاهش هزینه پُرسمان کمک کنند. ترتیب زیرپرسش رعایت می‌شود، یعنی مرحله sort(...) در زیرپرسش ترتیب نتایج را در آرایه نهایی کنترل می‌کند.

از پوشاننده کیت توسعه نرم‌افزار toArrayExpression() برای تبدیل پُرسمان به آرایه استفاده کنید.

زیرپرسش‌های اسکالر

زیرپرس‌وجوهای اسکالر اغلب در مرحله select(...) یا where(...) استفاده می‌شوند زیرا امکان فیلتر کردن یا به‌دست آوردن نتیجه زیرپرس‌وجو را بدون تحقق مستقیم پرس‌وجوی کامل فراهم می‌کنند.

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

وقتی یک زیرپرس‌وجوی اسکالر فقط یک فیلد برای هر نتیجه تولید می‌کند، فیلد ارتقا می‌یابد تا نتیجه سطح بالا برای زیرپرس‌وجو باشد. این مورد معمولاً زمانی دیده می‌شود که زیرپرس‌وجو با select(field("user_name")) یا aggregate(countAll().as("total")) به‌پایان می‌رسد و طرح‌واره زیرپرس‌وجو فقط یک فیلد است. درغیراین‌صورت، وقتی یک زیرپرس‌وجو می‌تواند چندین فیلد تولید کند، آن‌ها در یک نقشه پیچیده می‌شوند.

از پوشاننده کیت توسعه نرم‌افزار toScalarExpression() برای تبدیل پُرسمان به عبارت اسکالر استفاده کنید.

subcollection(...) زیرپرس‌وجو

درحالی‌که به‌عنوان مرحله ارائه می‌شود، مرحله ورودی subcollection(...) امکان انجام پیوستن‌ها را روی مدل داده سلسله‌مراتبی Cloud Firestore فراهم می‌کند. در مدل سلسله‌مراتبی، پُرسمان‌ها اغلب باید سندی را به‌همراه داده‌های زیرمجموعه‌های خودش بازیابی کنند. اگرچه می‌توانید این کار را بااستفاده از مرحله ورودی collection_group(...) و سپس فیلتر کردن مرجع والد انجام دهید، subcollection(...) نحو بسیار مختصرتری ارائه می‌دهد.

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

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

مثال‌ها

داده‌های نمونه

در زیر مجموعه‌ای از داده‌های آزمایشی برای استفاده در همه مثال‌های بعدی بار می‌شود.

Node.js

// Load set of cities.
const cities = collection(db, "cities");

await setDoc(doc(cities, "SF"), {
  name: "San Francisco",
  state: "CA",
  country: "USA",
});
await setDoc(doc(cities, "LA"), {
  name: "Los Angeles",
  state: "CA",
  country: "USA"
});
await setDoc(doc(cities, "DC"), {
  name: "Washington, D.C.",
  state: null,
  country: "USA"
});
await setDoc(doc(cities, "TOK"), {
  name: "Tokyo",
  state: null,
  country: "Japan"
});

// Load restaurants in various cities.
const sfRestaurants = collection(db, "cities", "SF", "restaurants");
const laRestaurants = collection(db, "cities", "LA", "restaurants");
const dcRestaurants = collection(db, "cities", "DC", "restaurants");

const rest1 = await addDoc(sfRestaurants, {
  name: "Golden Gate Pizza",
  type: "pizza",
  owner_id: "Mario Rossi"
});
const rest2 = await addDoc(sfRestaurants, {
  name: "Bay Area Burger",
  type: "burger",
  owner_id: "Sarah Jenkins"
});
const rest3 = await addDoc(sfRestaurants, {
  name: "Sunset Taco",
  type: "mexican",
  owner_id: "Edward"
});

const rest4 = await addDoc(laRestaurants, {
  name: "Hollywood Sushi",
  type: "sushi",
  owner_id: "Ken Kenji"
});
const rest5 = await addDoc(laRestaurants, {
  name: "Venice Pizza",
  type: "pizza",
  owner_id: "Luigi Romano"
});

const rest6 = await addDoc(dcRestaurants, {
  name: "Capitol Tacos",
  type: "mexican",
  owner_id: "Maria Garcia"
});
const rest7 = await addDoc(dcRestaurants, {
  name: "Georgetown Coffee",
  type: "cafe",
  owner_id: "David Kim"
});

// Load collection of reviews.
const reviews = collection(db, "reviews");

await addDoc(reviews, { restaurant: rest1, rating: 5, reviewer_id "Alice" });
await addDoc(reviews, { restaurant: rest1, rating: 4, reviewer_id "Bob" });
await addDoc(reviews, { restaurant: rest2, rating: 4, reviewer_id "Charlie" });
await addDoc(reviews, { restaurant: rest3, rating: 5, reviewer_id "Diana" });
await addDoc(reviews, { restaurant: rest3, rating: 4, reviewer_id "Edward" });
await addDoc(reviews, { restaurant: rest3, rating: 4, reviewer_id "Fiona" });
// rest4 has 0 reviews
await addDoc(reviews, { restaurant: rest5, rating: 3, reviewer_id "George" });
await addDoc(reviews, { restaurant: rest6, rating: 5, reviewer_id "Hannah" });
await addDoc(reviews, { restaurant: rest6, rating: 4, reviewer_id "Ian" });
await addDoc(reviews, { restaurant: rest7, rating: 5, reviewer_id "Julia" });

جستجوی سند در مجموعه‌ای دیگر

پُرسمان زیر در گروه مجموعه reviews جستجویی در گروه مجموعه restaurant بااستفاده از مرجع کلید اصلی انجام می‌دهد.

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("reviews")
  .define(field("restaurant").as("restaurant_name"))
  .addFields(db.pipeline()
    .collectionGroup("restaurant")
    .where(field("__name__").equal(variable("restaurant_name")))
    .select("name", "type")
    .toScalarExpression()
    .as("restaurant")));

پاسخ

{
  rating: 5,
  reviewer_id "Alice",
  restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
  rating: 4,
  reviewer_id "Bob",
  restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
  rating: 4,
  reviewer_id "Charlie",
  restaurant: { name: "Bay Area Burger", type: "burger" }
},
{
  rating: 5,
  reviewer_id "Diana",
  restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
  rating: 4,
  reviewer_id "Edward",
  restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
  rating: 4,
  reviewer_id "Fiona",
  restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
  rating: 3,
  reviewer_id "George",
  restaurant: { name: "Venice Pizza", type: "pizza" }
},
{
  rating: 5,
  reviewer_id "Hannah",
  restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
  rating: 4,
  reviewer_id "Ian",
  restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
  rating: 5,
  reviewer_id "Julia",
  restaurant: { name: "Georgetown Coffee", type: "cafe" }
}

ترکیب کردن چند مجموعه

پرسمان زیر همه پیتزافروشی‌ها را از گروه مجموعه restaurants واکشی می‌کند و از یک زیرپرسمان آرایه برای واکشی و جاسازی مرورهای مرتبط با آن‌ها مستقیماً در پاسخ استفاده می‌کند.

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .where(field("type").equal("pizza"))
  .define(field("__name__").as("restaurant_name"))
  .select(
    field("name"),
    db.pipeline()
      .collectionGroup("reviews")
      .where(field("restaurant").equal(variable("restaurant_name")))
      .select("rating", "reviewer_id")
      .toArrayExpression()
      .as("reviews")));

پاسخ

{
  name: "Golden Gate Pizza",
  reviews: [
    { rating: 5, reviewer_id "Alice" },
    { rating: 4, reviewer_id "Bob" }
  ]
},
{
  name: "Venice Pizza",
  type: "pizza",
  owner_id: "Luigi Romano",
  reviews: [
    { rating: 3, reviewer_id "George" }
  ]
}

انبوهش در چندین مجموعه

پرسمان زیر در گروه مجموعه restaurants از یک زیرپرسمان همبسته برای دریافت میانگین رتبه‌بندی هر رستوران از گروه مجموعه reviews استفاده می‌کند.

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .where(field("type").equal("pizza"))
  .define(field("__name__").as("restaurant_name"))
  .select(
    field("name"),
    db.pipeline()
      .collectionGroup("reviews")
      .where(field("restaurant").equal(variable("restaurant_name")))
      .aggregate(average("rating").as("avg_rating"))
      .toScalarExpression()
      .as("avg_rating")));

پاسخ

{
  name: "Golden Gate Pizza",
  avg_rating: 4.5
},
{
  name: "Venice Pizza",
  avg_rating: 3.0
}

Top-N Per Group (Subquery with Limit)

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

این کار تضمین می‌کند که آرایه مرورها بیش‌ازحد بزرگ نشود و به حد مجاز حافظه پُرسمان نرسد.

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .define(field("__name__").as("restaurant_name"))
  .select(
    field("name"),
    db.pipeline()
      .collectionGroup("reviews")
      .where(field("restaurant").equal(variable("restaurant_name")))
      .sort(field("rating").descending())
      .limit(2)
      .select("rating", "reviewer_id")
      .toArrayExpression()
      .as("top_reviews")));

پاسخ

{
  name: "Golden Gate Pizza",
  top_reviews: [
    { rating: 5, reviewer_id "Alice" },
    { rating: 4, reviewer_id "Bob" }
  ]
},
{
  name: "Bay Area Burger",
  top_reviews: [
    { rating: 4, reviewer_id "Charlie" }
  ]
},
{
  name: "Sunset Taco",
  top_reviews: [
    { rating: 5, reviewer_id "Diana" },
    { rating: 4, reviewer_id "Edward" }
  ]
},
{
  name: "Hollywood Sushi",
  top_reviews: []
},
{
  name: "Venice Pizza",
  top_reviews: [
    { rating: 3, reviewer_id "George" }
  ]
},
{
  name: "Capitol Tacos",
  top_reviews: [
    { rating: 5, reviewer_id "Hannah" },
    { rating: 4, reviewer_id "Ian" }
  ]
},
{
  name: "Georgetown Coffee",
  top_reviews: [
    { rating: 5, reviewer_id "Julia" }
  ]
}

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

پرس‌وجو زیر مجموعه cities را اسکن می‌کند و از مرحله subcollection(...) برای پیوستن ضمنی به اسناد از یک مجموعه تودرتو برای یافتن تعداد رستوران‌ها در هر شهر استفاده می‌کند.

Node.js

let results = await execute(db.pipeline()
  .collection("cities")
  .addFields(subcollection("restaurants")
    .toArrayExpression()
    .length()
    .as("restaurant_count")));

پاسخ

{
  __name__: cities/SF,
  name: "San Francisco",
  state: "CA",
  country: "USA",
  restaurant_count: 3
},
{
  __name__: cities/LA,
  name: "Los Angeles",
  state: "CA",
  country: "USA",
  restaurant_count: 2
},
{
  __name__: cities/DC,
  name: "Washington, D.C.",
  state: null,
  country: "USA",
  restaurant_count: 2
},
{
  __name__: cities/TOK,
  name: "Tokyo",
  state: null,
  country: "Japan",
  restaurant_count: 0
}

شرایط پیوستن چندگانه سریع

پرس‌وجو زیر گروه مجموعه restaurants را اسکن می‌کند و یک اتصال چندفیلدی با گروه مجموعه reviews انجام می‌دهد تا مالکانی را که رستوران‌های خودشان را مرور می‌کنند پیدا کند.

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .define(field("owner_id"), field("__name__"))
  .where(db.pipeline()
    .collectionGroup("reviews")
    .where(field("restaurant").equal(variable("__name__")))
    .where(field("author").equal(variable("owner_id")))
    .aggregate(count().as("c"))
    .toScalarExpression()
    .greaterThan(0)));

پاسخ

{
  __name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
  name: "Sunset Taco",
  type: "mexican",
  owner_id: "Edward"
}

Anti-Join (NOT EXISTS)

پرس‌وجو زیر گروه مجموعه restaurants را اسکن می‌کند و همه رستوران‌هایی را که هنوز مروری ندارند پیدا می‌کند.

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .define(field("__name__").as("restaurant_name"))
  .where(db.pipeline()
    .collectionGroup("reviews")
    .where(field("restaurant").equal(variable("restaurant_name")))
    .aggregate(count().as("review_count"))
    .toScalarExpression()
    .equal(0)));

پاسخ

{
  __name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
  name: "Hollywood Sushi",
  type: "sushi",
  owner_id: "Ken Kenji"
}

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

پرسمان زیر رابطه بین هر پیتزافروشی و مرورهای آن را مسطح می‌کند. با قرار دادن زیرپُرسمان در مرحله unnest(...)، سرور سند رستوران بیرونی را برای هر مرور منطبق تکرار می‌کند و اسناد مسطح و پیوست‌شده‌ای تولید می‌کند (مشابه SQL INNER JOIN).

Node.js

let results = await execute(db.pipeline()
  .collectionGroup("restaurants")
  .where(field("type").equal("pizza"))
  .define(field("__name__").as("restaurant_name"))
  .unnest(
    db.pipeline()
      .collectionGroup("reviews")
      .where(field("restaurant").equal(variable("restaurant_name")))
      .select("rating", "reviewer_id")
      .toArrayExpression()
      .as("review")));

پاسخ

{
  __name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
  name: "Golden Gate Pizza",
  type: "pizza",
  owner_id: "Mario Rossi"
  review: { rating: 5, reviewer_id "Alice" }
},
{
  __name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
  name: "Golden Gate Pizza",
  type: "pizza",
  owner_id: "Mario Rossi",
  review: { rating: 4, reviewer_id "Bob" }
},
{
  __name__: "cities/LA/restaurants/6CYntvNgbYzgaW652Gq1",
  name: "Venice Pizza",
  type: "pizza",
  owner_id: "Luigi Romano",
  review: { rating: 3, reviewer_id "George" }
}

زیرپرس‌وجوی غیرهمبسته به‌عنوان فیلتر

پرسمان زیر در مجموعه reviews فیلترها را بااستفاده از یک زیرپرسمان غیرهمبسته روی خودش انجام می‌دهد تا مرورهای بالاتر از میانگین رده‌بندی را پیدا کند.

Node.js

let results = await execute(db.pipeline()
  .collection("reviews")
  // Average review rating is 4.3
  .where(field("rating").greaterThan(db.pipeline()
    .collection("reviews")
    .aggregate(average("rating").as("avg"))
    .toScalarExpression())))
  .select("rating", "reviewer_id");

پاسخ

{
  rating: 5,
  reviewer_id "Alice"
},
{
  rating: 5,
  reviewer_id "Diana"
},
{
  rating: 5,
  reviewer_id "Hannah"
},
{
  rating: 5,
  reviewer_id "Julia"
}

روال‌های مطلوب

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

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

محدودیت‌ها

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