Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 7 — Relationships — One-to-One, One-to-Many, Many-to-Many

Data Modeling and ERD Mastery Bài 7/60

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn đang xây một ngôi nhà bằng Lego. Mỗi viên Lego là một entity — Khách hàng, Đơn hàng, Sản phẩm. Nhưng nếu bạn chỉ có một đống viên gạch rời rạc, bạn chưa có ngôi nhà. Thứ biến đống gạch đó thành một công trình có ý nghĩa chính là cách các viên gạch khớp với nhau — đó chính là relationship (mối quan hệ) trong mô hình dữ liệu.

Trong các bài trước, bạn đã học cách định nghĩa entity (Bài 4), gắn cho chúng các attribute (Bài 5) và chọn khóa chính để nhận diện từng dòng dữ liệu (Bài 6). Bây giờ là lúc kết nối chúng lại. Đây là bước mà rất nhiều BA (Business Analyst) mới vào nghề làm sai, dẫn đến hậu quả nghiêm trọng: dữ liệu trùng lặp, không thể truy vấn báo cáo, hoặc tệ hơn là viết sai logic nghiệp vụ ngay từ gốc.

Một sự thật ít người nói thẳng với bạn: phần lớn lỗi thiết kế cơ sở dữ liệu không nằm ở việc đặt tên cột hay chọn kiểu dữ liệu, mà nằm ở việc hiểu sai bản chất mối quan hệ giữa các thực thể nghiệp vụ. Khi bạn xác định đúng rằng "một khách hàng có thể có nhiều đơn hàng" hay "một sinh viên đăng ký nhiều môn học và một môn học có nhiều sinh viên", bạn đang dịch ngôn ngữ kinh doanh thành cấu trúc dữ liệu. Bài học hôm nay sẽ trang bị cho bạn ba loại quan hệ nền tảng — và quan trọng hơn, cách phân biệt chúng trong tình huống thực tế.

Khái niệm cốt lõi

Relationship là gì?

Relationship là sự liên kết (association) có ý nghĩa nghiệp vụ giữa hai (hoặc nhiều) entity. Khi bạn nói "Khách hàng đặt Đơn hàng", từ "đặt" chính là mối quan hệ. Nó cho biết hai thực thể này không độc lập mà ràng buộc với nhau theo một logic kinh doanh cụ thể.

Một relationship luôn có hai khía cạnh cần làm rõ:

  • Bản chất ngữ nghĩa (verb phrase): hành động hay sự liên kết, ví dụ "sở hữu", "thuộc về", "đăng ký", "quản lý".
  • Cardinality (lực lượng/bội số): con số cho biết bao nhiêu bản ghi bên này tương ứng với bao nhiêu bản ghi bên kia.
