[Flask] – Phần 4: Sử dụng cơ sở dữ liệu
Trong phần này, chúng ta sẽ cùng tìm hiểu về cách sử dụng cơ sở dữ liệu với ứng dụng Flask.

Admins
---

Cơ sở dữ liệu trong Flask
Nếu bạn có tìm hiểu qua, bạn sẽ biết rằng Flask không hỗ trợ sẵn cơ sở dữ liệu. Tuy nhiên đây không phải là do sơ sót mà Flask được thiết kế như vậy để cho phép bạn chọn cơ sở dữ liệu nào thích hợp nhất cho ứng dụng của bạn thay vì bắt buộc bạn phải dùng một hệ cơ sở dữ liệu nào đó.Các thư viện mở rộng trong Flask hỗ trợ cho việc sử dụng nhiều cơ sở dữ liệu khác nhau. Các hệ cơ sở dữ liệu có thể được chia thành hai nhóm chính: nhóm cơ sở dữ liệu theo mô hình quan hệ (relational) và nhóm còn lại không sử dụng mô hình này. Nhóm sau cũng được biết với tên gọi NoSQL bởi vì các hệ cơ sở dữ liệu trong nhóm này không sử dụng ngôn ngữ hỏi đáp SQL nối tiếng. Mặc dù cả hai nhóm đều có các đại diện xuất sắc, các hệ cơ sở dữ liệu trong nhóm đầu thích hợp hơn với các dữ liệu có cấu trúc như là danh sách người sử dụng, các bài viết … trong khi NoSQL thích hợp cho dữ liệu không có cấu trúc chặt chẽ. Ứng dụng của chúng ta có thể sử dụng các hệ cơ sở dữ liệu trong cả hai nhóm, nhưng chúng ta sẽ chọn cơ sở dữ liệu quan hệ theo như lý do đã nói ở trên.
Trong Phần 3, chúng ta đã sử dụng thư viện mở rộng đầu tiên trong Flask (còn nhớ Flask-WTF không?). Trong phần này, chúng ta sẽ sử dụng thêm hai thư viện mở rộng nữa. Thư viện đầu tiên là Flask-SQLAlchemy. Đây là một thư viện cho phép chúng ta sử dụng một gói rất phổ biến là SQLAlchemy. Đây là một phần mềm trong nhóm sản phẩm gọi là Object Relational Mapping hay ORM. Các phần mềm trong nhóm này giúp chúng ta giao tiếp với cơ sở dữ liệu thông qua các thực thể như lớp, đối tượng và phương thức thay vi dùng các bảng và ngôn ngữ SQL (Các đại diện tiêu biểu cho nhóm sản phẩm này gồm có Hibernate cho Java, NHibernate hoặc EntityFramework cho .NET framework, …). Công việc chính của các phần mềm ORM là dịch các tác vụ bậc cao thành các lệnh dùng trong cơ sở dữ liệu.
Một đặc điểm rất hay của SQLAlchemy là nó hỗ trợ cho rất nhiều cơ sở dữ liệu quan hệ khác nhau như MySQL, PostgreSQL và SQLite. Điều này rất hữu ích vì nó có nghĩa là bạn có thể phát triển ứng dụng với một hệ cơ sở dữ liệu đơn giản như SQLite – vốn không đòi hỏi việc cài đặt phức tạp, nhưng khi triển khai ứng dụng, bạn có thể chuyển sang một hệ cơ sở dữ liệu khác tốt hơn như là MySQL hay PostgreSQL mà không cần phải thay đổi mã trong ứng dụng của bạn.
Để cài đặt Flask-SQLAlchemy trong môi trường ảo của bạn, hãy kích hoạt nó và chạy lệnh sau:
(myenv) $ pip3 install flask-sqlalchemy
Chuyển đổi dữ liệu
Phần lớn các bài hướng dẫn về cơ sở dữ liệu chỉ dạy cách tạo và sử dụng chúng nhưng lại không đề cập đến vấn đề làm sao để cập nhật cấu trúc của cơ sở dữ liệu sẵn có khi ứng dụng thay đổi. Điều này khó thực hiện bởi vì các thiết kế cơ sở dữ liệu quan hệ có liên quan chặt chẽ đến cấu trúc của dữ liệu. Khi cấu trúc của dữ liệu thay đổi, cơ sở dữ liệu cũng phải được chuyển đổi để thích ứng với cấu trúc mới.Thư viện mở rộng thứ hai mà chúng ta sẽ sử dụng là Flask-Migrate. Thư viện này là một giao diện của Flask với Alembic – một framework chuyên cho viện chuyển đổi dữ liệu với SQLAlchemy. Việc chuyển đổi cơ sở dữ liệu sẽ làm cho chúng ta mất công hơn một chút khi khởi tạo cơ sở dữ liệu, nhưng đáng giá vì nó sẽ giảm nhiều gánh nặng cho chúng ta trong việc thay đổi cơ sở dữ liệu trong tượng lai.
Quá trình cài đặt thư viện Flask-Migrate hoàn toàn tương tự như các thư viện khác:
(myenv) $ pip3 install flask-migrate
Thiết lập cấu hình cho Flask-SQLAlchemy
Chúng ta sẽ sử dụng cơ sở dữ liệu SQLite cho ứng dụng của chúng ta. SQLite là một hệ cơ sở dữ liệu được sử dụng rộng rãi trong quá trình phát triển các ứng dụng nhỏ hoặc thậm chí một vài ứng dụng lớn. Với SQLite, mỗi cơ sở dữ liệu sẽ được lưu trữ trong một file duy nhất và không đòi hỏi phải cài đặt thêm các phần mềm khác trên máy chủ như trong trường hợp của MySQL và PostgreSQL.Chúng ta sẽ phải thêm vào một số cấu hình mới trong file config.py:
- config.py: Cấu hình cho Flask-SQLAlchemy
import os basedir = os.path.abspath(os.path.dirname(__file__)) class Config(object): ... SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 'sqlite:///' + os.path.join(basedir, 'app.db') SQLALCHEMY_TRACK_MODIFICATIONS = False
Tham số SQLALCHEMY_TRACK_MODIFICATIONS sẽ được gán là False bởi vì chúng ta không cần chức năng này của SQLAlchemy – nó sẽ theo dõi và thông báo cho ứng dụng mỗi khi dữ liệu được cập nhật.
Trong ứng dụng của chúng ta, cơ sở dữ liệu sẽ được đại diện bởi một đối tượng gọi là thực thể cơ sở dữ liệu (database instance). Thành phần đảm nhiệm việc chuyển đổi cơ sở dữ liệu cũng sẽ được đại diện bởi một đối tượng khác. Các đối tượng này cần được khởi tạo trong ứng dụng, cụ thể hơn là trong file app/__init__.py:
- app/__init__.py: Khởi tạo các đối tượng Flask-SQLAlchemy và Flask-Migrate
from flask import Flask from config import Config from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate app = Flask(__name__) app.config.from_object(Config) db = SQLAlchemy(app) migrate = Migrate(app, db) from app import routes, models
Mô hình dữ liệu
Dữ liệu trong các hệ thống cơ sở dữ liệu sẽ được đại diện bởi một tập hợp các lớp, thường được gọi là mô hình dữ liệu (database model). Lớp ORM bên trong của SQLAlchemy sẽ thực hiện các công việc chuyển đổi cần thiết để ánh xạ các đối tượng từ mô hình dữ liệu vào các hàng và bảng trong cơ sở dữ liệu.Để bắt đầu, chúng ta hãy thử tạo một mô hình cho người sử dụng theo như sơ đồ sau:

