Query Builder: สร้าง Query อย่างยืดหยุ่นด้วย ORM
ในบทความก่อนหน้านี้ เราได้เรียนรู้เกี่ยวกับการจัดการความสัมพันธ์ระหว่าง Entity ต่างๆ ในฐานข้อมูลด้วย ORM (Doctrine) ซึ่งเป็นวิธีที่ช่วยให้เราไม่ต้องเขียน SQL เองโดยตรง แต่ในบางครั้งความต้องการในการดึงข้อมูลของเราอาจซับซ้อนกว่าการใช้ Repository Method ที่ ORM เตรียมไว้ให้ ในสถานการณ์เช่นนี้ Query Builder จะเข้ามาช่วยให้เราสร้าง Query ที่ยืดหยุ่นและปรับแต่งได้ตามต้องการ
Query Builder คืออะไร?
Query Builder คือเครื่องมือที่ ORM เตรียมไว้ให้เพื่อช่วยในการสร้าง SQL Query แบบ Dynamic (เปลี่ยนแปลงได้ตามเงื่อนไข) โดยไม่ต้องเขียน SQL เองโดยตรง มันเป็น API ที่ช่วยให้เราสามารถสร้าง Query ได้ทีละส่วน โดยการเพิ่มเงื่อนไขต่างๆ เช่น WHERE, ORDER BY, JOIN, และอื่นๆ เข้าไปใน Query ทีละขั้นตอน
เหตุผลเชิงออกแบบ (Design Trade-offs): ทำไมต้องมี Query Builder ในเมื่อเราเขียน SQL เองได้? คำตอบคือ Query Builder ช่วยลดความผิดพลาดที่อาจเกิดขึ้นจากการเขียน SQL เองโดยตรง (เช่น SQL Injection) และทำให้โค้ดของเราอ่านง่ายขึ้น บำรุงรักษาง่ายขึ้น และสามารถนำไปใช้ซ้ำได้ง่ายขึ้น นอกจากนี้ Query Builder ยังช่วยให้เราไม่ต้องกังวลเกี่ยวกับความแตกต่างของ Syntax (ไวยากรณ์) SQL ระหว่างฐานข้อมูลแต่ละประเภท เพราะ ORM จะจัดการแปลง Query ที่เราสร้างขึ้นให้เป็น SQL ที่ถูกต้องสำหรับฐานข้อมูลที่เราใช้งานอยู่
การใช้งาน Query Builder
การใช้งาน Query Builder ใน Doctrine (หรือ ORM อื่นๆ) มักจะเริ่มต้นด้วยการเรียกใช้ createQueryBuilder() จาก Repository ของ Entity ที่เราต้องการดึงข้อมูล ตัวอย่างเช่น ถ้าเราต้องการดึงข้อมูลจาก Entity Product เราสามารถทำได้ดังนี้:
// สมมติว่าเรามี ProductRepository ที่ inject เข้ามาใน Class นี้แล้ว
$queryBuilder = $this->productRepository->createQueryBuilder('p');
// 'p' คือ alias (ชื่อแทน) สำหรับ Entity Product ใน Query นี้
หลังจากได้ $queryBuilder มาแล้ว เราสามารถเพิ่มเงื่อนไขต่างๆ เข้าไปใน Query ได้ ตัวอย่างเช่น:
การ Filter ข้อมูล
การ Filter ข้อมูลคือการกำหนดเงื่อนไข WHERE ใน SQL Query เพื่อเลือกเฉพาะข้อมูลที่เราต้องการ Query Builder มีวิธีการ Filter ข้อมูลหลายแบบ เช่น
- เท่ากับ (Equals): ใช้
setParameter()และwhere() - ไม่เท่ากับ (Not Equals): ใช้
setParameter()และwhere()ร่วมกับ!=หรือ<> - มากกว่า (Greater Than): ใช้
setParameter()และwhere()ร่วมกับ> - น้อยกว่า (Less Than): ใช้
setParameter()และwhere()ร่วมกับ< - ระหว่าง (Between): ใช้
setParameter()และwhere()ร่วมกับBETWEEN - อยู่ใน (In): ใช้
setParameter()และwhere()ร่วมกับIN - คล้าย (Like): ใช้
setParameter()และwhere()ร่วมกับLIKE
ตัวอย่างการ Filter ข้อมูลเพื่อดึง Product ที่มีราคามากกว่า 1000 บาท:
$queryBuilder = $this->productRepository->createQueryBuilder('p')
->where('p.price > :price') // กำหนดเงื่อนไข WHERE price > :price
->setParameter('price', 1000); // กำหนดค่าให้ parameter 'price' คือ 1000
$products = $queryBuilder->getQuery()->getResult(); // execute query และ return ผลลัพธ์
อธิบายเพิ่มเติม:
'p.price > :price'คือเงื่อนไขที่เราต้องการใช้ โดยpคือ alias ของ EntityProductและpriceคือ attribute ของ Entity':price'คือ Parameter ที่เราใช้เพื่อป้องกัน SQL Injection และทำให้โค้ดของเราอ่านง่ายขึ้นsetParameter('price', 1000)คือการกำหนดค่าให้ Parameterpriceเป็น 1000getQuery()คือการสร้าง Query Object จาก Query BuildergetResult()คือการ execute Query และ return ผลลัพธ์เป็น Array ของ EntityProduct
การ Sort ข้อมูล
การ Sort ข้อมูลคือการเรียงลำดับข้อมูลตามเงื่อนไขที่เรากำหนด Query Builder มี Method orderBy() สำหรับเรียงลำดับข้อมูล
ตัวอย่างการ Sort ข้อมูลเพื่อเรียง Product ตามราคาจากมากไปน้อย:
$queryBuilder = $this->productRepository->createQueryBuilder('p')
->orderBy('p.price', 'DESC'); // เรียงตาม price จากมากไปน้อย (Descending)
$products = $queryBuilder->getQuery()->getResult();
อธิบายเพิ่มเติม:
orderBy('p.price', 'DESC')คือการเรียงลำดับข้อมูลตาม attributepriceของ EntityProductจากมากไปน้อย (DESCย่อมาจาก Descending) ถ้าเราต้องการเรียงจากน้อยไปมาก เราสามารถใช้ASC(Ascending) แทนได้
การ Join Table
ในบทก่อนหน้านี้ เราได้เรียนรู้เกี่ยวกับการจัดการความสัมพันธ์ระหว่าง Entity ต่างๆ การ Join Table คือการรวมข้อมูลจากหลาย Table ที่มีความสัมพันธ์กัน Query Builder มี Method join(), leftJoin(), และ rightJoin() สำหรับ Join Table
ตัวอย่างการ Join Table เพื่อดึง Product และ Category ของ Product นั้น:
$queryBuilder = $this->productRepository->createQueryBuilder('p')
->join('p.category', 'c') // Join ตาราง Product กับ Category โดยใช้ความสัมพันธ์ใน Entity Product (p.category)
->where('c.name = :categoryName') // Filter โดย category name
->setParameter('categoryName', 'Electronics');
$products = $queryBuilder->getQuery()->getResult();
อธิบายเพิ่มเติม:
join('p.category', 'c')คือการ Join TableProductกับCategoryโดยใช้ความสัมพันธ์categoryที่เราได้กำหนดไว้ใน EntityProduct(p.category) และกำหนด Alias ให้ TableCategoryเป็นcwhere('c.name = :categoryName')คือการ Filter ข้อมูลโดยเลือกเฉพาะ Product ที่มี Category Name เท่ากับ:categoryNamesetParameter('categoryName', 'Electronics')คือการกำหนดค่าให้ ParametercategoryNameเป็นElectronics
แนวปฏิบัติที่ดี (Best Practices):
- ใช้ Parameter: ควรใช้ Parameter เสมอเมื่อต้องการใส่ค่าเข้าไปใน Query เพื่อป้องกัน SQL Injection
- Alias ที่สื่อความหมาย: ตั้ง Alias ให้ Entity และ Table ให้สื่อความหมาย เพื่อให้โค้ดอ่านง่ายขึ้น
- แบ่ง Query ที่ซับซ้อนออกเป็นส่วนๆ: ถ้า Query ซับซ้อนมาก ควรแบ่งออกเป็นส่วนๆ เพื่อให้ง่ายต่อการทำความเข้าใจและบำรุงรักษา
สรุป
Query Builder เป็นเครื่องมือที่มีประโยชน์มากในการสร้าง SQL Query ที่ยืดหยุ่นและปรับแต่งได้ตามต้องการใน ORM ช่วยให้เราไม่ต้องเขียน SQL เองโดยตรง ลดความผิดพลาด และทำให้โค้ดของเราอ่านง่ายขึ้น บำรุงรักษาง่ายขึ้น และสามารถนำไปใช้ซ้ำได้ง่ายขึ้น การเข้าใจและใช้งาน Query Builder ได้อย่างมีประสิทธิภาพ จะช่วยให้เราสามารถพัฒนาแอปพลิเคชันที่ซับซ้อนได้อย่างรวดเร็วและมีประสิทธิภาพมากยิ่งขึ้น
ในบทความต่อไป เราจะมาเรียนรู้เกี่ยวกับ Repository Pattern ซึ่งเป็น Pattern ที่ช่วยให้เราจัดการ Data Access Layer ได้อย่างเป็นระบบและมีประสิทธิภาพมากยิ่งขึ้น