Trong phần này, chúng ta sẽ tạo chức năng cho phép user theo dõi (follow) các user khác tương tự như Facebook, Twitter và các mạng xã hội khác cho ứng dụng của chúng ta.
Để giúp cho bạn dễ theo dõi, sau đây là danh sách các bài viết trong loạt bài hướng dẫn này:
- Phần 1: Hello, World
- Phần 2: Tìm hiểu về template
- Phần 3: Tìm hiểu về Web Forms
- Phần 4: Sử dụng cơ sở dữ liệu
- Phần 5: Xử lý đăng nhập
- Phần 6: Hồ sơ cá nhân và ảnh đại diện
- Phần 7: Xử lý lỗi
- Phần 8: Tạo chức năng follower (Bài viết này)
- Phần 9: Phân trang
- Phần 10: Hỗ trợ email
- Phần 11: Nâng cấp giao diện
- Phần 12: Xử lý thời gian
- Phần 13: Hỗ trợ đa ngôn ngữ
- Phần 14: Sử dụng Ajax
- Phần 15: Tinh chỉnh cấu trúc ứng dụng
- Phần 16: Hỗ trợ tìm kiếm
- Phần 17: Triển khai ứng dụng trên Linux
- Phần 18: Triển khai ứng dụng với Heroku
- Phần 19: Triển khai ứng dụng với Docker
- Phần 20: JavaScript nâng cao
- Phần 21: Thông báo cho người sử dụng
- Phần 22: Tìm hiểu về tác vụ nền
- Phần 23: Xây dựng API
Bạn có thể truy cập mã nguồn cho phần này tại GitHub.
Với chức năng này, chúng ta muốn để cho các user dễ dàng chọn lựa user nào mà họ muốn theo dõi. Vì vậy, chúng ta sẽ trở lại với thiết kế cơ sở dữ liệu và thay đổi nó để thiết lập quan hệ giữa các user, điều này khó hơn là bạn nghĩ.
Quan hệ trong cơ sở dữ liệu
Để xây dựng chức năng này, chúng ta cần có danh sách của các user đang được theo dõi cũng như các user đang theo dõi một user nào đó. Tuy nhiên, các hệ thống cơ sở dữ liệu quan hệ không có một cấu trúc dữ liệu có sẵn cho kiểu dữ liệu này. Chúng chỉ có các bảng (table) chứa các bảng ghi (record) và mối liên hệ giữa các record.
Hiện giờ trong cơ sở dữ liệu có một bảng chứa dữ liệu về user, vì vậy chúng ta phải tìm cách để định nghĩa một loại quan hệ có thể mô hình hóa liên hệ giữa người được theo dõi và người theo dõi. Để làm điều này, chúng ta cần ôn lại một chút về các loại quan hệ trong mô hình quan hệ dữ liệu:
Một-Nhiều (One-to-Many)
Chúng ta đã sử dụng loại quan hệ này trong Phần 4 như trong sơ đồ dưới đây:
Trong sơ đồ trên, chúng ta có hai thực thể users và post. Trong đó, một user có thể có nhiều post (bài viết) và mỗi post được tạo ra bởi một user. Loại quan hệ này được gọi là quan hệ một-nhiều và được xác lập trong cơ sở dữ liệu bằng cách sử dụng một foreign key (khóa ngoại) ở phía “nhiều”. Trong kiểu quan hệ này, khóa ngoại là trường user_id
được thêm vào trong bảng posts. Trường này sẽ liên kết các bảng ghi post và các bảng ghi tương ứng trong bảng user.
Rõ ràng là trường user_id
cho phép xác định tác giả từ một bài viết, nhưng chiều ngược lại thì thế nào? Trong trường hợp này, trường user_id
trong bảng post
cũng sẽ giúp chúng ta trong việc tìm kiếm tất cả các post được viết ra bởi một user thông qua các truy vấn kiểu như “tìm tất cả các post từ user có user_id là X”.
Nhiều-Nhiều (Many-to-Many)
So với các mối quan hệ một-nhiều, các mối quan hệ nhiều-nhiều phức tạp hơn một chút. Ví dụ như trong trường hợp của giáo viên
và học sinh
. Chúng ta có thể nói rằng một học sinh có nhiều giáo viên, và một giáo viên có nhiều học sinh. Điều này giống như là chúng ta có hai quan hệ một-nhiều từ cả hai phía giáo viên và học sinh.
Đối với các quan hệ loại này, các bảng và quan hệ giữa các bảng phải được thiết kế để có thể trả lời được những truy vấn cơ bản như tìm danh sách các giáo viên đang dạy cho một học sinh nào đó, và danh sách của các học sinh trong một lớp của một giáo viên nào đó. Việc hiểu và thiết kế cho kiểu quan hệ này rất quan trọng bởi vì nó được ứng dụng trong nhiều mô hình dữ liệu phức tạp. Và chúng ta không thể áp dụng cách thiết kế sử dụng khóa ngoại trong một bảng duy nhất như đối với mối quan hệ một-nhiều mà chúng ta đã biết
Để biểu diễn mối quan hệ nhiều-nhiều trong cơ sở dữ liệu, chúng ta cần sử dụng thêm một bảng phụ gọi là bảng kết hợp (association table). Sau đây là mô hình các bảng liên quan cho ví dụ giáo viên và học sinh mà chúng ta đang tìm hiểu:
Thoạt nhìn thì cách thiết kế này không rõ ràng lắm, nhưng hai khóa ngoại trong bảng kết hợp là câu trả lời cho các truy vấn về quan hệ giữa học sinh và giáo viên.
Nhiều-Một và Một-Một
Quan hệ nhiều-một tương tự như một-nhiều. Điều khác nhau duy nhất là trong mối quan hệ này, chúng ta truy vấn từ phía “nhiều”.
Quan hệ một-một là trường hợp đặc biệt của quan hệ một-nhiều. Cách thiết kế các bảng cũng gần giống nhau, nhưng với một ngoại lệ là chúng ta sẽ thêm một ràng buộc vào cơ sở dữ liệu để ngăn ngừa trường hợp phía “nhiều” có nhiều hơn một liên kết. Trong thực tế, kiểu quan hệ này cũng được sử dụng nhưng không phổ biến như các kiểu quan hệ khác.
Thiết kế cơ sở dữ liệu để tạo chức năng người theo dõi (Follower)
Từ định nghĩa về các kiểu quan hệ, chúng ta có thể dễ dàng nhận ra mô hình dữ liệu thích hợp giữa người theo dõi và người được theo dõi là quan hệ nhiều-nhiều bởi vì một user có thể theo dõi nhiều user khác, đồng thời một user cũng có nhiều người khác theo dõi mình. Tuy nhiên, so với ví dụ về giáo viên – học sinh ở trên, chúng ta thấy có một điểm khác biệt: trong ví dụ về giáo viên – học sinh, chúng ta có hai thực thể là giáo viên và học sinh, nhưng trong trường hợp này, cả người theo dõi và người được theo dõi đều là user. Vậy thì làm thế nào chúng ta xác định thực thể thứ hai trong quan hệ nhiều-nhiều?
Thực thể thứ hai trong quan hệ này cũng là user. Các mối quan hệ mà một thực thể được liên kết với chính nó được gọi là quan hệ với chính nó (self-referential relationship). Và đây là kiểu quan hệ chúng ta mà chúng ta sẽ sử dụng.
Sau đây là mô hình biểu diễn cho quan hệ nhiều-nhiều và đồng thời là quan hệ với chính nó của các follower:
Bảng followers
là bảng liên kết trong quan hệ. Các khóa ngoại trong bảng này đều tham chiếu đến bảng user bởi vì nó liên kết giữa các user. Mỗi bảng ghi trong bảng này đại diện cho một liên kết giữa một người theo dõi và một người được theo dõi. Như trong ví dụ về giáo viên – học sinh, cách thiết kế này cho phép cơ sở dữ liệu trả lời các truy vấn về người theo dõi và người được theo dõi.
Mô hình dữ liệu
Chúng ta cần đưa các thông tin về người theo dõi vào cơ sở dữ liệu. Sau đây là bảng kết hợp followers
:
app/models.py: Bảng kết hợp followers
1 2 3 4 |
followers = db.Table('followers', db.Column('follower_id', db.Integer, db.ForeignKey('user.id')), db.Column('followed_id', db.Integer, db.ForeignKey('user.id')) ) |
Chúng ta diễn dịch trực tiếp từ mô hình biểu diễn được minh họa ở trên. Lưu ý rằng chúng ta không khai báo bảng này là một mô hình dữ liệu như đối với các bảng users và posts. Bởi vì đây là một bảng phụ và không có dữ liệu nào khác ngoài các khóa ngoại, chúng ta sẽ tạo ra nó mà không dùng lớp mô hình dữ liệu tương ứng.
Tiếp theo, chúng ta sẽ khai báo quan hệ nhiều-nhiều trong bảng user:
app/models.py: Quan hệ nhiều-nhiều giữa người theo dõi và người được theo dõi
1 2 3 4 5 6 7 |
class User(UserMixin, db.Model): ... followed = db.relationship( 'User', secondary=followers, primaryjoin=(followers.c.follower_id == id), secondaryjoin=(followers.c.followed_id == id), backref=db.backref('followers', lazy='dynamic'), lazy='dynamic') |
Việc thiết lập các quan hệ rất quan trọng. Tương tự như trong trường hợp của quan hệ một nhiều giữa user và post, chúng ta sử dụng hàm db.relationship
để định nghĩa quan hệ giữa các lớp mô hình dữ liệu. Ở đây, quan hệ này sẽ liên kết một User
với các User
khác và là quan hệ hai ngôi – gồm có một thực thể bên trái là một user và một hoặc nhiều thực thể bên phải cũng là user. Vì vậy chúng ta có thể quy ước rằng với mỗi cặp user trong quan hệ này, user ở phía trái đang theo dõi user ở phía phải. Và do đó, khi chúng ta truy vấn dữ liệu từ phía trái, chúng ta sẽ được một danh sách các user đang được theo dõi
(hay followed
). Để rõ ràng hơn, chúng ta sẽ phân tích chi tiết các tham số đã dùng trong đoạn mã trên:
- ‘
User
’ là thực thể bên phải của mối quan hệ (thực thể bên trái là lớp cha). Bởi vì đây là một quan hệ với chính nó (self-referential), chúng ta sẽ dùng cùng một lớp mô hình dữ liệu cho cả hai phía. secondary
chỉ định bảng liên kết (association table) cho quan hệ – bảng followers mà chúng ta vừa định nghĩa ở trên.primaryjoin
chỉ định điều kiện để liên kết thực thể bên trái (các follower hay người theo dõi) với bảng liên kết. Điều kiện liên kết cho bên trái của quan hệ là ID của user trùng với giá trị của trườngfollower_id
trong bảng liên kết. Biểu thứcfollowers.c.follower_id
tham chiếu đến cộtfollower_id
trong bảng liên kết.secondaryjoin
chỉ định điều kiện để liên kết thực thể bên phải (followed user hay người được/bị theo dõi) với bảng liên kết. Điều kiện liên kết cũng tương tự như trong trường hợp củaprimaryjoin
. Điểm khác nhau duy nhất ở đây là chúng ta sử dụng khóa ngoại thứ hai trong bảng liên kết là trườngfollowed_id
.backref
định nghĩa cách truy cập quan hệ từ thực thể bên phải. Bởi vì chúng ta quy ước các thực thể ở bên trái của mối quan hệ là người theo dõi, các thực thể ở bên trái sẽ truy cập các thực thể ở bên phải sẽ theo quan hệtheo dõi
(followed
). Ngược lại, các thực thể ở bên phải sẽ truy cập các thực thể bên trái theo quan hệ đượctheo dõi
(follower
).lazy
chỉ định chế độ thực thi cho truy vấn này. Thiết lậpdynamic
sẽ chỉ chạy truy vấn khi có yêu cầu, tương tự như trong trường hợp của truy vấn giữa user và post mà chúng ta đã thực hiện trước đây.
Đừng lo nếu bạn chưa hiểu rõ các thiết lập này. Chúng ta sẽ xem nó hoạt động như thế nào và bạn sẽ thấy rõ hơn.
Đã đến lúc chúng ta cập nhật cơ sở dữ liệu với các thay đổi này:
1 2 3 4 5 6 7 8 9 10 11 12 |
(myenv) $ flask db migrate -m "followers" [2019-09-14 10:08:51,449] INFO in __init__: Myblog startup INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'followers' Generating /home/thaipt/Works/Flask/myblog/migrations/versions/26a2421aa8b7_followers.py ... done (myenv) $ flask db upgrade [2019-09-14 10:10:04,338] INFO in __init__: Myblog startup INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade 18e92e3cb029 -> 26a2421aa8b7, followers |
Thêm và bớt người theo dõi.
Nhờ SQLAlchemy, một user theo dõi một user khác có thể được lưu lại trong cơ sở dữ liệu với quan hệ followed
như là một danh sách. Ví dụ như nếu chúng ta có hai đối tượng user1
và user2
, chúng ta có thể mô tả việc user1
theo dõi user2
qua biểu thức đơn giản sau:
1 |
user1.followed.append(user2) |
Và để ngừng theo dõi, chúng ta có thể làm như sau:
1 |
user1.followed.remove(user2) |
Tuy nhiên, dù việc thêm và bớt các theo dõi tương đối dễ, chúng ta muốn làm cho mã của chúng ta có hệ thống và dễ sử dụng hơn thay vì dùng trực tiếp các hàm “append” và “remove” như trên. Vì vậy, chúng ta sẽ tạo ra các phương thức “follow” và “unfollow” trong mô hình User. Để giúp cho việc viết mã kiểm tra dễ dàng – điều mà chúng ta sẽ thực hiện trong phần sau – tốt nhất là chúng ta nên chuyển các logic của ứng dụng ra khỏi các hàm hiển thị và đưa vào các mô hình dữ liệu hoặc các lớp hay module phụ.
Sau đây là phần cập nhật trong mô hình user để thêm và bớt các quan hệ:
app/models.py: Thêm và bớt người theo dõi
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class User(UserMixin, db.Model): ... def follow(self, user): if not self.is_following(user): self.followed.append(user) def unfollow(self, user): if self.is_following(user): self.followed.remove(user) def is_following(self, user): return self.followed.filter( followers.c.followed_id == user.id).count() > 0 |
Các phương thức follow()
và unfollow()
sử dụng các hàm append()
và remove()
của đối tượng quan hệ như trong ví dụ ngắn mà chúng ta đã thử ở trên. Nhưng trước khi cập nhật các quan hệ, các phương thức này sẽ gọi một phương thức hỗ trợ là is_following()
để bảo đảm rằng việc cập nhật là chính xác. Ví dụ như nếu chúng ta thiết lập cho user1
theo dõi user2
, nhưng mối quan hệ này đã có sẵn trong cơ sở dữ liệu, chúng ta không muốn có dữ liệu trùng lặp. Quá trình chấm dứt theo dõi cũng theo logic như vậy.
Phương thức is_following()
sẽ tạo một truy vấn trên quan hệ followed
và kiểm tra nếu có một liên kết giữa hai user trong cơ sở dữ liệu. Trước đây, chúng ta đã sử dụng phương thức filter_by()
để tìm một user với username. Phương thức filter()
mà chúng ta sử dụng ở đây cũng tương tự như vậy, nhưng ở mức thấp hơn và cho phép chúng ta sử dụng các điều kiện lọc tùy ý chứ không như filter_by()
chỉ có thể dùng điều kiện so sánh với một giá trị không đổi. Điều kiện mà chúng ta sử dụng trong hàm is_following()
sẽ tìm các bản ghi trong bảng liên kết có khóa ngoại followed_id
trùng với ID của user hiện hành (self
) và khóa ngoại follower_id
trùng với ID của user được dùng làm tham số cho hàm này. Và truy vấn kết thúc với hàm count()
để trả về số bản ghi phù hợp. Số bản ghi chỉ có thể là 0 hoặc 1 trong trường hợp này, vì vậy kiểm tra số bản ghi là 1 hoặc lớn hơn 0 đều cho kết quả giống nhau. Các hàm khác có chức năng tương tự mà chúng ta đã dùng qua là all()
và first()
.
Tìm danh sách các bài viết (post) từ các user bị theo dõi
Chúng ta đã gần như hoàn tất các cấu trúc dữ liệu để hỗ trợ cho chức năng theo dõi, nhưng vẫn còn một đặc tính quan trọng. Trong trang chủ của ứng dụng, chúng ta cần hiển thị các bài được viết bởi các user đang được theo dõi bởi user đang đăng nhập. Do đó, chúng ta cần tạo thêm một truy vấn để tìm các bài viết này.
Giải pháp có vẻ rõ ràng nhất là sử dụng một truy vấn để lấy danh sách các user đang bị theo dõi – bằng cách dùng hàm user.followed.all()
. Sau đó, chúng ta sẽ chạy một truy vấn nữa với từng user trong danh sách này để tìm tất cả các bài viết. Sau khi chúng ta có tất cả các bài viết từ các user khác nhau, chúng ta sẽ tổng hợp thành một danh sách mới và sắp xếp theo thứ tự ngày tháng. Điều này có vẻ hợp lý phải không? Nhưng thật sự thì không chính xác là vậy.
Giải pháp này có một vài vấn đề. Điều gì sẽ xảy ra nếu một user đang theo dõi cả nghìn user khác? Lúc đó, chúng ta sẽ phải thực thi cả nghìn truy vấn khác nhau với cơ sở dữ liệu chỉ để lấy các bài viết. Và sau đó, chúng ta còn phải tổng hợp lại và sắp xếp cả nghìn danh sách các bài viết từ các kết quả của các truy vấn trước đó trong bộ nhớ. Vấn đề thứ hai là sau này chúng ta sẽ thực hiện chức năng phân trang (pagination) cho trang chủ của ứng dụng – có nghĩa là nó sẽ không hiển thị toàn bộ mà chỉ một số các bài viết mà thôi và một liên kết để giúp chúng ta truy cập các bài viết không được hiển thị. Nếu chúng ta hiển thị các bài viết được sắp xếp theo thứ tự ngày tháng, chúng ta phải lấy toàn bộ các bài viết và sắp xếp chúng trước. Do đó, đây là một ý tưởng tồi.
Thật ra thì không có cách nào để tránh việc tổng hợp và sắp xếp các bài viết theo thứ tự. Tuy nhiên, đây là công việc này nên được thực hiện bởi các cơ sở dữ liệu quan hệ thay vì ứng dụng vì đây là công việc chuyên môn của các hệ cơ sở dữ liệu – nhất là các cơ sở dữ liệu quan hệ bởi vì chúng có hệ thống chỉ mục (index). Điều này cho phép cơ sở dữ liệu thực hiện công việc này nhanh và hiệu quả hơn nhiều so với khi phải làm trong ứng dụng. Vì vậy, chúng ta cần tìm ra một truy vấn giúp chúng ta định nghĩa dữ liệu mà chúng ta cần, và để cho cơ sở dữ liệu dùng cách tốt nhất để lấy các thông tin này.
Sau đây là truy vấn mà chúng ta sẽ sử dụng:
app/models.py: Truy vấn để tìm các bài viết từ các user được theo dõi
1 2 3 4 5 6 7 |
class User(UserMixin, db.Model): ... def followed_posts(self): return Post.query.join( followers, (followers.c.followed_id == Post.user_id)).filter( followers.c.follower_id == self.id).order_by( Post.timestamp.desc()) |
Đây là truy vấn phức tạp nhất mà chúng ta đã sử dụng đến thời điểm này. Và chúng ta sẽ cố gắng để hiểu từng phần của truy vấn này. Nếu bạn quan sát kỹ cấu trúc của nó, bạn sẽ thấy có ba phần chính là các phương thức join()
, filter()
và order_by()
trong SQLAlchemy:
1 |
Post.query.join(...).filter(...).order_by(...) |
Liên kết bảng (Join)
Để hiểu rõ join là gì, chúng ta sẽ xem xét vài ví dụ. Giả sử bảng User
hiện giờ có các dữ liệu như sau:
id | username |
1 | thái |
2 | nguyên |
3 | long |
4 | huy |
Để đơn giản hóa, chúng ta sẽ không sử dụng đến các trường còn lại trong bảng này.
Tiếp theo, giả định rằng trong bảng kết hợp followers
, chúng ta có dữ liệu là user thái
đang theo dõi user nguyên
và huy
, user nguyên
đang theo dõi user long
và user long
đang theo dõi user huy
. Các dữ liệu này được tổ chức như sau:
follower_id | followed_id |
1 | 2 |
1 | 4 |
2 | 3 |
3 | 4 |
Và cuối cùng, bảng post chứa một post từ mỗi user:
id | text | user_id |
1 | post của nguyên | 2 |
2 | post của long | 3 |
3 | post của huy | 4 |
4 | post của thái | 1 |
Chúng ta cũng đơn giản hóa cấu trúc của bảng này tương tự như bảng user.
Chúng ta sẽ xem lại cách gọi hàm join() một lần nữa:
1 |
Post.query.join(followers, (followers.c.followed_id == Post.user_id)) |
Ở đây, chúng ta đang thực hiện join với bảng posts.Tham số thứ nhất là bảng liên kết followers, và tham số thứ hai là điều kiện để join.Điều chúng ta đang làm với hàm này là thông báo với cơ sở dữ liệu để tạo ra một bảng tạm thời có chứa dữ liệu từ cả hai bảng posts và followers. Các dữ liệu này sẽ được kết hợp theo điều kiện mà chúng ta đã đưa vào từ các tham số.
Điều kiện mà chúng ta sử dụng nói rằng trường followed_id
trong bảng followers phải trùng với trường user_id
trong bảng posts. Để thực hiện việc kết hợp này, cơ sở dữ liệu sẽ lấy mỗi bản ghi từ bảng post (phía trái của join) và kết hợp với tất cả các bản ghi từ bảng followers
(phía phải của join) thỏa điều kiện join. Nếu có nhiều bản ghi từ bản followers
phù hợp với điều kiện, bản ghi của post sẽ được lặp lại cho mỗi bản ghi từ followers. Nếu có một bản ghi post không có bản ghi phù hợp trong followers, bản ghi post đó không thuộc về join.
Trong dữ liệu từ ví dụ trên, kết quả của quá trình join như sau:
id | text | user_id | follower_id | followed_id |
1 | post của nguyên | 2 | 1 | 2 |
2 | post của long | 3 | 2 | 3 |
3 | post của huy | 4 | 1 | 4 |
3 | post của huy | 4 | 3 | 4 |
Chú ý là các giá trị từ hai cột user_id
và followed_id
luôn giống nhau trong mọi trường hợp vì đây là điều kiện cho join. Các bài viết từ user thái
không có trong kết quả này vì không có bản ghi nào trong followers mà trong đó thái
là user được theo dõi. Hay nói cách khác, không có ai theo dõi thái
. Ngược lại, có hai bài viết từ huy
bởi vì user này được hai user khác theo dõi.
Đến bây giờ, lý do chúng ta tạo ra join này vẫn chưa rõ ràng lắm, nhưng bạn sẽ hiểu rõ hơn khi đọc tiếp phần sau vì đây chỉ là một phần của một truy vấn phức tạp hơn.
Các bộ lọc (filter)
Việc thực hiện join sẽ trả về một danh sách tất cả các bài viết được các user theo dõi, tuy nhiên kết quả này có nhiều dữ liệu hơn những gì chúng ta cần. Chúng ta chỉ quan tâm đến một phần của các dữ liệu này, đó là danh sách các post mà một user nhất định đang theo dõi. Vì vậy, chúng ta cần cắt bớt những gì chúng ta không cần bằng cách gọi hàm filter()
.
Sau đây là bộ lọc trong truy vấn:
1 |
filter(followers.c.follower_id == self.id) |
Bởi vì truy vấn này là một hàm của lớp User
, biểu thức self.id
tham chiếu đến ID của user chúng ta cần tìm thông tin. Hàm filter()
sẽ chọn các dữ liệu trong bảng kết quả từ tác vụ join sao cho giá trị từ cột follower_id
trùng với giá trị ID của user này, hay nói cách khác, nó chỉ giữ lại các dữ liệu trong đó user này đóng vai trò follower.
Giả sử chúng ta đang tìm các bài viết mà thái
đang theo dõi với giá trị id
là 1. Khi đó bảng kết quả từ tác vụ join ở trên sẽ tương tự như sau:
id | text | user_id | follower_id | followed_id |
1 | post của nguyên | 2 | 1 | 2 |
3 | post của huy | 4 | 1 | 4 |
Đây là các bài viết mà chúng ta muốn tìm.
Lưu ý rằng truy vấn này được thực hiện với lớp Post
, vì vậy dù rằng việc truy vấn thực sự xảy ra và trả về kết quả trong bảng tạm thời từ cơ sở dữ liệu, chúng ta sẽ nhận được kết quả cuối cùng dưới dạng các post với các dữ liệu trong bảng này (nhưng không có các cột được thêm vào trong quá trình join).
Sắp xếp (Sorting)
Cuối cùng, chúng ta cần sắp xếp lại kết quả. Công việc này được thực hiện trong phần còn lại của đoạn mã truy vấn:
1 |
order_by(Post.timestamp.desc()) |
Đoạn mã này sẽ sắp xếp kết quả theo thứ tự thời gian từ mới đến cũ bằng cách sử dụng trường timestamp. Với thứ tự này, các bài viết mới nhất sẽ được sắp xếp trên cùng.
Kết hợp các bài viết của chính mình và của các user được follow
Truy vấn mà chúng ta sử dụng trong hàm followed_posts()
rất hữu dụng nhưng lại có một hạn chế: nếu user muốn thấy các bài viết của mình cùng với các bài viết của các user mà họ theo dõi, truy vấn không làm được điều này.
Có hai cách khả dĩ để làm cho truy vấn này trả về các bài viết của user hiện hành. Cách trực tiếp nhất là không thay đổi truy vấn, nhưng làm cho các user trở thành người theo dõi chính họ. Nếu bạn theo dõi chính mình, truy vấn này sẽ tìm tất cả bài viết của bạn cùng với các bài viết của các user mà bạn đang theo dõi. Điều bất tiện của phương pháp này là nó làm ảnh hưởng đến tình trạng của các follower: tất cả bộ đếm follower sẽ tăng lên một. Vì vậy chúng ta phải điều chỉnh con số này trước khi hiển thị. Cách thứ hai là tạo một truy vấn thứ hai để trả về các bài viết của chính mình và dùng toán tử union
(hợp) để gộp kết quả từ hai truy vấn thành một.
Chúng ta sẽ dùng phương pháp thứ hai. Vì vậy chúng ta cần điều chỉnh hàm followed_posts()
để như sau:
app/models.py: truy vấn cho các bài viết từ các user được theo dõi và từ chính mình
1 2 3 4 5 6 |
def followed_posts(self): followed = Post.query.join( followers, (followers.c.followed_id == Post.user_id)).filter( followers.c.follower_id == self.id) own = Post.query.filter_by(user_id=self.id) return followed.union(own).order_by(Post.timestamp.desc()) |
Bạn hãy lưu ý cách chúng ta gộp hai truy vấn followed và own trước khi sắp xếp chúng.
Thực hiện unit test cho mô hình dữ liệu User
Dù rằng chức năng follower mà chúng ta vừa xây dựng không phải quá phức tạp, nó là một chức năng tương đối quan trọng. Và khi chúng ta xây dựng các chức năng hay thành phần quan trọng, chúng ta cần bảo đảm rằng nó sẽ tiếp tục làm việc theo đúng thiết kết khi chúng ta cập nhật các phần khác nhau của ứng dụng sau này. Cách tốt nhất để làm điều này là viết các chương trình tự động kiểm tra (automated test) và thực hiện chúng mỗi khi chúng ta cập nhật ứng dụng.
Python có sẵn một gói rất hữu ích là unittest
cho mục đích này. Gói này giúp chúng ta viết và thực hiện các unit test. Trước hết, hãy viết vài unit test trong module test.py để kiểm tra các phương thức trong lớp User:
tests.py: Unit test cho mô hình dữ liệu User
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
from datetime import datetime, timedelta import unittest from app import app, db from app.models import User, Post class UserModelCase(unittest.TestCase): def setUp(self): app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' db.create_all() def tearDown(self): db.session.remove() db.drop_all() def test_password_hashing(self): u = User(username='nguyen') u.set_password('cat') self.assertFalse(u.check_password('dog')) self.assertTrue(u.check_password('cat')) def test_avatar(self): u = User(username='thai', email='thai@example.com') self.assertEqual(u.avatar(128), ('https://www.gravatar.com/avatar/' 'd4c74594d841139328695756648b6bd6' '?d=identicon&s=128')) def test_follow(self): u1 = User(username='thai', email='thai@example.com') u2 = User(username='nguyen', email='nguyen@example.com') db.session.add(u1) db.session.add(u2) db.session.commit() self.assertEqual(u1.followed.all(), []) self.assertEqual(u1.followers.all(), []) u1.follow(u2) db.session.commit() self.assertTrue(u1.is_following(u2)) self.assertEqual(u1.followed.count(), 1) self.assertEqual(u1.followed.first().username, 'nguyen') self.assertEqual(u2.followers.count(), 1) self.assertEqual(u2.followers.first().username, 'thai') u1.unfollow(u2) db.session.commit() self.assertFalse(u1.is_following(u2)) self.assertEqual(u1.followed.count(), 0) self.assertEqual(u2.followers.count(), 0) def test_follow_posts(self): # tạo ra 4 users u1 = User(username='thai', email='thai@example.com') u2 = User(username='nguyen', email='nguyen@example.com') u3 = User(username='long', email='long@example.com') u4 = User(username='huy', email='huy@example.com') db.session.add_all([u1, u2, u3, u4]) # tạo ra 4 post để kiểm tra now = datetime.utcnow() p1 = Post(body="post của thái", author=u1, timestamp=now + timedelta(seconds=1)) p2 = Post(body="post của nguyên", author=u2, timestamp=now + timedelta(seconds=4)) p3 = Post(body="post của long", author=u3, timestamp=now + timedelta(seconds=3)) p4 = Post(body="post của huy", author=u4, timestamp=now + timedelta(seconds=2)) db.session.add_all([p1, p2, p3, p4]) db.session.commit() # thiết lập các followers u1.follow(u2) # thái follows nguyên u1.follow(u4) # thái follows huy u2.follow(u3) # nguyên follows long u3.follow(u4) # long follows huy db.session.commit() # kiểm tra các bài viết được theo dõi từ các user f1 = u1.followed_posts().all() f2 = u2.followed_posts().all() f3 = u3.followed_posts().all() f4 = u4.followed_posts().all() self.assertEqual(f1, [p2, p4, p1]) self.assertEqual(f2, [p2, p3]) self.assertEqual(f3, [p3, p4]) self.assertEqual(f4, [p4]) if __name__ == '__main__': unittest.main(verbosity=2) |
Chúng ta xây dựng bốn chương trình kiểm tra cho các chức năng hash password, ảnh đại diện và chức năng follower trong mô hình dữ liệu user. Các phương thức setUp()
và tearDown()
là các phương thức đặc biệt trong framework của unit test. Các phương thức này sẽ được thực hiện lần lượt trước và sau khi các mã trong unit test được thực thi. Chúng ta cũng dùng một mẹo nhỏ trong setUp()
để tránh việc mã của unit test sử dụng dữ liệu từ cơ sở dữ liệu chính bằng cách thay đổi cấu hình ứng dụng trong đoạn mã trên thành sqlite://
để SQLAlchemy sử dụng cơ sở dữ liệu trong bộ nhớ (in-memory) khi chạy mã kiểm tra. Hàm db.create_all()
sẽ tạo ra các bảng cần thiết cho cơ sở dữ liệu này. Đây là mẹo để tạo ra cơ sở dữ liệu mới hoàn toàn và rất hữu ích cho quá trình test. Trong quá trình phát triển ứng dụng và sử dụng chính thức, chúng ta luôn luôn tạo ra các bảng từ quá trình chuyển đổi cơ sở dữ liệu mà chúng ta đã học ở các phần trước.
Bạn có thể chạy toàn bộ mã để test với lệnh sau:
1 2 3 4 5 6 7 8 9 10 |
(myenv) $ python tests.py test_avatar (__main__.UserModelCase) ... ok test_follow (__main__.UserModelCase) ... ok test_follow_posts (__main__.UserModelCase) ... ok test_password_hashing (__main__.UserModelCase) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.494s OK |
Từ bây giờ, mỗi lần chúng ta cập nhật ứng dụng, bạn có thể chạy lại mã kiểm tra để bảo đảm rằng các chức năng mà chúng ta đã kiểm tra không bị ảnh hưởng. Tương tự như vậy, mỗi khi chúng ta thêm một tính năng mới vào ứng dụng, chúng ta cũng nên viết mã để kiểm tra chúng.
Tích hợp chức năng theo dõi vào ứng dụng
Chúng ta đã thực hiện các công đoạn chuẩn bị cần thiết cho chức năng theo dõi trong cơ sở dữ liệu và mô hình dữ liệu, nhưng chúng ta vẫn chưa tích hợp chức năng này vào trong ứng dụng. Tin tốt là sẽ không có quá khó, việc tích hợp vẫn dựa trên các khái niệm quen thuộc mà chúng ta đã học.
Đầu tiên, chúng ta sẽ thêm hai địa chỉ mới vào ứng dụng để theo dõi (follow) và chấm dứt theo dõi (unfollow) một user:
app/routes.py: các địa chỉ để theo dõi và chấm dứt theo dõi
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
@app.route('/follow/<username>') @login_required def follow(username): user = User.query.filter_by(username=username).first() if user is None: flash('User {} not found.'.format(username)) return redirect(url_for('index')) if user == current_user: flash('You cannot follow yourself!') return redirect(url_for('user', username=username)) current_user.follow(user) db.session.commit() flash('You are following {}!'.format(username)) return redirect(url_for('user', username=username)) @app.route('/unfollow/<username>') @login_required def unfollow(username): user = User.query.filter_by(username=username).first() if user is None: flash('User {} not found.'.format(username)) return redirect(url_for('index')) if user == current_user: flash('You cannot unfollow yourself!') return redirect(url_for('user', username=username)) current_user.unfollow(user) db.session.commit() flash('You are not following {}.'.format(username)) return redirect(url_for('user', username=username)) |
Các đoạn mã trên tương đối quen thuộc, nhưng bạn cũng nên để ý đến các điều kiện kiểm tra để ngăn các lỗi không mong muốn và cung cấp các thông báo có ý nghĩa cho người sử dụng khi có vấn đề.
Bây giờ, chúng ta đã có các hàm hiển thị và có thể tạo liên kết tới chúng từ các trang trong ứng dụng. Chúng ta sẽ thêm các liên kết để theo dõi và chấm dứt theo dõi một user vào trang hồ sơ cá nhân của mỗi user:
app/templates/user.html: Liên kết để theo dõi và chấm dứt theo dõi trong trang hồ sơ cá nhân
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... <h1>User: {{ user.username }}</h1> {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %} {% if user.last_seen %}<p>Last seen on: {{ user.last_seen }}</p>{% endif %} <p>{{ user.followers.count() }} followers, {{ user.followed.count() }} following.</p> {% if user == current_user %} <p><a href="{{ url_for('edit_profile') }}">Edit your profile</a></p> {% elif not current_user.is_following(user) %} <p><a href="{{ url_for('follow', username=user.username) }}">Follow</a></p> {% else %} <p><a href="{{ url_for('unfollow', username=user.username) }}">Unfollow</a></p> {% endif %} ... |
Trong template hồ sơ cá nhân, chúng ta sẽ thêm một hàng mới dưới phần thời gian sử dụng lần cuối để hiển thị số người mà user hiện tại đang theo dõi cũng như số người đang theo dõi họ. Và trên cùng hàng với liên kết “Edit”, chúng ta sẽ thêm vào một trong ba liên kết sau:
- Nếu user đang truy cập hồ sơ cá nhân của chính họ, liên kết “Edit” sẽ được hiển thị như cũ.
- Nếu user truy cập hồ sơ cá nhân của một user khác không ở trong danh sách theo dõi của họ, liên kết “Follow” sẽ được hiển thị.
- Nếu user truy cập hồ sơ cá nhân của một user khác đang ở trong danh sách theo dõi của họ, liên kết “Unfollow” sẽ được hiển thị.
Đến đây, bạn có thể chạy ứng dụng, tạo một số user và thử chức năng theo dõi mà chúng ta vừa thêm vào. Tuy nhiên bạn sẽ cần phải nhập địa chỉ trang hồ sơ cá nhân của các user mà bạn muốn theo dõi/chấm dứt theo dõi bởi vì chúng ta còn chưa tạo ra chức năng để liệt kê danh sách các user. Ví dụ như nếu bạn muốn theo dõi user susan, bạn cần phải nhập địa chỉ http://localhost:5000/user/nguyen vào thanh địa chỉ của trình duyệt để truy cập trang hồ sơ cá nhân của user đó. Bạn cũng nên kiểm tra số người theo dõi và được theo dõi khi bạn sử dụng chức năng này.
Lẽ ra chúng ta cũng nên hiển thị số bài viết được theo dõi trên trang chủ của ứng dụng, nhưng chúng ta chưa có đủ các thành phần cần thiết để làm việc đó vì ứng dụng của chúng ta chưa có chức năng tạo bài viết. Do đó, chúng ta sẽ đợi đến khi chúng ta xây dựng chức năng viết bài.
Chúng ta sẽ kết thúc phần này ở đây. Hẹn gặp bạn trong phần tiếp theo.
Cảm ơn anh, seri bài viết rất hay, a có nhận dạy kèm không ạ. Nếu được liên hệ em qua: 0942.95.1234
Rất vui vì các bài viết này giúp ích cho bạn. Rất tiếc là tôi không thể dạy vì quỹ thời gian không cho phép và tôi không ở Việt nam. Nhưng nếu bạn có thắc mắc gì về lập trình, bạn có thể liên lạc với tôi qua Facebook, GitHub hoặc LinkedIn.
Thân ái,
Thái
tôi đang bị lỗi như bên dưới không biết sửa thế nào
flask.cli.NoAppException: While importing “myblog”, an ImportError was raised:
Traceback (most recent call last):
File “c:\users\huy\appdata\local\programs\python\python38\lib\site-packages\flask\cli.py”, line 240, in locate_app
__import__(module_name)
File “D:\Python\myblog-0.11\Huy\myblog.py”, line 1, in
from app import app
File “D:\Python\myblog-0.11\Huy\app\__init__.py”, line 11, in
from app import routes, models, errors
File “D:\Python\myblog-0.11\Huy\app\routes.py”, line 2, in
from app import app
ImportError: cannot import name ‘app’ from partially initialized module ‘app’ (most likely due to a circular import) (D:\Python\myblog-0.11\Huy\app\__init__.py)
Dường như bạn không theo đúng hướng dẫn từ Phần 1 phải không? Bạn thử kiểm tra lại một số thiết lập sau nhé:
1. Thiết lập môi trường ảo (virtual environment).
2. Trong module app/__init__.py, dòng lệnh “from app import routes, models, errors” phải nằm ở cuối chứ không phải trên đầu file. Bạn có thể đối chiếu với mã nguồn của tôi tại GitHub (https://github.com/thaipt/myblog)
Cám ơn bạn có đều tôi thử chạy nguồn của bạn tải về từ github nó vẫn báo lỗi dù đã cài đủ các thư viện như yêu cầu.
flask.cli.NoAppException
flask.cli.NoAppException: While importing “myblog”, an ImportError was raised:
Traceback (most recent call last):
File “c:\users\huy\appdata\local\programs\python\python38\lib\site-packages\flask\cli.py”, line 240, in locate_app
__import__(module_name)
File “D:\Python\myblog-master\myblog.py”, line 4, in
app = create_app()
File “D:\Python\myblog-master\app\__init__.py”, line 43, in create_app
from app.main import bp as main_bp
File “D:\Python\myblog-master\app\main\__init__.py”, line 5, in
from app.main import routes
File “D:\Python\myblog-master\app\main\routes.py”, line 5, in
from guess_language import guess_language
File “c:\users\huy\appdata\local\programs\python\python38\lib\site-packages\guess_language\guess_language.py”, line 37, in
from blocks import unicodeBlock
ModuleNotFoundError: No module named ‘blocks’
Bạn đang dùng Python 2 hay Python 3? Bạn cần chắc rằng bạn đang dùng Python 3 và có tạo môi trường ảo như trong Phần 1.
Sau đó, nếu vẫn không giải quyết được, bạn có thể thử giải pháp sau đây:
1. Bạn tải mã nguồn từ Phần 15 (Phần mới nhất trên GitHub)
2. Sau khi tải mã nguồn về, bạn vào thư mục myblog và xóa thư mục con “myenv”
3. Mở file requirements.txt trong thư mục myblog bằng Notepad hay một trình soạn thảo văn bản và xóa dòng sau đây: “pkg-resources==0.0.0” (tôi mới phát hiện lỗi này nhưng chưa có thời gian để cập nhật)
4. Mở file app/templates/auth/login.html và sửa dòng sau đây: url_for(‘auht.reset_password_request’) thành: url_for(‘auth.reset_password_request’) (Lỗi đánh máy nhưng tôi chưa có thời gian sửa)
5. Dùng lệnh sau đây: pip3 install -r requirements.txt để cài đặt lại các thư viện cần thiết trong môi trường ảo của bạn như hướng dẫn cuối Phần 15
Sau đó, bạn sẽ chạy được ứng dụng với lệnh “flask run”
Ok rồi bạn, thanks bạn nhiều, mong bạn viết tiếp hướng dẫn. Tôi cũng đang có một dự án các bạn bên mỹ làm giờ các bạn đó nhiều việc quá giao lại mà ở VN ít người biết python flask quá lên mạng tìm cũng chủ yếu ở nước ngoài, giờ phải tự học từ đầu. Có gì không hiểu nhờ bạn chỉ thêm.
/home/ubuntu/microblog/.env
chổ tạo file .env tôi chưa biết cách tạo vì nhập như trên đường dẫn không có. Bạn chỉ giúp với.