Nhấn vào theo dõi chúng tôi để cập nhật các bài viết mới nhất. Theo dõi!
Bài đăng

Hướng dẫn lập trình Flask – Phần 8: Tạo chức năng follower

Bài viết liên quan:
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.
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
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
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ường follower_id trong bảng liên kết. Biểu thức followers.c.follower_id tham chiếu đến cột follower_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ủa primaryjoin. Đ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ường followed_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ệ được theo dõi (follower).
  • lazy chỉ định chế độ thực thi cho truy vấn này. Thiết lập dynamic 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:
(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:
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
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
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:
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 thai
2 nguyen
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:
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 1 4
3 post của huy 4 2 3
4 post của thái 1 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:
	
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.
id text user_id follower_id followed_id
1 post của nguyên 2 1 2
3 post của huy 4 2 3

Getting Info...

Đăng nhận xét

Đồng ý cookie
Chúng tôi cung cấp cookie trên trang web này để phân tích lưu lượng truy cập, ghi nhớ các tùy chọn của bạn và tối ưu hóa trải nghiệm của bạn.
Oops!
Có vẻ như đã xảy ra sự cố với kết nối Internet của bạn. Vui lòng kết nối với internet và bắt đầu duyệt lại.
Trang web bị chặn
Xin lỗi! Trang web này không có sẵn ở quốc gia của bạn.