ความสัมพันธ์ระหว่าง Entity: One-to-One, One-to-Many, Many-to-Many
ในบทที่แล้ว เราได้เรียนรู้วิธีการจัดการข้อมูลพื้นฐาน (CRUD Operations) ด้วย ORM (Object-Relational Mapper) กันไปแล้ว แต่ในโลกของฐานข้อมูลจริงๆ ความสัมพันธ์ระหว่างตาราง (Table) ต่างๆ นั้นซับซ้อนกว่านั้นมาก แอปพลิเคชันส่วนใหญ่มักจะมีข้อมูลที่เชื่อมโยงกัน เช่น ผู้ใช้งาน (User) กับบทความ (Article), สินค้า (Product) กับหมวดหมู่ (Category), หรือ นักเรียน (Student) กับคอร์สเรียน (Course) เป็นต้น
ORM เข้ามาช่วยให้เราจัดการความสัมพันธ์เหล่านี้ได้ง่ายขึ้น โดยการแปลงความสัมพันธ์ระหว่างตารางในฐานข้อมูล ให้กลายเป็นความสัมพันธ์ระหว่าง Object ในโค้ดของเรา ทำให้เราสามารถเขียนโค้ดที่อ่านง่าย เข้าใจง่าย และบำรุงรักษาง่ายขึ้นได้มาก
ในบทนี้ เราจะมาเจาะลึกถึงประเภทของความสัมพันธ์ที่พบบ่อยที่สุดในฐานข้อมูล และวิธีการจัดการความสัมพันธ์เหล่านี้ด้วย ORM กันครับ
ประเภทของความสัมพันธ์
ความสัมพันธ์ระหว่าง Entity (หรือตารางในฐานข้อมูล) มีอยู่ 3 ประเภทหลักๆ คือ:
- One-to-One (หนึ่งต่อหนึ่ง): หนึ่ง Entity สามารถเชื่อมโยงกับอีก Entity หนึ่งได้เพียงหนึ่งเดียวเท่านั้น
- One-to-Many (หนึ่งต่อหลาย): หนึ่ง Entity สามารถเชื่อมโยงกับอีก Entity หนึ่งได้หลาย Entity
- Many-to-Many (หลายต่อหลาย): หลาย Entity สามารถเชื่อมโยงกับอีกหลาย Entity ได้
เราจะมาดูแต่ละประเภทกันอย่างละเอียด พร้อมตัวอย่างการใช้งานจริงครับ
One-to-One Relationship (หนึ่งต่อหนึ่ง)
ความสัมพันธ์แบบ One-to-One คือความสัมพันธ์ที่ Entity หนึ่งเชื่อมโยงกับ Entity อื่นได้เพียง Entity เดียวเท่านั้น ตัวอย่างเช่น:
- User กับ Profile: ผู้ใช้งานหนึ่งคน (User) มีข้อมูลส่วนตัว (Profile) ได้เพียงหนึ่งเดียว
- Person กับ Passport: บุคคลหนึ่งคน (Person) มีหนังสือเดินทาง (Passport) ได้เพียงหนึ่งเดียว
- Headquarter กับ Country: สำนักงานใหญ่ (Headquarter) ตั้งอยู่ในประเทศ (Country) ได้เพียงประเทศเดียว
ในการกำหนดความสัมพันธ์แบบ One-to-One ใน ORM เรามักจะใช้ Foreign Key (คีย์นอก) เพื่อเชื่อมโยง Entity ทั้งสองเข้าด้วยกัน
ตัวอย่าง (Doctrine):
// Entity User
/**
* @Entity
* @Table(name="users")
*/
class User
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
private $id;
/**
* @Column(type="string", length=255)
*/
private $username;
/**
* @OneToOne(targetEntity="Profile", mappedBy="user")
*/
private $profile;
// ... getters and setters ...
}
// Entity Profile
/**
* @Entity
* @Table(name="profiles")
*/
class Profile
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
private $id;
/**
* @OneToOne(targetEntity="User", inversedBy="profile")
* @JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
/**
* @Column(type="string", length=255)
*/
private $fullname;
// ... getters and setters ...
}
ในตัวอย่างนี้ @OneToOne annotation ใน User Entity บอกว่า User มีความสัมพันธ์แบบ One-to-One กับ Profile Entity และ mappedBy="user" บอกว่าความสัมพันธ์นี้ถูกจัดการโดย user property ใน Profile Entity ส่วนใน Profile Entity @OneToOne annotation บอกว่า Profile มีความสัมพันธ์แบบ One-to-One กับ User Entity และ inversedBy="profile" บอกว่าความสัมพันธ์นี้ถูกจัดการโดย profile property ใน User Entity และ @JoinColumn บอกว่า user_id column ใน profiles table เป็น Foreign Key ที่เชื่อมโยงกับ id column ใน users table
เหตุผลเชิงออกแบบและ Trade-offs: การใช้ One-to-One Relationship มักจะถูกใช้เมื่อเราต้องการแบ่งข้อมูลออกเป็นส่วนๆ เพื่อความชัดเจนในการจัดการ หรือเพื่อเพิ่มประสิทธิภาพในการ query ข้อมูล (เช่น ข้อมูลส่วนตัวที่ไม่จำเป็นต้องโหลดทุกครั้งที่โหลดข้อมูลผู้ใช้งาน) แต่ข้อเสียคือ อาจจะทำให้ query ซับซ้อนขึ้นเล็กน้อย หากต้องดึงข้อมูลจากทั้งสอง Entity บ่อยๆ
One-to-Many Relationship (หนึ่งต่อหลาย)
ความสัมพันธ์แบบ One-to-Many คือความสัมพันธ์ที่ Entity หนึ่งเชื่อมโยงกับ Entity อื่นได้หลาย Entity ตัวอย่างเช่น:
- User กับ Article: ผู้ใช้งานหนึ่งคน (User) สามารถเขียนบทความ (Article) ได้หลายบทความ
- Category กับ Product: หมวดหมู่หนึ่งหมวด (Category) สามารถมีสินค้า (Product) ได้หลายรายการ
- Author กับ Book: นักเขียนหนึ่งคน (Author) สามารถเขียนหนังสือ (Book) ได้หลายเล่ม
ในการกำหนดความสัมพันธ์แบบ One-to-Many ใน ORM เรามักจะใช้ Foreign Key ใน Entity ฝั่ง "Many" เพื่อเชื่อมโยงกับ Entity ฝั่ง "One"
ตัวอย่าง (Doctrine):
// Entity User
/**
* @Entity
* @Table(name="users")
*/
class User
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
private $id;
/**
* @Column(type="string", length=255)
*/
private $username;
/**
* @OneToMany(targetEntity="Article", mappedBy="author")
*/
private $articles;
public function __construct() {
$this->articles = new \Doctrine\Common\Collections\ArrayCollection();
}
// ... getters and setters ...
public function addArticle(Article $article)
{
$this->articles[] = $article;
$article->setAuthor($this); // จำเป็นต้อง set author ใน article ด้วย
return $this;
}
}
// Entity Article
/**
* @Entity
* @Table(name="articles")
*/
class Article
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
private $id;
/**
* @Column(type="string", length=255)
*/
private $title;
/**
* @ManyToOne(targetEntity="User", inversedBy="articles")
* @JoinColumn(name="author_id", referencedColumnName="id")
*/
private $author;
// ... getters and setters ...
public function setAuthor(User $author)
{
$this->author = $author;
return $this;
}
}
ในตัวอย่างนี้ @OneToMany annotation ใน User Entity บอกว่า User มีความสัมพันธ์แบบ One-to-Many กับ Article Entity และ mappedBy="author" บอกว่าความสัมพันธ์นี้ถูกจัดการโดย author property ใน Article Entity ส่วนใน Article Entity @ManyToOne annotation บอกว่า Article มีความสัมพันธ์แบบ Many-to-One กับ User Entity และ inversedBy="articles" บอกว่าความสัมพันธ์นี้ถูกจัดการโดย articles property ใน User Entity และ @JoinColumn บอกว่า author_id column ใน articles table เป็น Foreign Key ที่เชื่อมโยงกับ id column ใน users table
เหตุผลเชิงออกแบบและ Trade-offs: One-to-Many Relationship เป็นความสัมพันธ์ที่พบบ่อยมากในการออกแบบฐานข้อมูล ช่วยให้เราจัดระเบียบข้อมูลได้อย่างมีประสิทธิภาพ และสามารถ query ข้อมูลที่เกี่ยวข้องได้อย่างง่ายดาย ข้อควรระวังคือ เมื่อ Entity ฝั่ง "Many" มีจำนวนมาก อาจจะส่งผลต่อประสิทธิภาพในการ query ข้อมูลได้ ในกรณีนี้ อาจจะต้องพิจารณาการใช้ Pagination (การแบ่งหน้า) หรือ Indexing (การสร้างดัชนี) เพื่อเพิ่มประสิทธิภาพ
Many-to-Many Relationship (หลายต่อหลาย)
ความสัมพันธ์แบบ Many-to-Many คือความสัมพันธ์ที่หลาย Entity สามารถเชื่อมโยงกับอีกหลาย Entity ได้ ตัวอย่างเช่น:
- Student กับ Course: นักเรียนหลายคน (Student) สามารถลงทะเบียนเรียนหลายคอร์ส (Course) ได้ และคอร์สหนึ่งคอร์สก็สามารถมีนักเรียนหลายคนลงทะเบียนได้
- Product กับ Category: สินค้าหลายรายการ (Product) สามารถอยู่ในหลายหมวดหมู่ (Category) ได้ และหมวดหมู่หนึ่งหมวดก็สามารถมีสินค้าหลายรายการได้
- Actor กับ Movie: นักแสดงหลายคน (Actor) สามารถเล่นในภาพยนตร์หลายเรื่อง (Movie) ได้ และภาพยนตร์หนึ่งเรื่องก็สามารถมีนักแสดงหลายคนได้
ในการกำหนดความสัมพันธ์แบบ Many-to-Many ใน ORM เรามักจะต้องใช้ Join Table (หรือบางครั้งเรียกว่า Pivot Table) เพื่อเก็บความสัมพันธ์ระหว่าง Entity ทั้งสอง Join Table จะมี Foreign Key สองตัว ซึ่งอ้างอิงไปยัง Primary Key ของ Entity ทั้งสอง
ตัวอย่าง (Doctrine):
// Entity Student
/**
* @Entity
* @Table(name="students")
*/
class Student
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
private $id;
/**
* @Column(type="string", length=255)
*/
private $name;
/**
* @ManyToMany(targetEntity="Course", inversedBy="students")
* @JoinTable(name="student_courses",
* joinColumns={@JoinColumn(name="student_id", referencedColumnName="id")},
* inverseJoinColumns={@JoinColumn(name="course_id", referencedColumnName="id")}
* )
*/
private $courses;
public function __construct() {
$this->courses = new \Doctrine\Common\Collections\ArrayCollection();
}
// ... getters and setters ...
}
// Entity Course
/**
* @Entity
* @Table(name="courses")
*/
class Course
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
private $id;
/**
* @Column(type="string", length=255)
*/
private $name;
/**
* @ManyToMany(targetEntity="Student", mappedBy="courses")
*/
private $students;
public function __construct() {
$this->students = new \Doctrine\Common\Collections\ArrayCollection();
}
// ... getters and setters ...
}
ในตัวอย่างนี้ @ManyToMany annotation ใน Student Entity บอกว่า Student มีความสัมพันธ์แบบ Many-to-Many กับ Course Entity และ inversedBy="students" บอกว่าความสัมพันธ์นี้ถูกจัดการโดย students property ใน Course Entity และ @JoinTable บอกว่า Join Table ชื่อ student_courses มี student_id column เป็น Foreign Key ที่เชื่อมโยงกับ id column ใน students table และ course_id column เป็น Foreign Key ที่เชื่อมโยงกับ id column ใน courses table ส่วนใน Course Entity @ManyToMany annotation บอกว่า Course มีความสัมพันธ์แบบ Many-to-Many กับ Student Entity และ mappedBy="courses" บอกว่าความสัมพันธ์นี้ถูกจัดการโดย courses property ใน Student Entity
เหตุผลเชิงออกแบบและ Trade-offs: Many-to-Many Relationship เป็นความสัมพันธ์ที่ซับซ้อนที่สุด แต่ก็มีความยืดหยุ่นมากที่สุด ช่วยให้เราสร้างความสัมพันธ์ที่ซับซ้อนระหว่างข้อมูลได้อย่างมีประสิทธิภาพ การออกแบบ Join Table ที่ดีมีความสำคัญมาก เพื่อให้สามารถ query ข้อมูลได้อย่างรวดเร็ว และหลีกเลี่ยงปัญหา Data Integrity (ความถูกต้องของข้อมูล)
สรุป
ในบทนี้ เราได้เรียนรู้เกี่ยวกับประเภทของความสัมพันธ์ระหว่าง Entity ต่างๆ ในฐานข้อมูล และวิธีการจัดการความสัมพันธ์เหล่านี้ด้วย ORM (Doctrine ORM เป็นตัวอย่าง) การเข้าใจความสัมพันธ์เหล่านี้เป็นสิ่งสำคัญมากในการออกแบบฐานข้อมูลที่มีประสิทธิภาพ และสามารถจัดการข้อมูลได้อย่างถูกต้องและแม่นยำ ในบทต่อไป เราจะมาเรียนรู้วิธีการสร้าง Query อย่างยืดหยุ่นด้วย ORM โดยใช้ Query Builder กันครับ