Trường Id thường hiện diện trong hầu hết các mô hình dữ liệu và được sử dụng như là khóa chính (primary key). Mỗi người sử dụng sẽ được gán một giá trị Id duy nhất. Trong đa số các trường hợp, các khóa chính thường được gán giá trị tự động bởi cơ sở dữ liệu, vì vậy chúng ta chỉ cần chỉ định trường id là khóa chính là đủ.
Các trường username, email và password_hash được định nghĩa là các chuỗi (hay là kiểu VARCHAR theo quy ước của cơ sở dữ liệu). Độ dài tối đa của các chuỗi này cũng được xác định để hệ thống cơ sở dữ liệu có thể tối ưu không gian lưu trữ. Ý nghĩa của các trường username và password tương đối dễ hiểu, nhưng chúng ta cần giải thích một chút về trường password_hash. Dù ứng dụng của chúng ta không phải là một ứng dụng hoàn chỉnh, chúng ta vẫn muốn tuần theo những quy tắc bảo mật nhất định. Và một trong những quy tắc cơ bản nhất là không bao giờ lưu trữ mật mã (password) trong cơ sở dữ liệu dưới dạng văn bản (text). Lý do là vì nếu cơ sở dữ liệu bị xâm nhập, hacker sẽ truy nhập được tất cả các mật mã và từ đó có thể lấy tất cả thông tin cá nhân của người sử dụng. Vì vậy, thay vì lưu trữ mật mã dưới dạng văn bản, chúng ta sẽ dùng định dạng băm của mật mã (password hash) – hash là cấu trúc một chiều, chúng ta sẽ không thể chuyển đổi hash thành văn bản gốc – nhờ đó, chúng ta sẽ tránh được tình trạng truy nhập mật khẩu trái phép ngay cả khi cơ sở dữ liệu của chúng ta rơi vào tay của hacker. Chúng ta sẽ thảo luận vấn đề bảo mật kỹ hơn trong các phần sau.
Bây giờ, chúng ta có thể chuyển đổi mô hình dữ liệu người dùng ở trên thành mã chương trình trong file app/models như sau:
- app/models.py: Mô hình dữ liệu về người sử dụng
from app import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), index=True, unique=True) email = db.Column(db.String(120), index=True, unique=True) password_hash = db.Column(db.String(128)) def __repr__(self): return '<User {}>'.format(self.username)
Phương thức __repr__() báo cho trình biên dịch Python biết cách để in ra các thông tin về đối tượng, và rất hữu dụng cho việc dò tìm lỗi (debugging). Chúng ta có thể thấy cách hoạt động của phương thức __repr__() như trong phần minh họa dưới đây:
>>> from app.models import User >>> u = User(username='thai', email='thai@example.com') >>> u <User thai>
Tạo ra kho lưu trữ cho quá trình chuyển đổi dữ liệu (migration repository)
Mô hình dữ liệu mà chúng ta tạo ra ở trên định nghĩa cấu trúc ban đầu của dữ liệu (schema) mà chúng ta sẽ dùng trong ứng dụng. Nhưng khi chúng ta tiếp tục phát triển ứng dụng về sau, sẽ có những lúc chúng ta sẽ cần thay đổi hay cập nhật cấu trúc này. Để giúp cho quá trình này, Alembic (framework cho việc chuyển đổi dữ liệu được sử dụng trong thư viện Flask-Migrate) sẽ đảm nhiệm việc thay đổi cấu trúc dữ liệu trong cơ sở dữ liệu mà không cần phải tạo lại cơ sở dữ liệu từ đầu.Để thực thi công việc có vẻ khó khăn này, Alembic có một kho lưu trữ cho quá trình chuyển đổi dữ liệu (migration repository). Đây thực ra là một thư mục có chứa các đoạn mã kịch bản (script) cho việc chuyển đổi dữ liệu. Mỗi khi chúng ta cập nhật cấu trúc dữ liệu, một đoạn mã mới có chứa các thay đổi cần thiết sẽ được thêm vào trong kho lưu trữ này. Để thực hiện việc chuyển đổi cho cơ sở dữ liệu, các đoạn mã này sẽ được thực hiện theo đúng thứ tự mà chúng đã được tạo ra.
Thư viện Flask-Migrate có các lệnh riêng được gọi thông qua lệnh flask. Cho đến giờ, chúng ta đã biết một lệnh flask là flask run. Lệnh này được hỗ trợ trực tiếp từ Flask. Hôm nay chúng ta sẽ dùng thêm một lệnh nữa là flask db, đây là lệnh mở rộng khi chúng ta dùng thư viện Flask-Migrate để thực hiện các thao tác chuyển đổi dữ liệu. Trước hết, hãy tạo một kho lưu trữ chuyển đổi dữ liệu cho ứng dụng của chúng ta bằng lệnh flask db init:
(myenv) $ flask db init Creating directory /home/thaipt/Works/Flask/myblog/migrations ... done Creating directory /home/thaipt/Works/Flask/myblog/migrations/versions ... done Generating /home/thaipt/Works/Flask/myblog/migrations/env.py ... done Generating /home/thaipt/Works/Flask/myblog/migrations/README ... done Generating /home/thaipt/Works/Flask/myblog/migrations/script.py.mako ... done Generating /home/thaipt/Works/Flask/myblog/migrations/alembic.ini ... done Please edit configuration/connection/logging settings in '/home/thaipt/Works/Flask/myblog/migrations/alembic.ini' before proceeding.
Sau khi chạy lệnh này, bạn sẽ thấy thư mục mới tên là migrations có chứa một số file và một thư mục con của thư mục này tên là versions. Tất cả những file này là một phần trong mã nguồn của bạn và cần được thêm vào hệ thống quản lý mã nguồn của bạn.
Chuyển đổi dữ liệu lần đầu
Khi đã có kho lưu trữ cho chuyển đổi dữ liệu, chúng ta có thể tiến hành thực hiện việc chuyển đổi dữ liệu lần thứ nhất. Quá trình này sẽ tạo ra bảng user trong hệ thống cơ sở dữ liệu tương ứng với mô hình dữ liệu User. Có hai cách để thực hiện việc chuyển đổi dữ liệu: tự động hay thủ công. Với cách chuyển đổi tự động, Alembic sẽ so sánh cấu trúc dữ liệu được định nghĩa bởi mô hình dữ liệu và cơ sở dữ liệu thật sự và tạo ra mã kịch bản (script) để thay đổi cấu trúc của cơ sở dữ liệu cho phù hợp với mô hình dữ liệu. Trong trường hợp này, bởi vì chúng ta chưa tạo ra cơ sở dữ liệu, quá trình chuyển đổi tự động sẽ đưa toàn bộ mô hình dữ liệu User vào mã chuyển đổi. Để bắt đầu quá trình chuyển đổi tự động, chúng ta dùng lệnh flask db migrate như sau:(myenv) $ flask db migrate -m "Tạo bảng users" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'user' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']' Generating /home/thaipt/Works/Flask/myblog/migrations/versions/fcf566b5be7f_tao_bang_users.py ... done
Mã kịch bản cho chuyển đổi dữ liệu sinh ra sau lệnh này sẽ là một phần trong dự án của bạn và cần được thêm vào hệ thống quản lý mã nguồn mà bạn đang sử dụng. Nếu bạn muốn, bạn có thể xem thử các đoạn mã này. Bạn sẽ thấy rằng nó có hai hàm gọi là upgrade() và downgrade(). Hàm upgrade() sẽ thực hiện việc chuyển đổi và hàm downgrade() sẽ xóa bỏ các thay đổi được tạo ra bởi upgrade(). Điều này cho phép Alembic chuyển đổi dữ liệu tới bất kỳ điểm nào trong lịch sử chuyển đổi – ngay cả khi cần chuyển về các phiên bản cũ hơn nhờ vào hàm downgrade().
Lệnh flask db migrate sẽ không thực hiện bất kỳ thay đổi nào trong cơ sở dữ liệu, nó chỉ sinh ra các đoạn mã kịch bản (script) cần thiết để chạy quá trình này mà thời. Để thực hiện việc chuyển đổi thực sự, chúng ta phải dùng lệnh flask db upgrade như sau:
(myenv) $ flask db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade -> fcf566b5be7f, Tao bang users
Cũng xin lưu ý rằng theo mặc định, Flask-SQLAlchemy sử dụng quy ước đặt tên theo kiểu con rắn (snake-case) cho các bảng trong cơ sở dữ liệu. Vì vậy, nó sẽ tạo ra bảng user cho mô hình dữ liệu User. Tương tự như vậy, nó sẽ đặt tên cho bảng ứng với mô hình dữ liệu AddressAndPhone là address_and_phone. Nếu bạn muốn sử dụng cách đặt tên theo ý bạn, bạn có thể thêm thuộc tính __tablename__ vào trong mô hình dữ liệu và gán tên mà bạn muốn vào đó dưới dạng chuỗi.
Quá trình nâng cấp và giảm cấp cơ sở dữ liệu
Tại thời điểm này, ứng dụng của chúng ta vẫn còn rất sơ khai. Dù vậy, chúng ta cũng nên nói về cách thức chúng ta sẽ dùng cho việc chuyển đổi dữ liệu sau này. Giả sử chúng ta có hai phiên bản của ứng dụng: một trên máy của bạn dùng để phát triển, và một sẽ được dùng để triển khai vào máy chủ chính thức trên mạng mà mọi người có thể sử dụng.Bây giờ chúng ta sẽ thử xem xét tình huống sau: trong lần phát hành tiếp theo của ứng dụng, bạn đã thay đổi mô hình dữ liệu của bạn – ví dụ như chúng ta sẽ thêm một bảng mới vào cơ sở dữ liệu. Nếu không có quá trình chuyển đổi bạn sẽ cần phải tìm hiểu làm thế nào để thay đổi cấu trúc của cơ sở dữ liệu của bạn trong cả hai môi trường: môi trường phát triển và máy chủ. Và chúng ta sẽ phải tốn rất nhiều công sức cho việc này.
Nhưng với sự hỗ trợ của chuyển đổi dữ liệu, sau khi bạn đã cập nhật mô hình dữ liệu, bạn sẽ tạo ra một mã kịch bản mới (với lệnh flask db migrate). Bạn sẽ cần duyệt lại mã này để chắn chắn rằng không có sai sót nào – bởi vì quá trình tạo ra mã này là tự động – và sau đó thực thi mã này trong môi trường phát triển (với lệnh flask db upgrade). Cuối cùng, bạn sẽ thêm mã này vào hệ thống quản lý mã nguồn.
Khi bạn đã sẵn sàng để phát hành phiên bản tiếp theo của ứng dụng trên môi trường production, bạn chỉ cần lấy mã đã được cập nhật từ hệ thống quản lý mã nguồn. Mã này sẽ bao gồm cả mã kịch bản cho chuyển đổi dữ liệu. Và công việc tiếp theo của bạn chỉ đơn giản là chạy lệnh flask db upgrade. Alembic sẽ tự động phát hiện ra là cơ sở dữ liệu trên môi trường production của bạn chưa được cập nhật với cấu trúc dữ liệu hiện tại và sẽ thực thi tất cả các mã kịch bản kể từ lần phát hành cuối cùng.
Như chúng ta đã thấy, Flask-Migration cũng bao gồm lệnh flask db downgrade để hủy bỏ các thay đổi trên cơ sở dữ liệu đã được thực hiện trước đây. Dù trong thực tế hiếm khi chúng ta phải sử dụng lệnh này trên máy chủ chính thức, nó sẽ rất hữu ích trong môi trường phát triển của chúng ta. Sẽ có lúc bạn tạo ra một một mã kịch bản chuyển đổi nhưng lại phát hiện nó không chạy như ý. Trong những trường hợp như vậy, bạn có thể dùng lệnh downgrade, xóa bỏ các mã kịch bản này và tạo ra các mã kịch bản đúng.
Sơ lược về quan hệ trong cơ sở dữ liệu
Các hệ thống cơ sở dữ liệu quan hệ đảm nhiệm rất tốt việc lưu trữ và thiết lập các mối quan hệ giữa các bản ghi (record). Ví dụ như trong trường hợp một người sử dụng viết một bài trên blog, người sử dụng sẽ được đại diện bởi một bản ghi trong bản (table) users, và bài viết sẽ được đại diện bởi một bản ghi khác trên bảng posts. Cách hiệu quả nhất để biết người nào viết bài nào là thiết lập mối quan hệ giữa hai bản ghi này.Sau khi mối quan hệ giữa người sử dụng và bài viết được thiết lập, hệ thống cơ sở dữ liệu sẽ có thể tìm ra kết quả cho các truy vấn (query) về mối liên hệ này. Một ví dụ dễ thấy là bạn có một bài viết và muốn tìm ra ai đã viết bài này. Ngược lại, nếu bạn muốn tìm ra các bài viết được thực hiện bởi một tác giả, bạn sẽ phải dùng một truy vấn phức tạp hơn. Flask-SQLAlchemy sẽ hỗ trợ cho chúng ta trong cả hai trường hợp.
Bây giờ hãy mở rộng cơ sở dữ liệu của chúng ta để lưu trữ cả các bài viết để hiểu rõ hơn về các quan hệ. Sau đây là cấu trúc của bảng posts:

