Thao tác đa bảng trong Django
Tạo model
Giả sử chúng ta có các khái niệm, trường dữ liệu và mối quan hệ sau:
Model tác giả: Một tác giả có tên và tuổi.
Model chi tiết tác giả: Lưu trữ thông tin chi tiết về tác giả bao gồm ngày sinh, số điện thoại, địa chỉ nhà. Quan hệ giữa tác giả và chi tiết tác giả là quan hệ một-một (one-to-one).
Model nhà xuất bản: Nhà xuất bản có tên, thành phố và email.
Model sách: Sách có tiêu đề và ngày xuất bản. Một cuốn sách có thể có nhiều tác giả, và một tác giả cũng có thể viết nhiều cuốn sách - đây là quan hệ nhiều-nhiều (many-to-many). Mỗi cuốn sách chỉ do một nhà xuất bản phát hành, nên quan hệ giữa nhà xuất bản và sách là một-nhiều (one-to-many).
Cấu trúc model như sau:
from django.db import models
class Author(models.Model):
id = models.AutoField(primary_key=True)
full_name = models.CharField(max_length=100)
age = models.IntegerField()
# Quan hệ một-một với AuthorDetail
detail = models.OneToOneField(
to="AuthorDetail",
on_delete=models.CASCADE
)
class AuthorDetail(models.Model):
id = models.AutoField(primary_key=True)
birthday = models.DateField()
phone = models.BigIntegerField()
address = models.CharField(max_length=200)
class Publisher(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=100)
city = models.CharField(max_length=50)
email = models.EmailField()
class Book(models.Model):
id = models.AutoField(primary_key=True)
title = models.CharField(max_length=100)
publish_date = models.DateField()
price = models.DecimalField(max_digits=6, decimal_places=2)
# Quan hệ một-nhiều với Publisher, khóa ngoại đặt ở bên nhiều
publisher = models.ForeignKey(
to="Publisher",
to_field="id",
on_delete=models.CASCADE
)
# Quan hệ nhiều-nhiều với Author
# ManyToManyField có thể đặt ở một trong hai model
writers = models.ManyToManyField(to='Author')
Kết quả tạo bảng:
Lưu ý:
- Tên bảng được tự động sinh theo định dạng
appname_modelname, có thể tùy chỉnh - Trường
idđược tự động thêm vào - Với khóa ngoại, Django thêm hậu tố
_idđể tạo tên cột trong database - Câu lệnh SQL trong ví dụ sử dụng cú pháp PostgreSQL, Django sẽ tự động điều chỉnh theo database được cấu hình trong settings
- Sau khi định nghĩa model, cần thêm tên ứng dụng vào INSTALLED_APPS trong settings
- ForeignKey có thể đặt
null=Trueđể cho phép giá trị NULL
Thêm dữ liệu vào bảng
Trước khi thao tác, cần nhập một số dữ liệu mẫu:
Bảng publisher:
Bảng author:
Bảng author_detail:
Quan hệ một-nhiều
# Cách 1: Truyền đối tượng Publisher
publisher_obj = Publisher.objects.get(id=1)
book_obj = Book.objects.create(
title="Bí Kíp Sinh Tồn",
publish_date="2020-05-15",
price=150,
publisher=publisher_obj
)
# Cách 2: Truyền id trực tiếp
book_obj = Book.objects.create(
title="Bí Kíp Sinh Tồn",
publish_date="2020-05-15",
price=150,
publisher_id=1
)
Giải thích: book_obj.publisher và `book_obj.publisher_id khác gì nhau?
# Tạo đối tượng sách mới
book_obj = Book.objects.create(
title="Người Săn Mồi",
price=250,
publish_date="2019-08-20",
publisher_id=2
)
# Lấy các đối tượng tác giả
author1 = Author.objects.filter(full_name="Minh").first()
author2 = Author.objects.filter(full_name="Huy").first()
# Liên kết quan hệ nhiều-nhiều
# Thêm tác giả vào tập hợp liên kết
book_obj.writers.add(author1, author2)
Kết quả trong database:
Bảng book:
Bảng book_writers:
Giải thích: book_obj.writers.all() trả về gì?
Các API khác cho quan hệ nhiều-nhiều:
book_obj.writers.remove() # Xóa một đối tượng cụ thể khỏi tập hợp
book_obj.writers.clear() # Xóa tất cả liên kết
book_obj.writers.set() # Xóa tất cả và thiết lập mới
Giới thiệu về Related Manager
Truy vấn cross-table dựa trên đối tượng
Truy vấn một-nhiều (Publisher và Book)
Truy vấn thuận chiều (theo trường: publisher):
# Tìm thành phố của nhà xuất bản cuốn sách có id=1
book_obj = Book.objects.filter(pk=1).first()
# book_obj.publisher là đối tượng Publisher liên kết
print(book_obj.publisher.city)
Truy vấn ngược chiều (theo tên bảng: book_set):
publisher = Publisher.objects.get(name="Nhà Xuất Bản Trẻ")
# publisher.book_set.all() lấy tất cả sách của nhà xuất bản này
book_list = publisher.book_set.all()
for book in book_list:
print(book.title)
Truy vấn một-một (Author và AuthorDetail)
Truy vấn thuận chiều (theo trường: detail):
author = Author.objects.filter(full_name="Minh").first()
print(author.detail.phone)
Truy vấn ngược chiều (theo tên bảng: author):
# Tìm tất cả tác giả ở Hà Nội
detail_list = AuthorDetail.objects.filter(address="Hà Nội")
for obj in detail_list:
print(obj.author.full_name)
Truy vấn nhiều-nhiều (Author và Book)
Truy vấn thuận chiều (theo trường: writers):
# Lấy tên và số điện thoại của tất cả tác giả cuốn sách "Bí Kíp Sinh Tồn"
book = Book.objects.filter(title="Bí Kíp Sinh Tồn").first()
authors = book.writers.all()
for author in authors:
print(author.full_name, author.detail.phone)
Truy vấn ngược chiều (theo tên bảng: book_set):
# Tìm tất cả sách của tác giả tên "Minh"
author = Author.objects.get(full_name="Minh")
book_list = author.book_set.all()
for book in book_list:
print(book.title)
Lưu ý:
Có thể tùy chỉnh tên FOO_set bằng tham số related_name trong ForeignKey và ManyToManyField:
publisher = models.ForeignKey(Publisher, related_name='bookList')
Khi đó:
# Tìm tất cả sách của nhà xuất bản Giáo Dục
publisher = Publisher.objects.get(name="Giáo Dục")
book_list = publisher.bookList.all()
Truy vấn cross-table sử dụng double underscore
Django cung cấp cách trực quan để biểu diễn mối quan hệ trong truy vấn, tự động tạo SQL JOIN. Sử dụng hai dấu gạch dưới để liên kết các trường giữa các model.
'''
Truy vấn thuận theo tên trường, ngược theo tên bảng viết thường
'''
Truy vấn một-nhiều
# Bài tập: Tìm tất cả sách của nhà xuất bản Trẻ
# Truy vấn thuận - theo trường publisher
query = Book.objects
.filter(publisher__name="Trẻ")
.values_list("title", "price")
# Truy vấn ngược - theo tên bảng book
query = Publisher.objects
.filter(name="Trẻ")
.values_list("book__title", "book__price")
Truy vấn nhiều-nhiều
# Bài tập: Tìm tất cả sách của tác giả tên "Minh"
# Truy vấn thuận - theo trường writers
query = Book.objects
.filter(writers__full_name="Minh")
.values_list("title")
# Truy vấn ngược - theo tên bảng book
query = Author.objects
.filter(full_name="Minh")
.values_list("book__title", "book__price")
Truy vấn một-một
# Tìm số điện thoại của tác giả tên "Huy"
# Truy vấn thuận
result = Author.objects.filter(full_name="Huy").values("detail__phone")
# Truy vấn ngược
result = AuthorDetail.objects.filter(author__full_name="Huy").values("phone")
Bài tập nâng cao (cross-table liên tiếp)
# Bài tập: Tìm tất cả sách của nhà xuất bản Giáo Dục và tên tác giả
# Truy vấn thuận
query = Book.objects
.filter(publisher__name="Giáo Dục")
.values_list("title", "writers__full_name")
# Truy vấn ngược
query = Publisher.objects
.filter(name="Giáo Dục")
.values_list("book__title", "book__writers__age", "book__writers__full_name")
# Bài tập: Tìm tất cả sách của tác giả có số điện thoại bắt đầu bằng 090
# Cách 1:
query = Book.objects
.filter(writers__detail__phone__startswith="090")
.values_list("title", "publisher__name")
# Cách 2:
result = Author.objects
.filter(detail__phone__startswith="090")
.values("book__title", "book__publisher__name")
related_name
Khi truy vấn ngược, nếu đã định nghĩa related_name, sử dụng related_name thay cho tên bảng:
publisher = models.ForeignKey(Publisher, related_name='bookList')
# Bài tập: Tìm giá sách của nhà xuất bản Giáo Dục
# Truy vấn ngược - dùng related_name thay vì tên bảng
query = Publisher.objects
.filter(name="Giáo Dục")
.values_list("bookList__title", "bookList__price")
Truy vấn tổng hợp và nhóm
Tổng hợp (Aggregate)
aggregate(*args, **kwargs)
# Tính giá trị trung bình của tất cả sách
from django.db.models import Avg
Book.objects.all().aggregate(Avg('price'))
# Kết quả: {'price__avg': 45.75}
aggregate() là một terminal clause của QuerySet, trả về dictionary chứa các cặp key-value. Key được tự động tạo từ tên trường và hàm aggregate. Có thể đặt tên tùy chỉnh:
Book.objects.aggregate(average_price=Avg('price'))
# Kết quả: {'average_price': 45.75}
Có thể tính nhiều aggregate cùng lúc:
from django.db.models import Avg, Max, Min
Book.objects.aggregate(Avg('price'), Max('price'), Min('price'))
# Kết quả: {'price__avg': 45.75, 'price__max': '200.00', 'price__min': '15.00'}
Nhóm (Group by)
##################################-- Truy vấn nhóm đơn bảng ##################################
Tìm mỗi phòng ban và số nhân viên
Employee:
id name age salary department
1 Linh 25 5000 Kỹ thuật
2 Nam 30 7000 Kinh doanh
3 Mai 28 6500 Kinh doanh
SQL:
select department, Count(*) from employee group by department
ORM:
Employee.objects.values("department").annotate(total=Count("id"))
##################################-- Truy vấn nhóm đa bảng ###########################
Tìm mỗi phòng ban và số nhân viên
Employee:
id name age salary department_id
1 Linh 25 5000 1
2 Nam 30 7000 2
3 Mai 28 6500 2
Department:
id name
1 Kỹ thuật
2 Kinh doanh
SQL:
select dept.name, Count(*) from employee left join department on employee.department_id=department.id group by department.id
ORM:
Department.objects.values("id").annotate(total=Count("employee")).values("name", "total")
class Employee(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
salary = models.DecimalField(max_digits=10, decimal_places=2)
department = models.CharField(max_length=50)
province = models.CharField(max_length=50)
annotate() tạo một giá trị thống kê riêng cho mỗi đối tượng trong QuerySet.
Tóm tắt: Truy vấn nhóm cross-table thực chất là join các bảng thành một rồi thực hiện nhóm theo cách thông thường.
Bài tập truy vấn
(1) Bài tập: Tìm sách rẻ nhất của mỗi nhà xuất bản
publishers = Publisher.objects.annotate(min_price=Min("book__price"))
for pub in publishers:
print(pub.name, pub.min_price)
annotate trả về QuerySet, có thể dùng values_list:
result = Publisher.objects
.annotate(min_price=Min("book__price"))
.values_list("name", "min_price")
print(result)
''
SELECT "app01_publisher"."name", MIN("app01_book"."price") AS "min_price"
FROM "app01_publisher"
LEFT JOIN "app01_book" ON ("app01_publisher"."id" = "app01_book"."publisher_id")
GROUP BY "app01_publisher"."id"
'''
(2) Bài tập: Đếm số tác giả của mỗi sách
books = Book.objects.annotate(author_count=Count('writers__full_name'))
(3) Đếm số tác giả của mỗi sách bắt đầu bằng "Python":
result = Book.objects
.filter(title__startswith="Python")
.annotate(author_count=Count('writers'))
(4) Tìm sách có nhiều hơn một tác giả:
result = Book.objects
.annotate(author_count=Count('writers'))
.filter(author_count__gt=1)
(5) Sắp xếp theo số lượng tác giả:
Book.objects.annotate(author_count=Count('writers')).order_by('author_count')
(6) Tìm tổng giá sách của mỗi tác giả:
# Group by theo tất cả các trường của bảng author
result = Author.objects
.annotate(total_price=Sum("book__price"))
.values_list("full_name", "total_price")
print(result)
F Query và Q Query
F Query
Trong tất cả các ví dụ trên, các bộ lọc chỉ so sánh trường với một hằng số. Để so sánh hai trường trong cùng một model, sử dụng F().
F() cho phép tham chiếu đến giá trị của các trường khác trong cùng một model.
# Tìm sách có số lượt bình luận ít hơn số lượt lưu
from django.db.models import F
Book.objects.filter(comment_count__lt=F('bookmark_count'))
Django hỗ trợ các phép toán cộng, trừ, nhân, chia giữa F() và hằng số.
# Tìm sách có số lượt bình luận ít hơn 2 lần số lượt lưu
Book.objects.filter(comment_count__lt=F('bookmark_count') * 2)
Cập nhật cũng có thể sử dụng F(), ví dụ tăng giá sách lên 30%:
Book.objects.all().update(price=F("price") + (F("price") * 30 / 100))
Q Query
Các tham số trong filter() mặc định kết hợp với nhau bằng "AND". Để thực hiện truy vấn phức tạp hơn (như OR), sử dụng Q object.
from django.db.models import Q
Q(title__startswith='Python')
Q object có thể kết hợp bằng toán tử & và | để tạo Q object mới.
book_list = Book.objects.filter(
Q(writers__full_name="Minh") | Q(writers__full_name="Huy")
)
Tương đương với SQL:
WHERE full_name = "Minh" OR full_name = "Huy"
Có thể kết hợp & và | với dấu ngoặc để tạo Q object phức tạp. Q object cũng có thể sử dụng toán tử ~ để phủ định:
book_list = Book.objects.filter(
Q(writers__full_name="Minh") & ~Q(publish_date__year=2020)
).values_list("title")
Có thể kết hợp Q object với tham số keyword. Tất cả tham số (keyword hoặc Q) đều được AND với nhau. Tuy nhiên, Q object phải đứng trước keyword arguments:
book_list = Book.objects.filter(
Q(publish_date__year=2019) | Q(publish_date__year=2020),
title__icontains="python"
)