ทำการรวมด้วยการสืบค้นย่อย

ภาพรวม

Firestore รุ่น Enterprise รองรับการผนวกแบบเชิงสัมพันธ์ผ่านการค้นหาย่อยแบบสัมพันธ์ การค้นหาย่อยช่วยให้คุณรวมและรวบรวมข้อมูลจากคอลเล็กชันหรือคอลเล็กชันย่อยที่เกี่ยวข้องได้โดยตรงบนเซิร์ฟเวอร์ ซึ่งแตกต่างจากฐานข้อมูล NoSQL หลายรายการที่มักจะต้องทำให้ข้อมูลเป็นปกติหรือทำการขอฝั่งไคลเอ็นต์หลายรายการ

การค้นหาย่อยเป็นนิพจน์ที่เรียกใช้ไปป์ไลน์แบบซ้อนสำหรับเอกสารทุกรายการที่การค้นหาภายนอกประมวลผล ซึ่งช่วยให้สามารถดึงข้อมูลที่ซับซ้อนได้ เช่น การดึงเอกสารพร้อมกับรายการคอลเล็กชันย่อยที่เกี่ยวข้อง หรือการรวมข้อมูลที่เชื่อมโยงกันอย่างมีเหตุผลจากคอลเล็กชันรากที่แตกต่างกัน

แนวคิด

ส่วนนี้จะแนะนำแนวคิดหลักเบื้องหลังการใช้การค้นหาย่อยเพื่อทำการผนวกในการดำเนินการไปป์ไลน์

การค้นหาย่อยเป็นนิพจน์

การค้นหาย่อยไม่ใช่ระยะระดับบนสุด แต่เป็นนิพจน์ ที่ ใช้ได้ในทุกระยะที่ยอมรับนิพจน์ เช่น select(...), add_fields(...), where(...) หรือ sort(...)

Cloud Firestore รองรับการค้นหาย่อย 3 ประเภท ได้แก่

  • การค้นหาย่อยแบบอาร์เรย์: สร้างชุดผลลัพธ์ทั้งหมดของการค้นหาย่อยเป็นอาร์เรย์ของเอกสาร
  • การค้นหาย่อยแบบสเกลาร์: ประเมินเป็นค่าเดียว เช่น จำนวน ค่าเฉลี่ย หรือฟิลด์ที่เฉพาะเจาะจงจากเอกสารที่เกี่ยวข้อง
  • การค้นหาย่อย subcollection(...): การผนวกที่ง่ายขึ้นสำหรับความสัมพันธ์แบบหนึ่งต่อหลายรายการระหว่างเอกสารหลักกับเอกสารย่อย

ขอบเขตและตัวแปร

เมื่อเขียนการผนวก การค้นหาย่อยแบบซ้อนมักจะต้องอ้างอิงฟิลด์จากเอกสาร "ภายนอก" (เอกสารหลัก) หากต้องการเชื่อมขอบเขตเหล่านี้ ให้ใช้ระยะ let(...) (เรียกว่า define(...) ใน SDK บางรายการ) เพื่อกำหนดตัวแปรในขอบเขตหลัก ซึ่งจากนั้นจะอ้างอิงได้ในการค้นหาย่อยโดยใช้ฟังก์ชัน variable(...)

ไวยากรณ์

ส่วนต่อไปนี้จะให้ภาพรวมของไวยากรณ์สำหรับการผนวก

ระยะ let(...)

ระยะ let(...) (เรียกว่า define(...) ใน SDK บางรายการ) เป็นระยะที่ไม่กรองซึ่งนำข้อมูลจากขอบเขตหลัก ไปยังตัวแปรที่มีชื่ออย่างชัดเจนเพื่อใช้ในขอบเขตแบบซ้อนที่ตามมา

การค้นหาย่อยแบบอาร์เรย์

การค้นหาย่อยแบบอาร์เรย์เป็นการค้นหาย่อยแบบนิพจน์กรณีพิเศษที่สร้างชุดผลลัพธ์ทั้งหมดของการค้นหาย่อยเป็นอาร์เรย์ หากการค้นหาย่อยแสดงผล 0 แถว การค้นหาย่อยจะประเมินเป็นอาร์เรย์ว่าง และจะไม่แสดงผลอาร์เรย์ null การค้นหาดังกล่าวมีประโยชน์เมื่อต้องการผลลัพธ์ทั้งหมดในผลลัพธ์สุดท้าย เช่น เมื่อสร้างคอลเล็กชันแบบซ้อนหรือแบบสัมพันธ์

การค้นหาสามารถกรอง จัดเรียง และรวมในการค้นหาย่อยเพื่อลดจำนวนข้อมูลที่ต้องดึงและแสดงผล ซึ่งจะช่วยลดค่าใช้จ่ายในการค้นหา ระบบจะพิจารณาลำดับของการค้นหาย่อย ซึ่งหมายความว่าระยะ sort(...) ในการค้นหาย่อยจะควบคุมลำดับของผลลัพธ์ในอาร์เรย์สุดท้าย