Bảng posts sẽ có trường id (bắt buộc), body và timestamp. Tuy vậy, ngoài các trường này, chúng ta sẽ thêm một trường mới là user_id để liên kết các bài viết với tác giả. Trước đây, chúng ta đã thảo luận về lý do tại sao chúng ta cần sử dụng trường id với vai trò khóa chính cho các bản ghi về người sử dụng, và chúng ta cũng đã biết rằng giá trị của các id phải là duy nhất. Để tạo liên kết giữa các bài viết và tác giả, chúng ta cần thêm tham chiếu đến id của tác giả (cũng là id từ các bản ghi trong bản users) từ bản posts, và đó là lý do tại sao chúng ta cần có trường user_id. Trường này được gọi là một khóa ngoại (foreign key). Sơ đồ trên cho chúng ta thấy vai trò của khóa ngoại trong việc thiết lập liên hệ giữa hai bảng posts và users. Trong trường hợp này, liên hệ giữa hai bảng được gọi là một-nhiều (one-to-many) vì một tác giả (user) có thể viết nhiều bài (post). Nếu bạn vẫn còn cảm thấy không hiểu rõ về quan hệ trong mô hình dữ liệu và cơ sở dữ liệu, bạn có thể tham khảo thêm tại đây.
Chúng ta sẽ cập nhật file app/models.py để tạo mô hình dữ liệu cho các bài viết như sau:
- app/models.py: Bảng Posts và liên hệ với bảng User
from datetime import datetime from app import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), index=True, unique=True) email = db.Column(db.String(120), index=True, unique=True) password_hash = db.Column(db.String(128)) posts = db.relationship('Post', backref='author', lazy='dynamic') def __repr__(self): return '<User {}>'.format(self.username) class Post(db.Model): id = db.Column(db.Integer, primary_key=True) body = db.Column(db.String(140)) timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) def __repr__(self): return '<Post {}>'.format(self.body)
Trường user_id được khởi tạo với vai trò là khóa ngoại của user.id. Nói cách khác, mỗi giá trị của trường này trong bảng posts là một giá trị từ trường id của bảng users. Trong giá trị user.id, chuỗi user đại diện cho tên của bảng tương ứng với mô hình dữ liệu. Tuy nhiên, ở đây có một vài điểm không nhất quán mà chúng ta phải tạm thời chấp nhận. Ví dụ như khi gọi hàm db.relationship(), chúng ta tham chiếu đến mô hình dữ liệu thông qua lớp tương ứng bằng cách dùng tên của nó – bắt đầu bằng chữ hoa, nhưng trong một số trường hợp khác như khi gọi hàm db.ForeignKey(), chúng ta phải sử dụng tên bảng trong cơ sở dữ liệu để tham chiếu đến mô hình dữ liệu tương ứng với tên gọi bằng chữ thường theo quy ước của SQLAlchemy (hoặc là dùng kiểu snake case nếu tên gọi của mô hình dữ liệu là từ ghép).
Lớp User cũng được cập nhật để có thêm trường posts và được khởi tạo với hàm db.relationship(). Đây không phải là một trường thật sự trong cơ sở dữ liệu mà chỉ là một định nghĩa cấp cao để mô tả liên hệ giữa tác giả và bài viết (trường ảo), vì vậy chúng ta không biểu diễn nó trong sơ đồ dữ liệu. Đối với các mối quan hệ một-nhiều, một trường được định nghĩa từ db.relationship ở phía của quan hệ “một” sẽ cho phép truy nhập vào các trường của ở phía “nhiều”. Ví dụ như nếu chúng ta lưu một bản ghi về tác giả trong table u, biểu thức u.posts sẽ thực hiện một truy vấn trong cơ sở dữ liệu để tìm tất cả những bài viết được viết bởi tác giả này. Khi gọi hàm db.relationship, tham số đầu tiên là lớp mô hình đại diện cho phía “nhiều” trong mối quan hệ giữa hai mô hình. Tham số này được truyền dưới dạng một chuỗi được gán tên của lớp mô hình tương ứng sẽ được định nghĩa trong phần tiếp theo của module. Tham số thứ hai (backref) định nghĩa tên của trường sẽ được thêm vào các đổi tượng ở phía “nhiều” (trong trường hợp này là các bài viết hay post) và nhờ đó chúng ta truy nhập ngược lại các đối tượng từ phía “một” trong mô hình quan hệ. Cụ thể là nó sẽ cho phép chúng ta dùng biểu thức post.author để tìm ra tác giả của một bài viết nào đó. Tại thời điểm này, chúng ta sẽ tạm thời lướt qua tham số tiếp theo là lazy và sẽ trở lại với tham số này trong các phần tiếp theo. Đừng lo nếu bạn vẫn thấy còn cảm thấy chưa hiểu rõ các chi tiết này, bạn sẽ thấy rõ ràng hơn qua các ví dụ minh họa tiếp theo sau.
Vì chúng ta vừa thay đổi mô hình dữ liệu, chúng ta cần tạo ra mã chuyển đổi mới:
(myenv) $ flask db migrate -m "Tạo bảng posts" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'post' INFO [alembic.autogenerate.compare] Detected added index 'ix_post_timestamp' on '['timestamp']' Generating /home/thaipt/Works/Flask/myblog/migrations/versions/f0db34c99253_tao_bang_posts.py ... done
(myenv) $ flask db upgrade INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.runtime.migration] Running upgrade fcf566b5be7f -> f0db34c99253, Tao bang posts
Thực hành
Suốt từ đầu đến giờ, chúng ta hầu như chỉ nói về việc định nghĩa dữ liệu, thật là chán phải không? Bây giờ chúng ta hãy thực tập một chút để làm quen với cách hoạt động của cơ sở dữ liệu và Python. Bạn hãy gọi trình biên dịch Python bằng lệnh python và hãy chắc rằng bạn đã kích hoạt môi trường ảo trước khi chúng ta bắt đầu.Tại dấu nhắc lệnh của trình biên dịch Python, hãy tham chiếu (import) đến các thư viện và đối tượng trong mô hình dữ liệu của chúng ta:
>>> from app import db >>> from app.models import User, Post
>>> u = User(username='thai', email='thai.pham@example.com') >>> db.session.add(u) >>> db.session.commit()
>>> u = User(username='nguyen', email='nguyen@example.com') >>> db.session.add(u) >>> db.session.commit()
>>> users = User.query.all() >>> users [<User thai>, <User nguyen>] >>> for u in users: ... print(u.id, u.username) ... 1 thai 2 nguyen
>>> u = User.query.get(1) >>> u <User thai>
>>> u = User.query.get(1) >>> p = Post(body='my first post!', author=u) >>> db.session.add(p) >>> db.session.commit()
>>> # Tìm tất cả bài được viết bởi một user >>> u = User.query.get(1) >>> u <User thai> >>> posts = u.posts.all() >>> posts [<Post my first post!>] >>> # tương tự như vậy, nhưng lần này là một user không có bài viết >>> u = User.query.get(2) >>> u <User nguyen> >>> u.posts.all() [] >>> # in ra tên tác giả và toàn bộ các bài viết >>> posts = Post.query.all() >>> for p in posts: ... print(p.id, p.author.username, p.body) ... 1 thai my first post! >>> # tìm tất cả các user theo thứ tự abc đảo ngược >>> User.query.order_by(User.username.desc()).all() [<User thai>, <User nguyen>]
>>> users = User.query.all() >>> for u in users: ... db.session.delete(u) ... >>> posts = Post.query.all() >>> for p in posts: ... db.session.delete(p) ... >>> db.session.commit()
Ngữ cảnh lệnh
Bạn có nhớ chúng ta đã làm gì ở đầu bài học trước không? Chúng ta đã gọi trình biên dịch Python và sau đó, thực hiện câu lệnh để tham chiếu (import) như sau:>>> from app import db >>> from app.models import User, Post
(myenv) $ python >>> app Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'app' is not defined >>> (myenv) $ flask shell >>> app Python 3.7.3 (default, Apr 3 2019, 05:39:12) [GCC 8.3.0] on linux App: app [production] Instance: /home/thaipt/Works/Flask/myblog/instance >>>
from app import app, db from app.models import User, Post @app.shell_context_processor def make_shell_context(): return {'db': db, 'User': User, 'Post': Post}
(myenv) $ flask shell Python 3.7.3 (default, Apr 3 2019, 05:39:12) [GCC 8.3.0] on linux App: app [production] Instance: /home/thaipt/Works/Flask/myblog/instance >>> db <SQLAlchemy engine=sqlite:////home/thaipt/Works/Flask/myblog/app.db> >>> User <class 'app.models.User'> >>> Post <class 'app.models.Post'> >>>