Khi một model trỏ tới model khác bằng ForeignKey, Django cần biết hai điều: gọi ngược từ bản ghi cha sang danh sách bản ghi con bằng tên nào, và khi bản ghi cha bị xóa thì bản ghi con sẽ ra sao. Đặt related_name rõ nghĩa và chọn on_delete đúng giúp code dễ đọc và tránh xóa nhầm dữ liệu. Bài này dùng ví dụ Author – Book để làm rõ cả hai lựa chọn.
Vấn đề
Giả sử bạn có model Author và Book, mỗi cuốn sách thuộc về một tác giả. Cách viết thường gặp là chỉ khai báo ForeignKey với on_delete=models.CASCADE mà không đặt related_name. Code vẫn chạy, nhưng hai rắc rối sẽ xuất hiện dần. Thứ nhất, muốn lấy sách của một tác giả, bạn phải dùng tên mặc định mà Django tự sinh là book_set, ví dụ author.book_set.all(). Tên này khó đọc, và càng tệ hơn khi một model có hai ForeignKey cùng trỏ tới một model khác — lúc đó Django báo xung đột tên truy vấn ngược. Thứ hai, CASCADE nghĩa là xóa tác giả sẽ xóa toàn bộ sách của tác giả đó. Với quan hệ như Order – OrderItem thì đúng, nhưng với Author – Book thì thường không: bạn muốn giữ lại sách, chỉ bỏ liên kết tác giả.
class Author(models.Model): name = models.CharField(max_length=100)class Book(models.Model): author = models.ForeignKey(Author, on_delete=models.CASCADE) title = models.CharField(max_length=200)
Giải pháp
Cách sửa rất gọn: đặt related_name thành danh từ số nhiều rõ nghĩa, và chọn on_delete theo câu hỏi ‘bản ghi con còn ý nghĩa không khi bản ghi cha mất đi?’ Với Author – Book, đáp án thường là SET_NULL kèm null=True, để sách còn lại nhưng không còn tác giả.
class Author(models.Model): name = models.CharField(max_length=100)class Book(models.Model): author = models.ForeignKey( Author, on_delete=models.SET_NULL, null=True, related_name="books", ) title = models.CharField(max_length=200)
Từ giờ, author.books.all() đọc đúng như tiếng Việt: ‘sách của tác giả’. Nhờ related_name, bạn còn lọc ngược được: Author.objects.filter(books__title__icontains=’django’) sẽ tìm tác giả có sách chứa từ django. Ngoài SET_NULL, hãy nhớ ba lựa chọn hay dùng khác. CASCADE xóa con theo cha, hợp với chi tiết đơn hàng hay bình luận. PROTECT chặn xóa cha khi còn con, hợp với danh mục còn sản phẩm. SET_DEFAULT gán về giá trị mặc định đã khai báo. Riêng DO_NOTHING thì gần như không nên dùng: cơ sở dữ liệu vẫn có ràng buộc khóa ngoại và sẽ báo lỗi khi bạn xóa bản ghi cha.
Áp dụng vào dự án của bạn
Vào dự án đang chạy, hãy rà soát toàn bộ ForeignKey và OneToOneField: liệt kê từng quan hệ, tự hỏi bản ghi con còn ý nghĩa không khi cha mất, rồi chốt on_delete và related_name theo đáp án đó. Đổi hai thuộc tính này chỉ tạo migration AlterField, không tự xóa dữ liệu — nhưng bạn vẫn nên sao lưu và thử xóa bản ghi cha trên môi trường staging trước, vì nếu chọn CASCADE thì một lần xóa nhầm là mất cả chùm bản ghi con. Đặt related_name theo danh từ số nhiều (books, comments, items) và giữ nhất quán toàn dự án.
Bài viết liên quan
- Django: sửa lỗi N+1 query bằng select_related trong 5 phút
- Django: sửa lỗi N+1 query với prefetch_related cho quan hệ ManyToMany và quan hệ ngược
- Django: viết form nhanh gấp đôi bằng ModelForm — khỏi map tay từng field
Câu hỏi thường gặp
Không đặt related_name thì sao?
Django tự tạo tên truy vấn ngược dạng <model>_set, ví dụ book_set, nên code vẫn chạy. Tên này khó đọc, và nếu hai ForeignKey cùng trỏ tới một model mà không đặt related_name khác nhau, Django sẽ báo xung đột và từ chối chạy.
Dùng SET_NULL mà quên null=True thì sao?
Migration sẽ lỗi vì cột trong cơ sở dữ liệu không cho phép giá trị NULL, trong khi SET_NULL cần ghi NULL vào đó. Cách sửa là thêm null=True, và thêm blank=True nếu bạn dùng form hay trang admin.
Đổi on_delete hay related_name ở dự án đang chạy có mất dữ liệu không?
Không, việc đổi hai thuộc tính này chỉ tạo migration sửa định nghĩa quan hệ, không tự xóa bản ghi nào. Rủi ro chỉ đến sau đó, khi bạn xóa bản ghi cha mà quan hệ đang đặt CASCADE.
💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.
