7. Tạo các bảng
7.1 Giải pháp
Tới phần này, bạn đã có tài liệu thiết kế của các bảng, các ràng buộc, mối quan hệ giữa các bảng. Công việc tiếp theo là tạo các bảng, tạo các ràng buộc, tạo các quan hệ giữa các bảng trên Hệ quản trị cơ sở dữ liệu (PostgreSQL).
Để thao tác với PostgreSQL, chúng ta có một số cách:
- Sử dụng giao diện dòng lệnh (CMD)
- Sử dụng giao diện đồ họa (pgAdmin4)
- Sử dụng ngôn ngữ lập trình
Một vài câu hỏi đặt ra là:
- Làm sao để việc tạo bảng, nhập dữ liệu mẫu được nhanh nhất
- Những người lập trình chung có thể tải dự án và phát triển tiếp một cách tiện lợi
Vậy giải pháp để tạo cơ sở dữ liệu nên được tiến hành như thế nào?
Bạn có thể trao đổi thêm với AI để có giải pháp phù hợp.
Ở đây, chúng ta sẽ làm việc với Hệ quản trị cơ sở dữ liệu bằng ngôn ngữ lập trình (Python) và Công cụ di chuyển cơ sở dữ liệu (database migration).
Sau đây là các bước thực hiện:
[1] Sử dụng công cụ Database Migration (Alembic)
Thay vì tạo bảng thủ công bằng CMD hay pgAdmin4, bạn nên sử dụng Alembic kết hợp với SQLAlchemy trong môi trường Python.
Cách làm này có vài điểm lợi:
- Mọi thay đổi về cấu trúc bảng (thêm cột, đổi kiểu dữ liệu, tạo ràng buộc) được ghi lại thành các tập tin mã nguồn (scripts). Khi người khác tải dự án về, họ chỉ cần chạy một câu lệnh để đồng bộ hóa cấu trúc database mà không cần thực hiện thủ công
- Giúp theo dõi lịch sử thay đổi của cơ sở dữ liệu tương tự như cách Git quản lý mã nguồn
[2] Định nghĩa Schema bằng Object-Relational Mapping (ORM)
Chúng ta sẽ định nghĩa các bảng (Users, Documents, FormulaEntries, Logs) dưới dạng các Class trong Python sử dụng SQLAlchemy.
Việc sử dụng Snake Case (user_id, file_name) trong định nghĩa bảng giúp ánh xạ tự động và tự nhiên sang các đối tượng Python, giúp viết mã xử lý dữ liệu nhanh hơn.
Các ràng buộc phức tạp như CASCADE (tự động xóa dữ liệu liên quan) và các Trigger (tự động cập nhật thời gian updated_at) được thiết lập ngay trong mã nguồn, đảm bảo tính toàn vẹn dữ liệu một cách có hệ thống.
[3] Nhập dữ liệu mẫu (Seeding) thông qua Script
Để nhập dữ liệu mẫu nhanh nhất cho các thành viên trong nhóm phát triển, chúng ta sẽ xây dựng các script Python, sử dụng các Model đã định nghĩa để chèn (insert) dữ liệu giả vào các bảng.
Việc này giúp môi trường phát triển của mọi thành viên luôn có sẵn dữ liệu để kiểm thử ngay lập tức.
[4] Triển khai nhất quán với Docker
Sử dụng tệp docker-compose.yml để khởi chạy dịch vụ PostgreSQL (db) với các thiết lập môi trường thống nhất (tên database, port, mật khẩu) từ tệp .env. Điều này đảm bảo rằng dù lập trình viên chạy dự án trên Windows, Linux hay macOS, cơ sở dữ liệu vẫn luôn hoạt động giống hệt nhau.
7.2 Cài đặt
Để triển khai giải pháp quản lý cơ sở dữ liệu chuyên nghiệp bằng Python, SQLAlchemy và Alembic, bạn hãy thực hiện theo các bước chi tiết dưới đây. Cách tiếp cận này giúp bạn đồng bộ hóa database giữa các thành viên trong nhóm một cách tự động.
Bước 1: Thiết lập môi trường và Cài đặt thư viện
Trước tiên, bạn cần cài đặt các thư viện cần thiết để Python có thể giao tiếp với PostgreSQL và quản lý việc di chuyển cơ sở dữ liệu (migration).
Tạo môi trường ảo:
Để thực hiện các cài đặt, phải đảm bảo trên máy của bạn đã cài trình dịch lệnh Python. Trong cửa sổ dòng lệnh, nhập lệnh sau để xem trên máy có Python hay chưa (nhớ viết hoa chữ V):
python -V
# Python 3.14.0
Mở chương trình dòng lệnh (CMD), di chuyển dấu nhắc lệnh vào thư mục dự án, nhập lệnh sau:
python -m venv venv
Ý nghĩa của lệnh:
- python -m venv: Gọi module tạo môi trường ảo
- venv (cái tên cuối cùng): Là tên thư mục sẽ chứa môi trường ảo. Bạn có thể đặt tên khác, nhưng venv là tên phổ biến nhất
Sau khi chạy lệnh trên, bạn vào thư mục dự án, thấy xuất hiện thư mục venv là đã tạo được môi trường ảo.
Môi trường ảo là một thư mục, chứa phiên bản Python riêng biệt và các thư viện riêng dành cho một dự án cụ thể.
Môi trường ảo giúp:
- Giữ thư viện của dự án không làm ảnh hưởng (xung đột) đến dự án khác hoặc hệ thống máy tính
- Đảm bảo dự án luôn chạy đúng các phiên bản thư viện đã cài đặt
- Giúp người khác tải dự án của bạn về và cài đặt mọi thứ đồng bộ một cách dễ dàng thông qua tập tin danh sách thư viện (requirements.txt).
Bạn chạy tiếp lệnh sau để kích hoạt môi trường ảo
venv\Scripts\activate # Windows
Khi bạn chạy lệnh này, các thay đổi sau sẽ xảy ra:
- Máy tính sẽ tạm thời "quên" phiên bản Python chung của hệ thống. Thay vào đó, nó sẽ ưu tiên sử dụng trình thông dịch Python và các thư viện nằm bên trong thư mục venv của dự án
- Bạn sẽ thấy tên môi trường (ví dụ: (venv)) xuất hiện ở đầu dòng lệnh trong Terminal/CMD. Điều này báo hiệu rằng bạn đang đứng "bên trong" chiếc hộp cách ly của dự án. Ví dụ:
D:\DuAn\Ebook2LateX>venv\scripts\activate
(venv) D:\DuAn\Ebook2LateX>
- Từ thời điểm này, bất kỳ thư viện nào bạn cài đặt bằng lệnh pip install sẽ chỉ được lưu vào thư mục venv đó, không ảnh hưởng đến các dự án khác hay cài đặt gốc của máy tính
Cài đặt các thư viện:
pip install sqlalchemy alembic psycopg2-binary python-dotenv
Mục đích của các thư viện:
- SQLAlchemy giúp bạn làm việc với cơ sở dữ liệu bằng ngôn ngữ Python
- Alembic theo dõi và quản lý những thay đổi của các bảng dữ liệu theo thời gian
- psycopg2-binary chuyển lệnh xuống cho PostgreSQL thực hiện
- python-dotenv lấy mật khẩu để mở cổng kết nối
Sau khi quá trình cài đặt thành công, bạn vào thư mục của dự án, vào “...\venv\Lib\site-packages” để kiểm tra, các gói vừa được cài đặt sẽ có tại đây (ví dụ: D:\DuAn\Ebook2LateX\venv\Lib\site-packages)
Bước 2: Cấu hình Alembic (Khởi tạo Migration)
Alembic sẽ giúp bạn quản lý các phiên bản của database (giống như Git quản lý mã nguồn).
Khởi tạo Alembic trong thư mục backend/:
- Vào cửa sổ dòng lệnh, di chuyển dấu nhắc lệnh vào thư mục backend, gõ lệnh sau:
alembic init migrations
- Lệnh này tạo ra thư mục migrations/ và tập tin alembic.ini. (trong thư mục backend)
- Cấu hình kết nối: Mở tệp migrations/env.py, tìm và chỉnh sửa dòng target_metadata để Alembic biết các Model của bạn nằm ở đâu:
[Mã python]
from models import Base # Import Base từ file models.py của bạn
target_metadata = Base.metadata # Thiết lập metadata để Alembic theo dõi
Giải thích dòng mã trên:
Dòng mã trên đóng vai trò là "trạm kết nối" giữa các định nghĩa bảng trong mã Python và công cụ quản lý cơ sở dữ liệu Alembic. Đây là bước quan trọng nhất để Alembic có thể hiểu được cấu trúc Database của bạn.
from models import Base
- Dòng này thực hiện nạp đối tượng Base từ tập tin định nghĩa dữ liệu của bạn (thường là models.py)
- Tại sao cần Base? Trong SQLAlchemy, tất cả các Class (như User, Document) đều phải kế thừa từ một lớp cha chung gọi là Base (thường được tạo bằng hàm declarative_base()). Khi các Class này kế thừa Base, chúng sẽ tự động đăng ký cấu trúc của mình (tên bảng, các cột, kiểu dữ liệu) vào một "sổ cái" chung nằm bên trong Base.
target_metadata = Base.metadata
- Base.metadata là bản thiết kế (Schema) tổng thể của toàn bộ cơ sở dữ liệu. Nó chứa danh sách tất cả các bảng, các ràng buộc (constraints), và mối quan hệ mà bạn đã khai báo trong mã nguồn Python
- target_metadata là một biến đặc biệt mà Alembic sẽ tham chiếu tới
Luồng hoạt động của 2 dòng mã trên:
Khi bạn chạy lệnh tạo phiên bản database mới (ví dụ: alembic revision --autogenerate), Alembic sẽ thực hiện các bước sau:
- Tham chiếu tới biến target_metadata: Xem trong mã Python bạn đang khai báo những bảng nào, cột nào (Dựa trên thông tin từ Base.metadata)
- Nhìn vào Database thực tế: Kết nối xuống PostgreSQL để xem cấu trúc bảng hiện tại ở đó
- So sánh (Diff): Nếu trong mã nguồn có bảng mới mà dưới Database chưa có, Alembic sẽ tự động viết một tập tin script để tạo bảng đó
Nếu không có hai dòng mã này, Alembic sẽ bị "mù". Nó sẽ không biết bạn đã định nghĩa những gì trong Python và không thể tự động tạo ra các tập tin cập nhật cấu trúc cơ sở dữ liệu cho bạn và đồng nghiệp.
Bước 3: Định nghĩa Schema bằng SQLAlchemy (ORM)
Thay vì viết SQL, bạn định nghĩa các bảng bằng cách tạo các Class trong Python.
Trong thư mục backend của dự án, tạo tập tin models.py và nhập vào đoạn mã sau:
[models.py]
import uuid
from sqlalchemy import Column, String, Integer, ForeignKey, DateTime, Boolean, Text, Numeric
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
# Khởi tạo lớp Base để các Model kế thừa
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
user_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
username_email = Column(String(255), unique=True, nullable=False)
password_hash = Column(Text, nullable=False)
full_name = Column(String(100))
role = Column(String(20), default='Editor') # Admin, Editor, Viewer
last_login = Column(DateTime(timezone=True))
is_active = Column(Boolean, default=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# Quan hệ: Một người dùng có thể tải lên nhiều tài liệu
documents = relationship("Document", back_populates="owner")
class Document(Base):
__tablename__ = 'documents'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
user_id = Column(UUID(as_uuid=True), ForeignKey('users.user_id', ondelete='SET NULL'))
file_name = Column(Text, nullable=False)
file_path_url = Column(Text, nullable=False)
upload_date = Column(DateTime(timezone=True), server_default=func.now())
status = Column(String(50), default='Pending') # Pending, Processed, Error
# Quan hệ ngược lại với User
owner = relationship("User", back_populates="documents")
# Quan hệ: Một tài liệu có nhiều mục công thức
formulas = relationship("FormulaEntry", back_populates="document", cascade="all, delete-orphan")
class FormulaEntry(Base):
__tablename__ = 'formula_entries'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
document_id = Column(UUID(as_uuid=True), ForeignKey('documents.id', ondelete='CASCADE'), nullable=False)
raw_image_path = Column(Text)
latex_content = Column(Text)
order_index = Column(Integer, nullable=False)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
# Quan hệ ngược lại với Document
document = relationship("Document", back_populates="formulas")
# Quan hệ: Một công thức có thể có nhiều log (nếu chạy OCR nhiều lần)
logs = relationship("Log", back_populates="formula", cascade="all, delete-orphan")
class Log(Base):
__tablename__ = 'logs'
log_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
formula_id = Column(UUID(as_uuid=True), ForeignKey('formula_entries.id', ondelete='CASCADE'))
processing_time_ms = Column(Integer)
confidence_score = Column(Numeric(3, 2))
error_type = Column(String(100))
error_message = Column(Text)
timestamp = Column(DateTime(timezone=True), server_default=func.now())
environment_info = Column(JSONB) # Lưu trữ thông số CPU/GPU dưới dạng JSON
# Quan hệ ngược lại với FormulaEntry
formula = relationship("FormulaEntry", back_populates="logs")
Các điểm lưu ý trong mã nguồn trên:
- UUID: Sử dụng uuid.uuid4 để tự động tạo ID duy nhất cho mỗi bản ghi ngay từ phía Python nếu PostgreSQL chưa hỗ trợ
- Mối quan hệ (Relationship):
+ relationship() giúp bạn truy xuất dữ liệu dễ dàng. Ví dụ: từ một đối tượng document, bạn có thể gọi document.formulas để lấy danh sách tất cả công thức của nó
+ cascade="all, delete-orphan": Khi bạn xóa một tài liệu, tất cả các công thức và log liên quan sẽ tự động bị xóa theo, giúp database sạch sẽ
- Tự động cập nhật thời gian: server_default=func.now(): Tự lấy thời gian hiện tại khi tạo mới bản ghi
- onupdate=func.now(): Tự cập nhật lại thời gian vào cột updated_at mỗi khi bạn sửa nội dung công thức
- JSONB: Cột environment_info trong bảng Log sử dụng kiểu dữ liệu JSONB của PostgreSQL, cho phép bạn lưu trữ các bộ tham số không cố định một cách linh hoạt
Bước 4: Thực hiện Migration (Tạo bảng tự động)
Sau khi đã có tập tin định nghĩa cơ sở dữ liệu models.py, chúng ta sẽ sử dụng Alembic để chuyển đổi các Class Python này thành các bảng thực tế trong cơ sở dữ liệu PostgreSQL mà không cần viết lệnh CREATE TABLE, hoặc dùng giao diện pgAdmin 4 thủ công.
Ở phần trên chúng ta đã tạo ra môi trường Alembic: đã tạo ra thư mục Migrations, tạo tập tin alembic.ini.
Chúng ta sẽ cấu hình để kết nối tới Database:
- Mở tập tin alembic.ini và tìm dòng sqlalchemy.url. Hãy cập nhật thông tin kết nối PostgreSQL của bạn theo định dạng (nhớ lưu lại tập tin sau khi cập nhật thông tin):
sqlalchemy.url = postgresql://user:password@localhost:5432/ebook2latex_db
# ví dụ: sqlalchemy.url = postgresql://postgres:p@ssword1@localhost:5432/ebook2latex_db
Ở phần trên, bạn cũng đã thực hiện liên kết Alembic với Model của bạn.
Để chắc chắn, bạn có thể kiểm tra lại, bằng cách vào tập tin migrations/env.py:
Đảm bảo đã có 2 dòng cấu hình sau:
from models import Base # Import Base từ file models.py của bạn
target_metadata = Base.metadata # Thiết lập metadata để Alembic theo dõi
(Lưu ý: Đảm bảo đường dẫn import models chính xác với cấu trúc thư mục của bạn).
Tạo bản thảo Migration (Autogenerate):
- Bây giờ, hãy để Alembic tự động so sánh sự khác biệt giữa các Class trong Python và Database hiện tại:
Gõ lệnh sau vào cửa sổ dòng lệnh. Lưu ý: dấu nhắc lệnh phải đang nằm ở thư mục của dự án mà có chứa tập tin alembic.ini (ví dụ: D:\DuAn\Ebook2LateX\backend)
alembic revision --autogenerate -m "Tao cac bang ban dau cho Ebook2LateX"
Lưu ý: (có thể) chạy lệnh trên bạn sẽ gặp thông báo lỗi vì ký tự “@” trong chuỗi kết nối của tập tin alembic.ini gây ra hiểu lầm về đường dẫn. Bạn hãy vào tập tin alembic.ini, tìm tới dòng kết nối và sửa lại: thay chữ “@” bằng “%%40”.
Ví dụ:
sqlalchemy.url = postgresql://postgres:p%%40ssword1@localhost:5432/ebook2latex_db
Alembic sẽ tạo ra một tập tin mới trong thư mục migrations/versions/. Tập tin này chứa mã Python mô tả việc tạo các bảng users, documents, formula_entries, và logs.
Thực thi tạo bảng (Upgrade)
Cuối cùng, chạy lệnh sau để áp dụng các thay đổi vào PostgreSQL:
Trong cửa sổ dòng lệnh, gõ lệnh sau (dấu nhắc lệnh đang ở (venv) D:\DuAn\Ebook2LateX\backend>):
alembic upgrade head
Kiểm tra kết quả
Sau khi chạy xong, bạn có thể vào công cụ quản lý database (như pgAdmin hoặc DBeaver) để kiểm tra:
- Các bảng đã được tạo đầy đủ với đúng kiểu dữ liệu (UUID, JSONB, TIMESTAMP...)
- Các khóa ngoại (Foreign Keys) đã được thiết lập đúng mối quan hệ
- Cột updated_at sẽ có Trigger tự động cập nhật thời gian mỗi khi dữ liệu thay đổi
Tại sao chúng ta làm cách này?
- An toàn dữ liệu: Sau này nếu bạn thêm cột accuracy_score vào bảng logs, bạn chỉ cần sửa models.py, chạy lại lệnh ở bước Tạo bản thảo Migration và Thực thi tạo bảng. Alembic sẽ chỉ thêm cột mới mà không xóa dữ liệu cũ của bạn
- Đồng bộ nhóm: Khi làm việc nhóm, bạn chỉ cần gửi file migration này cho thành viên khác, họ chỉ cần chạy lệnh upgrade là database sẽ giống hệt của bạn