Doctrine ORM: เชี่ยวชาญความสัมพันธ์ใน Symfony
คู่มือฉบับสมบูรณ์เกี่ยวกับความสัมพันธ์ Doctrine ORM ใน Symfony OneToMany, ManyToMany, กลยุทธ์การโหลดและการเพิ่มประสิทธิภาพพร้อมตัวอย่างจริง

ความสัมพันธ์ระหว่างเอนทิตีคือกระดูกสันหลังของแอปพลิเคชัน Symfony ทุกตัวที่ใช้ Doctrine ORM ความเข้าใจที่มั่นคงเกี่ยวกับประเภทความสัมพันธ์ กลยุทธ์การโหลด และกับดักด้านประสิทธิภาพช่วยให้สร้างแอปพลิเคชันที่แข็งแรงและรวดเร็ว คู่มือนี้ครอบคลุมแพตเทิร์นสำคัญสำหรับการจัดการความสัมพันธ์ Doctrine อย่างมีประสิทธิผล
Doctrine ใช้แนวคิดของฝั่งเจ้าของและฝั่งผกผัน การเข้าใจความแตกต่างนี้ช่วยป้องกันบั๊กจำนวนมากที่เกี่ยวข้องกับการบันทึกความสัมพันธ์
ประเภทความสัมพันธ์ใน Doctrine
Doctrine นำเสนอความสัมพันธ์ระหว่างเอนทิตีสี่แบบ ได้แก่ OneToOne, OneToMany, ManyToOne และ ManyToMany แต่ละประเภทตอบโจทย์ความต้องการทางธุรกิจที่ต่างกันและมีคุณสมบัติด้านประสิทธิภาพเฉพาะตัว
ความสัมพันธ์ ManyToOne: กรณีที่พบบ่อยที่สุด
ความสัมพันธ์ ManyToOne คือลิงก์ฐานข้อมูลที่พบบ่อยที่สุด เอนทิตีหลายตัวฝั่งหนึ่งสัมพันธ์กับเอนทิตีตัวเดียวอีกฝั่ง ความสัมพันธ์นี้เป็นฝั่งเจ้าของของการเชื่อมโยงเสมอ
// A comment belongs to a single article
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CommentRepository::class)]
class Comment
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: 'text')]
private string $content;
// ManyToOne: many comments → one article
// This entity owns the relationship (foreign key stored here)
#[ORM\ManyToOne(targetEntity: Article::class, inversedBy: 'comments')]
#[ORM\JoinColumn(nullable: false)] // Relationship is required
private Article $article;
#[ORM\Column]
private \DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
}
public function getArticle(): Article
{
return $this->article;
}
public function setArticle(Article $article): self
{
$this->article = $article;
return $this;
}
}เอนทิตี Comment เก็บคีย์ต่างประเทศ article_id Doctrine จัดการการบันทึกโดยอัตโนมัติ การเรียก $comment->setArticle($article) จะสร้างลิงก์ในฐานข้อมูล
ความสัมพันธ์ OneToMany: ฝั่งผกผัน
ความสัมพันธ์ OneToMany คือฝั่งผกผันของ ManyToOne ช่วยให้ไล่จากเอนทิตี «หนึ่ง» ไปยังเอนทิตี «หลาย» ได้ ความสัมพันธ์นี้ไม่เคยเป็นฝั่งเจ้าของและไม่มีคีย์ต่างประเทศ
// An article has multiple comments
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $title;
#[ORM\Column(type: 'text')]
private string $content;
// OneToMany: one article → many comments
// Inverse side: mappedBy points to the owning entity's property
#[ORM\OneToMany(
targetEntity: Comment::class,
mappedBy: 'article',
cascade: ['persist', 'remove'], // Cascade operations
orphanRemoval: true // Remove orphaned comments
)]
private Collection $comments;
public function __construct()
{
// Initialize collection in the constructor
$this->comments = new ArrayCollection();
}
/**
* @return Collection<int, Comment>
*/
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments->add($comment);
// CRITICAL: synchronize the owning side
$comment->setArticle($this);
}
return $this;
}
public function removeComment(Comment $comment): self
{
if ($this->comments->removeElement($comment)) {
// orphanRemoval handles deletion
}
return $this;
}
}เมธอด addComment() และ removeComment() รับประกันความสอดคล้องสองทาง หากขาดบรรทัด $comment->setArticle($this) ความสัมพันธ์จะไม่ถูกบันทึกอย่างถูกต้อง
การลืมซิงค์ทั้งสองฝั่งของความสัมพันธ์สองทางคือข้อผิดพลาดที่พบบ่อยที่สุด ควรแก้ไขฝั่งเจ้าของเสมอเพื่อรับประกันการบันทึก
ความสัมพันธ์ ManyToMany: การเชื่อมโยงหลายต่อหลาย
ความสัมพันธ์ ManyToMany เชื่อมหลายเอนทิตีในทั้งสองฝั่ง Doctrine จะสร้างตารางเชื่อมให้อัตโนมัติ ต้องกำหนดฝั่งหนึ่งให้เป็นเจ้าของผ่านแอตทริบิวต์ inversedBy
// An article can have multiple tags
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
// ... other properties
// ManyToMany: owning side (inversedBy)
// Join table: article_tag (auto-generated)
#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'articles')]
#[ORM\JoinTable(name: 'article_tag')] // Explicit table name
private Collection $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
/**
* @return Collection<int, Tag>
*/
public function getTags(): Collection
{
return $this->tags;
}
public function addTag(Tag $tag): self
{
if (!$this->tags->contains($tag)) {
$this->tags->add($tag);
// Owning side: no need to sync other side for persistence
// but recommended for in-memory consistency
$tag->addArticle($this);
}
return $this;
}
public function removeTag(Tag $tag): self
{
if ($this->tags->removeElement($tag)) {
$tag->removeArticle($this);
}
return $this;
}
}// A tag can belong to multiple articles
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TagRepository::class)]
class Tag
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 50, unique: true)]
private string $name;
// Inverse side: mappedBy points to the owner
#[ORM\ManyToMany(targetEntity: Article::class, mappedBy: 'tags')]
private Collection $articles;
public function __construct()
{
$this->articles = new ArrayCollection();
}
/**
* @return Collection<int, Article>
*/
public function getArticles(): Collection
{
return $this->articles;
}
public function addArticle(Article $article): self
{
if (!$this->articles->contains($article)) {
$this->articles->add($article);
}
return $this;
}
public function removeArticle(Article $article): self
{
$this->articles->removeElement($article);
return $this;
}
}สำหรับความสัมพันธ์ ManyToMany ที่ต้องเก็บข้อมูลเพิ่มเติม (วันที่เชื่อมโยง ลำดับ ฯลฯ) ควรเปลี่ยนเป็นความสัมพันธ์ ManyToOne สองชุดที่ชี้ไปยังเอนทิตีกลาง
พร้อมที่จะพิชิตการสัมภาษณ์ Symfony แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
กลยุทธ์การโหลดและประสิทธิภาพ
การโหลดความสัมพันธ์คือจุดสำคัญด้านประสิทธิภาพใน Doctrine มีสามกลยุทธ์ ได้แก่ LAZY (ค่าเริ่มต้น), EAGER และ EXTRA_LAZY
Lazy Loading: โหลดเมื่อต้องใช้
การโหลดแบบขี้เกียจดึงความสัมพันธ์เฉพาะเมื่อมีการเข้าถึงครั้งแรก กลยุทธ์ค่าเริ่มต้นนี้ช่วยหลีกเลี่ยงคิวรีที่ไม่จำเป็น แต่อาจก่อให้เกิดปัญหา N+1
// Demonstrating the N+1 problem
namespace App\Repository;
use App\Entity\Article;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class ArticleRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Article::class);
}
// N+1 PROBLEM: one query per article for comments
public function findAllWithLazyComments(): array
{
// Query 1: SELECT * FROM article
$articles = $this->findAll();
// In view or service:
// foreach ($articles as $article) {
// $article->getComments(); // Query N: SELECT * FROM comment WHERE article_id = ?
// }
return $articles;
}
}สำหรับ 100 บทความ โค้ดนี้จะสร้างคิวรี SQL 101 ครั้ง คือหนึ่งครั้งสำหรับบทความและอีกหนึ่งครั้งต่อบทความเพื่อโหลดความคิดเห็น
Eager Loading ด้วย join
Eager loading แก้ปัญหา N+1 โดยดึงความสัมพันธ์ในคิวรีเดียวกันด้วย join
// Optimized loading with joins
namespace App\Repository;
use App\Entity\Article;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class ArticleRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Article::class);
}
// SOLUTION: explicit join with fetch join
public function findAllWithComments(): array
{
return $this->createQueryBuilder('a')
// LEFT JOIN loads articles even without comments
->leftJoin('a.comments', 'c')
// addSelect includes comments in the result
->addSelect('c')
// Sort by comment creation date
->orderBy('c.createdAt', 'DESC')
->getQuery()
->getResult();
// Single SQL query with JOIN
}
// Loading multiple relationships
public function findAllWithCommentsAndTags(): array
{
return $this->createQueryBuilder('a')
->leftJoin('a.comments', 'c')
->addSelect('c')
->leftJoin('a.tags', 't')
->addSelect('t')
->leftJoin('c.author', 'ca') // Comment author join
->addSelect('ca')
->getQuery()
->getResult();
}
}การใช้ addSelect() หลังทุก leftJoin() เป็นสิ่งจำเป็น หากไม่มี addSelect() Doctrine จะทำ join แต่จะไม่โหลดเอนทิตีที่เกี่ยวข้อง
Extra Lazy: เพิ่มประสิทธิภาพคอลเลกชันขนาดใหญ่
กลยุทธ์ EXTRA_LAZY เพิ่มประสิทธิภาพการดำเนินการบนคอลเลกชันขนาดใหญ่โดยไม่ต้องโหลดทุกองค์ประกอบ
// Category with potentially thousands of products
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CategoryRepository::class)]
class Category
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 100)]
private string $name;
// EXTRA_LAZY: optimizes count(), contains(), slice()
#[ORM\OneToMany(
targetEntity: Product::class,
mappedBy: 'category',
fetch: 'EXTRA_LAZY' // Does not load all products
)]
private Collection $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
// count() executes SELECT COUNT(*) instead of loading collection
public function getProductCount(): int
{
return $this->products->count();
// SQL: SELECT COUNT(*) FROM product WHERE category_id = ?
}
// contains() checks existence without loading everything
public function hasProduct(Product $product): bool
{
return $this->products->contains($product);
// SQL: SELECT 1 FROM product WHERE id = ? AND category_id = ?
}
// slice() loads only a portion
public function getRecentProducts(int $limit = 5): array
{
return $this->products->slice(0, $limit);
// SQL: SELECT * FROM product WHERE category_id = ? LIMIT 5
}
}EXTRA_LAZY ช่วยหลีกเลี่ยงการโหลดเอนทิตีนับพันเพียงเพื่อตรวจสอบหรือนับอย่างง่าย
ควรใช้ EXTRA_LAZY กับคอลเลกชันที่อาจมีขนาดใหญ่ (เกิน 100 องค์ประกอบ) ซึ่งใช้การดำเนินการ count(), contains() หรือ slice() บ่อย
Cascade และการจัดการวงจรชีวิต
ตัวเลือก cascade ทำให้การเผยแพร่ปฏิบัติการไปยังเอนทิตีที่เกี่ยวข้องเป็นอัตโนมัติ มีตัวเลือกหลักสามแบบ ได้แก่ persist, remove และ orphanRemoval
Cascade Persist: การบันทึกอัตโนมัติ
Cascade persist บันทึกเอนทิตีที่เกี่ยวข้องที่สร้างใหม่โดยอัตโนมัติเมื่อ flush
// Order with cascaded order lines
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OrderRepository::class)]
#[ORM\Table(name: '`order`')] // order is a SQL reserved word
class Order
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 50)]
private string $reference;
// cascade persist: OrderLines are persisted with Order
#[ORM\OneToMany(
targetEntity: OrderLine::class,
mappedBy: 'order',
cascade: ['persist'] // Automatically persist lines
)]
private Collection $lines;
public function __construct()
{
$this->lines = new ArrayCollection();
$this->reference = 'ORD-' . uniqid();
}
public function addLine(OrderLine $line): self
{
if (!$this->lines->contains($line)) {
$this->lines->add($line);
$line->setOrder($this);
}
return $this;
}
}// Order creation with cascade persist
namespace App\Service;
use App\Entity\Order;
use App\Entity\OrderLine;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
class OrderService
{
public function __construct(
private readonly EntityManagerInterface $em
) {}
public function createOrder(array $cartItems): Order
{
$order = new Order();
foreach ($cartItems as $item) {
$line = new OrderLine();
$line->setProduct($item['product']);
$line->setQuantity($item['quantity']);
$line->setUnitPrice($item['product']->getPrice());
// addLine synchronizes the relationship
$order->addLine($line);
}
// Only Order is explicitly persisted
// OrderLines are persisted automatically (cascade)
$this->em->persist($order);
$this->em->flush();
return $order;
}
}Cascade Remove และ OrphanRemoval
Cascade remove ลบเอนทิตีที่เกี่ยวข้อง OrphanRemoval ก้าวไกลกว่านั้นโดยลบเอนทิตีที่ถูกแยกออกจากคอลเลกชันด้วย
// Blog post with images
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BlogPostRepository::class)]
class BlogPost
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private string $title;
#[ORM\OneToMany(
targetEntity: Image::class,
mappedBy: 'blogPost',
cascade: ['persist', 'remove'], // Delete images with post
orphanRemoval: true // Also delete detached images
)]
private Collection $images;
public function __construct()
{
$this->images = new ArrayCollection();
}
public function removeImage(Image $image): self
{
if ($this->images->removeElement($image)) {
// orphanRemoval: image will be deleted on flush
// Without orphanRemoval: image would remain in DB without blogPost
}
return $this;
}
public function clearImages(): self
{
// Clear collection → all images will be deleted
$this->images->clear();
return $this;
}
}ความแตกต่างสำคัญ: cascade: ['remove'] จะลบเอนทิตีที่เกี่ยวข้องเฉพาะเมื่อเอนทิตีพ่อถูกลบ ส่วน orphanRemoval: true ลบเอนทิตีที่ถูกนำออกจากคอลเลกชันด้วย
คิวรี DQL ขั้นสูงสำหรับความสัมพันธ์
DQL (Doctrine Query Language) มอบความยืดหยุ่นสูงสุดสำหรับคิวรีที่เกี่ยวข้องกับความสัมพันธ์ที่ซับซ้อน
การกรองตามความสัมพันธ์
// Advanced queries on relationships
namespace App\Repository;
use App\Entity\Article;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class ArticleRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Article::class);
}
// Articles with at least one comment
public function findWithComments(): array
{
return $this->createQueryBuilder('a')
->innerJoin('a.comments', 'c') // INNER JOIN excludes articles without comments
->addSelect('c')
->groupBy('a.id')
->getQuery()
->getResult();
}
// Articles by tag with comment count
public function findByTagWithCommentCount(string $tagName): array
{
return $this->createQueryBuilder('a')
->select('a', 'COUNT(c.id) as commentCount')
->leftJoin('a.comments', 'c')
->innerJoin('a.tags', 't')
->where('t.name = :tagName')
->setParameter('tagName', $tagName)
->groupBy('a.id')
->orderBy('commentCount', 'DESC')
->getQuery()
->getResult();
}
// Recent articles with comment authors
public function findRecentWithAuthors(\DateTimeInterface $since): array
{
return $this->createQueryBuilder('a')
->leftJoin('a.comments', 'c')
->addSelect('c')
->leftJoin('c.author', 'u') // Join on comment author
->addSelect('u')
->where('a.publishedAt > :since')
->setParameter('since', $since)
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult();
}
}Subquery และฟังก์ชันรวม
// Complex queries with subqueries
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class UserRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
// Most active users (by article count)
public function findMostActiveAuthors(int $limit = 10): array
{
return $this->createQueryBuilder('u')
->select('u', 'COUNT(a.id) as articleCount')
->leftJoin('u.articles', 'a')
->where('a.status = :published')
->setParameter('published', 'published')
->groupBy('u.id')
->having('COUNT(a.id) > 0') // HAVING to filter after GROUP BY
->orderBy('articleCount', 'DESC')
->setMaxResults($limit)
->getQuery()
->getResult();
}
// Users with articles having more than 10 comments
public function findAuthorsWithPopularArticles(): array
{
// DQL subquery
$em = $this->getEntityManager();
$subQuery = $em->createQueryBuilder()
->select('IDENTITY(a2.author)')
->from('App\Entity\Article', 'a2')
->leftJoin('a2.comments', 'c2')
->groupBy('a2.id')
->having('COUNT(c2.id) > 10')
->getDQL();
return $this->createQueryBuilder('u')
->where('u.id IN (' . $subQuery . ')')
->getQuery()
->getResult();
}
}เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แนวทางปฏิบัติและแพตเทิร์นขั้นสูง
การกำหนดค่าเริ่มต้นของคอลเลกชันอย่างถูกต้อง
คอลเลกชันต้องถูกกำหนดค่าเริ่มต้นใน constructor เสมอเพื่อหลีกเลี่ยงข้อผิดพลาดด้านชนิดข้อมูล
// Proper relationship initialization
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AuthorRepository::class)]
class Author
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\OneToMany(targetEntity: Book::class, mappedBy: 'author')]
private Collection $books;
#[ORM\ManyToMany(targetEntity: Genre::class)]
private Collection $favoriteGenres;
// ALWAYS initialize in constructor
public function __construct()
{
$this->books = new ArrayCollection();
$this->favoriteGenres = new ArrayCollection();
}
// Utility method to check if collection is loaded
public function areBooksLoaded(): bool
{
return $this->books->isInitialized();
}
}หลีกเลี่ยงการอ้างอิงแบบวงกลม
ความสัมพันธ์สองทางอาจสร้างวงวนไม่สิ้นสุดในการ serialize
// Handling circular references
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\MaxDepth;
#[ORM\Entity(repositoryClass: DepartmentRepository::class)]
class Department
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['department:read'])]
private ?int $id = null;
#[ORM\Column(length: 100)]
#[Groups(['department:read', 'employee:read'])]
private string $name;
// MaxDepth limits serialization depth
#[ORM\OneToMany(targetEntity: Employee::class, mappedBy: 'department')]
#[Groups(['department:read'])]
#[MaxDepth(1)] // Does not serialize employee relations
private Collection $employees;
public function __construct()
{
$this->employees = new ArrayCollection();
}
}แพตเทิร์น Repository พร้อมเงื่อนไขแบบไดนามิก
// Flexible search criteria
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
// Search with optional filters
public function findByCriteria(array $criteria): array
{
$qb = $this->createQueryBuilder('p')
->leftJoin('p.category', 'c')
->addSelect('c')
->leftJoin('p.tags', 't')
->addSelect('t');
// Conditional filters
if (isset($criteria['category'])) {
$qb->andWhere('c.slug = :category')
->setParameter('category', $criteria['category']);
}
if (isset($criteria['minPrice'])) {
$qb->andWhere('p.price >= :minPrice')
->setParameter('minPrice', $criteria['minPrice']);
}
if (isset($criteria['maxPrice'])) {
$qb->andWhere('p.price <= :maxPrice')
->setParameter('maxPrice', $criteria['maxPrice']);
}
if (isset($criteria['tags']) && is_array($criteria['tags'])) {
$qb->andWhere('t.name IN (:tags)')
->setParameter('tags', $criteria['tags']);
}
if (isset($criteria['inStock']) && $criteria['inStock']) {
$qb->andWhere('p.stock > 0');
}
return $qb->orderBy('p.createdAt', 'DESC')
->getQuery()
->getResult();
}
}บทสรุป
การเชี่ยวชาญความสัมพันธ์ Doctrine ORM ตั้งอยู่บนหลักการพื้นฐานไม่กี่ข้อ:
✅ ฝั่งเจ้าของกับฝั่งผกผัน: ปรับปรุงฝั่งเจ้าของเสมอเพื่อรับประกันการบันทึก
✅ การซิงค์สองทาง: เมธอด add/remove ต้องซิงค์ทั้งสองฝั่ง
✅ Fetch join: ใช้ addSelect() หลังทุก join เพื่อหลีกเลี่ยงปัญหา N+1
✅ EXTRA_LAZY: เปิดใช้งานในคอลเลกชันขนาดใหญ่เพื่อเพิ่มประสิทธิภาพ count() และ contains()
✅ Cascade อย่างระมัดระวัง: persist มักจะมีประโยชน์ ส่วน remove และ orphanRemoval ขึ้นอยู่กับบริบททางธุรกิจ
✅ การกำหนดค่าเริ่มต้นของคอลเลกชัน: ใน constructor เสมอด้วย ArrayCollection
แพตเทิร์นเหล่านี้เป็นรากฐานของแอปพลิเคชัน Symfony ที่มีประสิทธิภาพ ขั้นตอนถัดไปคือการเชี่ยวชาญดัชนีและคิวรี native สำหรับกรณีที่ต้องการประสิทธิภาพระดับสูงสุด
เริ่มฝึกซ้อมเลย!
ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ
แท็ก
แชร์
บทความที่เกี่ยวข้อง

คำถามสัมภาษณ์ Symfony: 25 อันดับแรกในปี 2026
25 คำถามสัมภาษณ์ Symfony ที่ถูกถามบ่อยที่สุด สถาปัตยกรรม, Doctrine ORM, บริการ, ความปลอดภัย, ฟอร์มและการทดสอบ พร้อมคำตอบละเอียดและตัวอย่างโค้ด

Symfony Live Components และ UX 3.0: แอปพลิเคชันแบบ Reactive โดยไม่ต้องใช้ JavaScript ในปี 2026
Symfony Live Components สร้างอินเทอร์เฟซแบบ reactive ด้วย PHP และ Twig โดยไม่ต้องใช้ JavaScript บทช่วยสอนเกี่ยวกับ LiveProp, LiveAction, form และ deferred loading

Symfony 7 กับ API Platform: แนวทางปฏิบัติที่ดีที่สุดสำหรับ REST API
คู่มือเชิงลึกสำหรับการสร้าง REST API ด้วย Symfony 7 และ API Platform 4 ครอบคลุม Resource configuration, State Processor, Serialization Groups, Security และการปรับแต่ง performance ด้วยตัวอย่างโค้ดที่ใช้งานได้จริง