Bài hôm nay tập trung vào ba loại cardinality kinh điển: One-to-One (1:1), One-to-Many (1:N)Many-to-Many (M:N). (Cách vẽ các ký hiệu này — Crow's Foot, Chen, UML — sẽ học kỹ ở Bài 8; ở đây ta tập trung vào hiểu bản chất.)

One-to-One (1:1) — Mỗi A có đúng một B

Quan hệ 1:1 nghĩa là: mỗi bản ghi ở entity A liên kết với tối đa một bản ghi ở entity B, và ngược lại mỗi bản ghi B cũng chỉ liên kết với tối đa một A.

Ví dụ kinh điển: Nhân viênHồ sơ lương. Mỗi nhân viên có đúng một hồ sơ lương, và mỗi hồ sơ lương thuộc về đúng một nhân viên. Hoặc Công dânSố CCCD (Căn cước công dân): một người có đúng một số CCCD, một số CCCD chỉ ứng với một người.

Câu hỏi nhiều bạn đặt ra: nếu chỉ là 1:1, tại sao không gộp luôn vào một bảng? Đúng, trong nhiều trường hợp bạn nên gộp. Nhưng có ba lý do chính đáng để tách thành 1:1:

  • Tách dữ liệu nhạy cảm: ví dụ tách Người dùngThông tin thanh toán để áp dụng quyền truy cập và mã hóa khác nhau.
  • Tách dữ liệu ít dùng / cồng kềnh: ví dụ Sản phẩmMô tả chi tiết (văn bản dài, hình ảnh) — tách ra để bảng chính nhẹ và truy vấn nhanh.
  • Quan hệ tùy chọn (optional): không phải nhân viên nào cũng có Thông tin hợp đồng bảo hiểm, nên tách ra cho gọn.

One-to-Many (1:N) — Mỗi A có nhiều B, mỗi B thuộc một A

Đây là loại quan hệ phổ biến nhất trong thực tế, chiếm có lẽ 80% các mối quan hệ bạn gặp. Một bản ghi A liên kết với nhiều bản ghi B, nhưng mỗi bản ghi B chỉ thuộc về một A.

Ví dụ: một Khách hàng có nhiều Đơn hàng, nhưng mỗi đơn hàng chỉ thuộc về một khách hàng. Một Tỉnh/Thành có nhiều Quận/Huyện, nhưng mỗi quận chỉ nằm trong một tỉnh. Một Tác giả viết nhiều Bài viết, nhưng (giả định) mỗi bài viết do một tác giả.

Cách triển khai 1:N rất quan trọng — và nó là điều bạn phải nắm: đặt khóa ngoại (foreign key) ở phía "many". Bảng Đơn hàng sẽ chứa cột customer_id trỏ ngược về bảng Khách hàng. Logic ở đây: vì mỗi đơn hàng chỉ thuộc một khách, ta lưu được "khách nào" ngay trong dòng đơn hàng. Còn nếu cố lưu danh sách đơn hàng trong bảng khách hàng thì sẽ vi phạm chuẩn hóa (một ô không nên chứa nhiều giá trị).

Many-to-Many (M:N) — Nhiều A liên kết nhiều B

Quan hệ M:N: một bản ghi A liên kết với nhiều B, một bản ghi B cũng liên kết với nhiều A.

Ví dụ: một Sinh viên đăng ký nhiều Môn học, và một Môn học có nhiều Sinh viên ghi danh. Hoặc một Đơn hàng chứa nhiều Sản phẩm, và một Sản phẩm xuất hiện trong nhiều đơn hàng.

Điểm mấu chốt mà mọi BA phải khắc cốt ghi tâm: cơ sở dữ liệu quan hệ KHÔNG triển khai trực tiếp được M:N. Bạn không thể nhét nhiều khóa ngoại vào một ô. Giải pháp là tạo ra một bảng trung gian (junction table / associative entity / bridge table), bảng này "bẻ" M:N thành hai quan hệ 1:N.

Ví dụ giữa Sinh viênMôn học, ta tạo bảng Đăng_ký chứa tối thiểu hai cột student_idcourse_id. Mỗi dòng trong Đăng_ký là một lần một sinh viên đăng ký một môn. Vẻ đẹp của bảng trung gian là nó thường còn mang theo thuộc tính riêng của mối quan hệ: ngày đăng ký, điểm số, trạng thái... Những thuộc tính này không thuộc về sinh viên cũng không thuộc về môn học, mà thuộc về sự kiện đăng ký — và đó chính xác là chỗ nó cần nằm.

Tình huống thực tế

Tình huống 1: Tiki và bài toán đơn hàng đa sản phẩm

Giả sử bạn là BA cho một sàn thương mại điện tử kiểu Tiki. Sản phẩm đầu tiên team backend xây dựng có một bảng orders với cột product_namequantity. Khi khách mua 5 món trong một đơn, nhân viên nhập liệu phải tạo... 5 dòng đơn hàng riêng, hoặc nhồi tên 5 sản phẩm vào một ô ngăn cách bằng dấu phẩy.

Hậu quả: khi giám đốc hỏi "tháng này sản phẩm nào bán chạy nhất", không ai trả lời được vì dữ liệu sản phẩm bị chôn trong chuỗi text. Báo cáo doanh thu theo sản phẩm là bất khả thi.

Diễn giải: bản chất đây là quan hệ M:N giữa Đơn hàngSản phẩm — một đơn có nhiều sản phẩm, một sản phẩm bán trong nhiều đơn. Lời giải là tạo bảng trung gian order_items với các cột order_id, product_id, quantity, unit_price. Lưu ý cột unit_price (giá tại thời điểm mua) nằm ở bảng trung gian, vì giá sản phẩm có thể thay đổi theo thời gian; ta cần "đóng băng" giá lúc khách đặt.

Bài học rút ra: khi thấy hiện tượng nhồi nhiều giá trị vào một ô hoặc nhân bản dòng dữ liệu, gần như chắc chắn bạn đang đối mặt một quan hệ M:N chưa được mô hình hóa đúng.

Tình huống 2: Ngân hàng và quan hệ 1:1 vì lý do bảo mật

Một ngân hàng số tại Việt Nam (giả định tên là "VietBank Digital") thiết kế hệ thống KYC (định danh khách hàng). Họ có entity Khách hàng chứa tên, số điện thoại, email. Câu hỏi đặt ra với đội thiết kế: thông tin ảnh CCCD, ảnh chân dung, kết quả xác thực sinh trắc học — để đâu?

Đội ban đầu định gộp hết vào bảng Khách hàng. Nhưng BA phụ trách compliance phản đối: dữ liệu sinh trắc học cực kỳ nhạy cảm theo Nghị định bảo vệ dữ liệu cá nhân, cần mã hóa riêng, lưu ở vùng truy cập hạn chế, và chỉ một số nhân viên được phép xem.

Diễn giải: giải pháp là tách thành bảng Khách hàng và bảng Hồ sơ KYC theo quan hệ 1:1. Mỗi khách có đúng một hồ sơ KYC, mỗi hồ sơ thuộc một khách. Bảng KYC nằm trong schema riêng với quyền truy cập siết chặt. Khóa chính của bảng KYC chính là customer_id (vừa là PK vừa là FK — một dấu hiệu kinh điển của quan hệ 1:1).

Bài học rút ra: quan hệ 1:1 không phải lúc nào cũng "thừa thãi gộp được". Nó là công cụ kiến trúc để tách mối quan tâm về bảo mật, hiệu năng và tính tùy chọn của dữ liệu.

Tình huống 3: Grab và quan hệ 1:N tài xế - chuyến xe

Hình dung hệ thống đặt xe kiểu Grab. Bạn có entity Tài xế và entity Chuyến xe. Một tài xế thực hiện nhiều chuyến trong ngày, nhưng mỗi chuyến xe chỉ do một tài xế đảm nhận tại một thời điểm.

Đội thiết kế đặt khóa ngoại driver_id trong bảng Chuyến xe (phía "many"). Nhờ vậy, hệ thống dễ dàng truy vấn "hôm nay tài xế Nguyễn Văn A chạy bao nhiêu chuyến, doanh thu bao nhiêu" — chỉ cần lọc bảng Chuyến xe theo driver_id.

Nhưng có một cái bẫy. Khi mở rộng sang tính năng "đánh giá", một BA non kinh nghiệm định thêm cột rating vào bảng Tài xế. Sai lầm: mỗi chuyến có một đánh giá riêng từ khách. Đánh giá là thuộc tính của chuyến xe, không phải của tài xế. Đặt sai chỗ thì điểm trung bình tài xế phải tính toán lại mỗi lần và mất lịch sử từng chuyến.

Diễn giải: đánh giá ratingreview_comment thuộc về bảng Chuyến xe. Điểm trung bình của tài xế là kết quả tính toán (aggregate) từ tất cả chuyến của họ, không lưu thô.

Bài học rút ra: trong quan hệ 1:N, hãy luôn hỏi "thuộc tính này phụ thuộc vào cái nào?" Đặt nó vào đúng entity mà nó mô tả.

Hướng dẫn từng bước

Khi đứng trước hai entity bất kỳ và cần xác định quan hệ, hãy làm theo quy trình sau:

Bước 1 — Đặt câu hỏi hai chiều. Luôn hỏi theo cả hai hướng:

  • "Một A có thể liên kết với bao nhiêu B?"
  • "Một B có thể liên kết với bao nhiêu A?"
Ví dụ với Phòng banNhân viên: Một phòng ban có nhiều nhân viên (many). Một nhân viên thuộc một phòng ban (one). → Kết luận 1:N.

Bước 2 — Ghép kết quả thành loại quan hệ.

  • one ↔ one → 1:1
  • one ↔ many → 1:N
  • many ↔ many → M:N
Bước 3 — Xác định tính tùy chọn (optionality). Quan hệ có bắt buộc không? "Mỗi đơn hàng phải có khách hàng" (bắt buộc) khác với "Mỗi khách hàng có thể chưa có đơn nào" (tùy chọn). Điều này ảnh hưởng đến việc khóa ngoại có cho phép NULL hay không.

Bước 4 — Quyết định cách triển khai.

  • 1:N → đặt khóa ngoại ở phía "many".
  • M:N → tạo bảng trung gian, đặt hai khóa ngoại trỏ về hai bên.
  • 1:1 → đặt khóa ngoại ở một trong hai bên (thường ở bên phụ thuộc / tùy chọn), và đánh dấu UNIQUE để bảo đảm tính 1:1.
Bước 5 — Kiểm tra thuộc tính của mối quan hệ. Hỏi: "Có thông tin nào chỉ tồn tại khi hai entity này gặp nhau không?" (ngày đăng ký, số lượng, giá tại thời điểm đó...). Nếu có, đó là tín hiệu mạnh cho bảng trung gian — kể cả khi bạn tưởng đó là 1:N.

Bước 6 — Đặt tên quan hệ bằng động từ rõ nghĩa. "Khách hàng đặt Đơn hàng", "Sinh viên đăng ký Môn học". Tên động từ tốt giúp người đọc sơ đồ hiểu nghiệp vụ ngay lập tức.

Lỗi thường gặp & mẹo

Lỗi 1 — Quên rằng M:N cần bảng trung gian. Đây là lỗi phổ biến nhất. Bạn vẽ một đường nối thẳng giữa Sản phẩmDanh mục (một sản phẩm thuộc nhiều danh mục, một danh mục có nhiều sản phẩm) rồi để đó. Khi triển khai sẽ tắc. Mẹo: mỗi khi thấy M:N, lập tức nghĩ ngay đến bảng thứ ba.

Lỗi 2 — Nhầm 1:N thành M:N (hoặc ngược lại) vì không hỏi hai chiều. Nhiều bạn chỉ hỏi một hướng rồi vội kết luận. Mẹo: bắt buộc viết ra cả hai câu hỏi trước khi quyết định.

Lỗi 3 — Đặt khóa ngoại sai phía trong 1:N. Đặt order_id vào bảng Khách hàng thay vì customer_id vào bảng Đơn hàng. Mẹo: khóa ngoại luôn nằm ở phía "many".

Lỗi 4 — Bỏ qua thuộc tính của bảng trung gian. Tạo bảng Đăng_ký chỉ với hai khóa ngoại mà quên ngày_đăng_ký, điểm, trạng thái. Sau này muốn thêm thì phải sửa cấu trúc và migrate dữ liệu. Mẹo: ngay khi tạo bảng trung gian, hỏi "sự kiện này còn mang theo thông tin gì?"

Lỗi 5 — Lạm dụng 1:1 một cách vô lý. Tách bảng 1:1 khi không có lý do bảo mật/hiệu năng/tùy chọn chỉ làm tăng độ phức tạp truy vấn (phải JOIN). Mẹo: mặc định gộp; chỉ tách 1:1 khi có lý do rõ ràng.

Mẹo vàng: mỗi mối quan hệ nên đọc lên thành một câu tiếng Việt hoàn chỉnh có chủ ngữ - động từ - tân ngữ, kèm số lượng. Nếu bạn đọc không trôi, mô hình của bạn đang có vấn đề.

Bài tập thực hành

Hãy xác định loại quan hệ (1:1, 1:N hay M:N) cho từng cặp entity dưới đây, hỏi theo hai chiều, và nêu cách triển khai (khóa ngoại ở đâu / có cần bảng trung gian không):

  • Khách hàngĐịa chỉ giao hàng (một khách có thể lưu nhiều địa chỉ).
  • Hộ chiếuCông dân.
  • Bác sĩBệnh nhân trong một bệnh viện (bác sĩ khám nhiều bệnh nhân, bệnh nhân được nhiều bác sĩ khám trong các lần khác nhau).
  • Bài viết (blog) và Thẻ (tag).
  • Hóa đơnChi tiết hóa đơn.
Bài tập nâng cao: Cho hệ thống thư viện gồm Độc giả, Sách, Lượt mượn. Hãy vẽ (bằng lời) ba entity này, xác định quan hệ giữa chúng, và liệt kê các thuộc tính nên đặt ở bảng Lượt mượn. Gợi ý: một độc giả mượn nhiều cuốn theo thời gian, một cuốn sách được nhiều người mượn — bạn nghĩ Lượt mượn đóng vai trò gì?

(Gợi ý đáp án câu 1–5: 1 là 1:N, FK ở Địa chỉ; 2 là 1:1; 3 là M:N, cần bảng Lần khám; 4 là M:N, cần bảng Bài viết_Thẻ; 5 là 1:N, FK ở Chi tiết hóa đơn.)

Tóm tắt

Relationship là cách bạn biến những entity rời rạc thành một mô hình dữ liệu sống động phản ánh đúng nghiệp vụ. Ba loại cardinality nền tảng bạn cần thuộc nằm lòng:

  • 1:1 — mỗi bên đúng một bên kia; dùng để tách dữ liệu nhạy cảm, cồng kềnh hoặc tùy chọn; khóa ngoại đánh dấu UNIQUE.
  • 1:N — phổ biến nhất; khóa ngoại đặt ở phía "many"; nhớ đặt thuộc tính vào đúng entity mà nó mô tả.
  • M:N — không triển khai trực tiếp được; luôn cần bảng trung gian để bẻ thành hai quan hệ 1:N; bảng trung gian thường mang thuộc tính riêng của mối quan hệ.
Quy trình vàng: hỏi hai chiều → ghép thành loại quan hệ → xét tính tùy chọn → chọn cách triển khai → kiểm tra thuộc tính quan hệ → đặt tên động từ rõ nghĩa. Nắm chắc bài này, bạn đã có nửa cốt lõi của nghề mô hình hóa dữ liệu. Ở Bài 8, chúng ta sẽ học cách vẽ các quan hệ này một cách chuẩn xác bằng ký hiệu Crow's Foot, Chen và UML.