ใช้ Wrapper SDK toArrayExpression() เพื่อแปลงการค้นหาเป็นอาร์เรย์

การค้นหาย่อยแบบสเกลาร์

การค้นหาย่อยแบบสเกลาร์มักใช้ในระยะ select(...) หรือ where(...) เพื่ออนุญาตการกรองหรือแสดงผลลัพธ์ของการค้นหาย่อยโดยไม่ต้องสร้างการค้นหาทั้งหมดโดยตรง

การค้นหาย่อยแบบสเกลาร์ที่แสดงผลลัพธ์ 0 รายการจะประเมินเป็น null เอง ในขณะที่การค้นหาย่อยที่ประเมินเป็นองค์ประกอบหลายรายการจะทำให้เกิดข้อผิดพลาดรันไทม์

เมื่อการค้นหาย่อยแบบสเกลาร์แสดงผลเพียงฟิลด์เดียวต่อผลลัพธ์ ระบบจะ ยกระดับ ฟิลด์ดังกล่าวให้เป็นผลลัพธ์ระดับบนสุดสำหรับการค้นหาย่อย ซึ่งมักจะเห็นได้ชัดเจนที่สุดเมื่อการค้นหาย่อยลงท้ายด้วย select(field("user_name")) หรือ aggregate(countAll().as("total")) โดยที่สคีมาของการค้นหาย่อยเป็นเพียง ฟิลด์เดียว มิฉะนั้น เมื่อการค้นหาย่อยแสดงผลหลายฟิลด์ได้ ระบบจะห่อฟิลด์เหล่านั้นไว้ในแผนที่

ใช้ Wrapper SDK 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
}

N รายการแรกต่อกลุ่ม (การค้นหาย่อยที่มีขีดจำกัด)

การค้นหาต่อไปนี้จะดึงเอกสารทั้งหมดจากกลุ่มคอลเล็กชัน restaurants และใช้การค้นหาย่อยแบบสัมพันธ์เพื่อดึงรีวิว 2 รายการแรกที่มีคะแนนสูงสุดสำหรับร้านอาหารแต่ละร้าน

วิธีนี้จะช่วยให้มั่นใจได้ว่าอาร์เรย์ของรีวิวจะไม่ใหญ่เกินไปและไม่เกินขีดจำกัดหน่วยความจำของการค้นหา

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

การรวมแบบไม่ตรงกัน (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(...) จะทำให้เซิร์ฟเวอร์ทำซ้ำเอกสารร้านอาหารภายนอกสำหรับรีวิวที่ตรงกันแต่ละรายการ ซึ่งจะสร้างเอกสารที่รวมกันแบบแบน (คล้ายกับ INNER JOIN ของ SQL)

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() เนื่องจากเอกสารจำนวนมากอาจทำให้หน่วยความจำของการค้นหาเต็ม (128 MiB) หากต้องการลดปัญหานี้ ให้ใช้ select(...) ในการค้นหาย่อยเพื่อแสดงผล เฉพาะฟิลด์ที่จำเป็น และใช้ where(...) ตัวกรองเพื่อจำกัดจำนวนเอกสารที่แสดงผล พิจารณาใช้ limit(...) หากเหมาะสมเพื่อจำกัดจำนวน เอกสารที่การค้นหาย่อยแสดงผล
  • การจัดทำดัชนี: ตรวจสอบว่าฟิลด์ที่ใช้ใน where(...) อนุประโยคของการค้นหาย่อยมีการจัดทำดัชนี การรวมที่มีประสิทธิภาพขึ้นอยู่กับความสามารถในการค้นหาดัชนีแทนการสแกนตารางทั้งหมด

ดูแนวทางปฏิบัติแนะนำเพิ่มเติมเกี่ยวกับการค้นหาได้ใน คู่มือการเพิ่มประสิทธิภาพการค้นหา

ข้อจำกัด

  • subcollection(...) ขอบเขต: ระบบรองรับระยะอินพุต subcollection(...) ภายในเฉพาะการค้นหาย่อยเท่านั้น เนื่องจากต้องมีบริบทของเอกสารหลักเพื่อแก้ความสัมพันธ์แบบลำดับชั้นและทำการรวม
  • ระดับการซ้อน: การค้นหาย่อยสามารถซ้อนกันได้สูงสุด 20 ระดับ
  • การใช้หน่วยความจำ: ขีดจำกัด 128 MiB สำหรับข้อมูลที่สร้างขึ้นมีผลกับการค้นหาทั้งหมด รวมถึงเอกสารที่รวมกันทั้งหมด