--------------- <> -----------------
--- KHOA HỌC - CÔNG NGHỆ - GIÁO DỤC - VIỆC LÀM ---
--- Học để đi cùng bà con trên thế giới ---

Tìm kiếm trong Blog

Ebook2LateX (7) - Tạo các bảng

Bài trước: Ebook2LateX (6) - Phân tích và thiết kế các bảng
-----

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 cùng nhóm 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.

Trong phần nà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ữ 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.

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, SQLAlchemyAlembic, 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

Trong cửa sổ 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)

Sau khi kiểm tra các gói đã xuất hiện trong site-packages, bước cuối cùng là trích xuất danh sách này ra tập tin cấu hình bằng lệnh: 

(lưu ý: dấu nhắc lệnh phải nằm ở thư mục \backend)

pip freeze > requirements.txt

Tại sao bước này là bắt buộc?

- Để Docker làm việc: Tập tin này chính là "thực đơn" mà Docker sẽ đọc để tự động cài đặt môi trường bên trong Container. Thiếu nó, Docker sẽ không thể vận hành Backend

- Đảm bảo tính nhất quán: Nó lưu lại chính xác phiên bản của các thư viện bạn đang dùng. Điều này giúp dự án của bạn không bị lỗi khi các thư viện này cập nhật phiên bản mới trong tương lai

- Thay thế cho venv: Thay vì phải gửi cả thư mục venv nặng nề cho người khác, bạn chỉ cần gửi tập tin requirements.txt nhỏ gọn này.

Bạn mở tập tin requirement.txt để đảm bảo đã có tên của các gói bạn vừa cài đặt.

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, FormulaEntry, Log) đề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 MigrationThự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 tập tin 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


Vậy là chúng ta đã tạo được bảng dữ liệu bằng ORM có tên là SQLAlchemy và công cụ di chuyển dữ liệu (database migration) Alembic. Chúng ta sẽ thực hiện commit vào Git để lưu lại trạng thái của hệ thống, phòng khi làm các bước tiếp theo có bị lỗi thì có mốc để khôi phục lại dự án.

- Trong chương trình cửa sổ dòng lệnh (CMD), dấu nhắc đang ở thư mục dự án (Ebook2LateX), nhập lệnh sau

- Kiểm tra để biết được các tập tin mới nào đã được tạo/sửa trong dự án và sẽ được commit

git status

- Chuyển các tập tin sang Vùng tạm (staging area):

git add .

- Commit 

git commit -m "feat: tao cac bang du lieu"


7.3 Bài tập và Câu hỏi

Bài tập

Bài tập 7a. Thực hiện các cài đặt, cấu hình trong phần lý thuyết.

Bài tập 7b. Gạch đầu dòng một số bước, một số lệnh quan trọng, khi bạn làm việc với các công cụ: ORM Alchemy, Database migration Alembic.

Câu hỏi

Câu 7.1 Môi trường ảo (venv) trong dự án Python được hiểu là gì? 

A. Là một trang web dùng để lưu trữ mã nguồn trực tuyến

B. Là một thư mục chứa phiên bản Python và các thư viện riêng biệt, giúp dự án không bị xung đột với các thư viện khác trong hệ thống

C. Là một phần mềm dùng để vẽ sơ đồ thiết kế cơ sở dữ liệu

D. Là một câu lệnh dùng để xóa toàn bộ các bảng trong PostgreSQL

Câu 7.2 Trong quy trình làm việc với Alembic, lệnh nào được dùng để thực thi các thay đổi từ tập tin bản thảo (script) vào cơ sở dữ liệu thực tế? 

A. alembic init migrations 

B. alembic revision --autogenerate 

C. alembic upgrade head 

D. pip freeze > requirements.txt

Câu 7.3 Tại sao chúng ta nên định nghĩa các bảng dưới dạng các Class trong Python bằng SQLAlchemy thay vì viết lệnh SQL trực tiếp? 

A. Vì SQLAlchemy sẽ giúp việc truy xuất dữ liệu dễ dàng hơn thông qua các đối tượng Python và hỗ trợ quản lý các mối quan hệ phức tạp

B. Vì sử dụng SQLAlchemy sẽ giúp ứng dụng chạy nhanh gấp 10 lần so với SQL thông thường

C. Vì SQLAlchemy là công cụ duy nhất có thể kết nối được với PostgreSQL

D. Vì viết mã Python ngắn hơn và không cần phải quan tâm đến kiểu dữ liệu của các cột

Câu 7.4 Lợi ích quan trọng nhất của việc sử dụng Database Migration (Alembic) khi làm việc nhóm là gì? 

A. Giúp mã nguồn chạy nhanh hơn trên máy tính của các thành viên khác

B. Giúp ghi lại lịch sử thay đổi cấu trúc bảng, cho phép các thành viên đồng bộ cơ sở dữ liệu chỉ bằng một câu lệnh mà không cần làm thủ công

C. Giúp bảo mật mật khẩu của cơ sở dữ liệu khi gửi qua Internet

D. Giúp tự động sửa các lỗi cú pháp trong tập tin models.py.

Câu 7.5 Giả sử bạn đã định nghĩa xong model UserDocument trong tập tin models.py. Để Alembic có thể "nhìn thấy" các định nghĩa này và tự động tạo tập tin script cập nhật, bạn bắt buộc phải thực hiện thao tác nào trong tập tin migrations/env.py

A. Chèn mật khẩu của PostgreSQL vào dòng target_metadata

B. Cài đặt lại thư viện psycopg2-binary

C. Import biến Base từ models.py và gán cho target_metadata = Base.metadata

D. Xóa bỏ thư mục venv và tạo lại từ đầu
-----
Bài sau: Ebook2LateX (8) - Nhập dữ liệu tự động