Python 8 — Làm việc với SQL, API & Async
Trong data engineering, ba kỹ năng xuất hiện gần như mỗi ngày: đọc/ghi cơ sở dữ liệu, gọi REST API để lấy dữ liệu từ dịch vụ bên ngoài, và chạy nhiều tác vụ I/O song song để pipeline không bị nghẽn. Bài này đi sâu cả ba, với ví dụ thực hành và những "cạm bẫy" hay gặp.
1. Kết nối cơ sở dữ liệu
1.1. DB-API: lớp nền tảng
Python chuẩn hoá cách nói chuyện với CSDL qua DB-API 2.0 (PEP 249). Mọi driver (sqlite3, psycopg2 cho PostgreSQL, pymysql cho MySQL) đều có cùng bộ khái niệm: connection, cursor, execute, fetchone/fetchall, commit.
import sqlite3
conn = sqlite3.connect("warehouse.db")
cur = conn.cursor()
cur.execute("""
CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY,
user_id INTEGER,
action TEXT,
ts TEXT
)
""")
cur.execute(
"INSERT INTO events (user_id, action, ts) VALUES (?, ?, ?)",
(42, "login", "2026-06-30T08:00:00"),
)
conn.commit() # bắt buộc để lưu thay đổi
cur.execute("SELECT id, action FROM events WHERE user_id = ?", (42,))
for row in cur.fetchall():
print(row) # (1, 'login')
conn.close()
Ba điểm cần nhớ:
commit()mới ghi thật. Không commit, dữ liệu mất khi đóng kết nối.cursorlà con trỏ duyệt kết quả, không phải kết quả.- Luôn
close()hoặc dùng context manager để trả tài nguyên.
1.2. Tham số hoá — chống SQL injection
Đây là quy tắc không bao giờ phá vỡ: không bao giờ ghép chuỗi để dựng câu SQL.
# SAI — lỗ hổng SQL injection
action = "login'; DROP TABLE events; --"
cur.execute(f"SELECT * FROM events WHERE action = '{action}'") # NGUY HIỂM
# ĐÚNG — để driver chèn giá trị an toàn
cur.execute("SELECT * FROM events WHERE action = ?", (action,))
Khi tham số hoá, driver gửi câu lệnh và dữ liệu tách riêng; CSDL không bao giờ hiểu dữ liệu thành mã. Lưu ý placeholder khác nhau theo driver: sqlite3 dùng ?, psycopg2 dùng %s.
1.3. SQLAlchemy Core — engine và SQL biểu đạt
sqlite3 đủ cho việc nhỏ, nhưng dự án thật thường dùng SQLAlchemy vì nó che giấu khác biệt giữa các CSDL và quản lý connection pool.
Engine là điểm vào trung tâm — nó giữ pool kết nối và biết phương ngữ (dialect) của CSDL.
from sqlalchemy import create_engine, text
# Chuỗi kết nối: dialect+driver://user:pass@host:port/dbname
engine = create_engine("postgresql+psycopg2://etl:secret@localhost:5432/dw")
# Ví dụ SQLite dùng ngay dưới đây:
engine = create_engine("sqlite:///warehouse.db", echo=False)
with engine.connect() as conn:
result = conn.execute(
text("SELECT action, COUNT(*) AS n FROM events WHERE user_id = :uid GROUP BY action"),
{"uid": 42},
)
for row in result:
print(row.action, row.n) # truy cập theo tên cột
conn.commit()
Với SQLAlchemy Core, placeholder là :tên và bạn truyền dict. Vẫn an toàn injection y như DB-API.
1.4. SQLAlchemy ORM — model, session, query
ORM ánh xạ bảng thành lớp Python và dòng thành đối tượng. Bạn làm việc với object thay vì câu SQL thô.
from datetime import datetime
from sqlalchemy import String, Integer, DateTime
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
class Base(DeclarativeBase):
pass
class Event(Base):
__tablename__ = "events"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, index=True)
action: Mapped[str] = mapped_column(String(50))
ts: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
def __repr__(self) -> str:
return f"<Event {self.action} u={self.user_id}>"
Base.metadata.create_all(engine) # tạo bảng nếu chưa có
Mọi thao tác ORM diễn ra trong một Session — đơn vị công việc gom các thay đổi rồi flush xuống CSDL khi commit.
with Session(engine) as session:
# Thêm
session.add(Event(user_id=42, action="login"))
session.add_all([
Event(user_id=42, action="view"),
Event(user_id=7, action="login"),
])
session.commit()
# Truy vấn (API 2.0)
from sqlalchemy import select
stmt = select(Event).where(Event.user_id == 42).order_by(Event.id)
for ev in session.scalars(stmt):
print(ev)
# Cập nhật: sửa thuộc tính rồi commit
ev = session.get(Event, 1)
ev.action = "login_ok"
session.commit()
# Xoá
session.delete(ev)
session.commit()
Core hay ORM? Core gần SQL, kiểm soát cao, hợp cho ETL aggregate lớn. ORM tiện cho logic nghiệp vụ với nhiều quan hệ giữa bảng. Trong data engineering, nhiều pipeline dùng Core (hoặc thậm chí SQL thuần) cho hiệu năng, và ORM cho phần metadata/cấu hình.
2. Gọi REST API
2.1. requests — thư viện kinh điển
import requests
resp = requests.get(
"https://api.example.com/v1/users",
params={"role": "analyst", "active": "true"},
headers={"Authorization": "Bearer TOKEN", "Accept": "application/json"},
timeout=10, # LUÔN đặt timeout
)
# Kiểm tra status code
if resp.status_code == 200:
data = resp.json() # parse JSON thành dict/list
print(data["users"][0])
else:
print("Lỗi:", resp.status_code, resp.text)
POST gửi dữ liệu JSON:
resp = requests.post(
"https://api.example.com/v1/events",
json={"user_id": 42, "action": "purchase"}, # json= tự set Content-Type
headers={"Authorization": "Bearer TOKEN"},
timeout=10,
)
resp.raise_for_status() # ném exception nếu 4xx/5xx
created = resp.json()
2.2. Xử lý lỗi & status code đúng cách
API thật sẽ thất bại — mạng chập chờn, server quá tải, token hết hạn. Code production cần phòng thủ.
import requests
from requests.exceptions import RequestException
def fetch_users(page: int) -> dict:
try:
resp = requests.get(
"https://api.example.com/v1/users",
params={"page": page},
timeout=10,
)
resp.raise_for_status() # 4xx/5xx -> HTTPError
return resp.json()
except requests.Timeout:
raise RuntimeError(f"Timeout ở trang {page}")
except requests.HTTPError as e:
# phân biệt lỗi client vs server
code = e.response.status_code
if code == 429:
raise RuntimeError("Bị rate limit")
raise RuntimeError(f"HTTP {code}: {e.response.text[:200]}")
except RequestException as e:
raise RuntimeError(f"Lỗi mạng: {e}")
Ý nghĩa nhóm status code: 2xx thành công, 3xx chuyển hướng, 4xx lỗi phía bạn (401 chưa xác thực, 403 không có quyền, 404 không tồn tại, 429 quá nhiều request), 5xx lỗi phía server (nên retry).
2.3. Phân trang & rate limit
Hầu hết API giới hạn số bản ghi mỗi lần trả về. Bạn phải lặp qua các trang.
import time
import requests
def fetch_all(url: str, token: str) -> list[dict]:
items, page = [], 1
while True:
resp = requests.get(
url,
params={"page": page, "per_page": 100},
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
if resp.status_code == 429:
# tôn trọng header Retry-After nếu có
wait = int(resp.headers.get("Retry-After", "5"))
time.sleep(wait)
continue
resp.raise_for_status()
batch = resp.json()["data"]
if not batch:
break # hết dữ liệu
items.extend(batch)
page += 1
time.sleep(0.2) # giãn nhịp, tránh chạm rate limit
return items
2.4. httpx — kế thừa requests, hỗ trợ async
httpx có API gần giống requests nhưng dùng được cả đồng bộ lẫn bất đồng bộ — điểm này quan trọng cho phần sau.
import httpx
# Đồng bộ — dùng Client để tái sử dụng kết nối
with httpx.Client(timeout=10) as client:
r = client.get("https://api.example.com/v1/users", params={"page": 1})
r.raise_for_status()
print(r.json())
3. Lập trình bất đồng bộ với asyncio
3.1. Vì sao cần async cho I/O-bound
Khi gọi 100 API, phần lớn thời gian là chờ mạng phản hồi, CPU gần như ngồi không. Code đồng bộ gọi tuần tự: trang 2 chỉ bắt đầu khi trang 1 xong. Async cho phép, trong lúc chờ một request, máy đi gửi request khác.
- I/O-bound (mạng, đĩa, CSDL) → async ăn điểm lớn.
- CPU-bound (tính toán nặng) → async không giúp; cần
multiprocessing.
Async không phải đa luồng thật: nó chạy trên một luồng, dùng event loop điều phối, "nhường lượt" tại mỗi điểm await khi tác vụ đang chờ.
3.2. async/await và event loop
async def tạo coroutine — hàm có thể tạm dừng. await nói "tạm dừng tại đây cho tới khi việc này xong, trong lúc đó loop làm việc khác".
import asyncio
async def say(msg: str, delay: float) -> str:
await asyncio.sleep(delay) # mô phỏng chờ I/O, không block luồng
print(msg)
return msg
async def main():
# await tuần tự — mất ~3s
await say("một", 1)
await say("hai", 2)
asyncio.run(main()) # khởi tạo event loop và chạy coroutine gốc
Chỉ await thôi vẫn tuần tự. Sức mạnh đến khi chạy song song.
3.3. Gọi nhiều API song song với asyncio.gather
import asyncio
import httpx
async def fetch(client: httpx.AsyncClient, url: str) -> dict:
r = await client.get(url, timeout=10)
r.raise_for_status()
return r.json()
async def fetch_many(urls: list[str]) -> list[dict]:
async with httpx.AsyncClient() as client:
tasks = [fetch(client, u) for u in urls]
# gather chạy tất cả đồng thời, trả kết quả theo đúng thứ tự
return await asyncio.gather(*tasks)
urls = [f"https://api.example.com/v1/page/{i}" for i in range(1, 21)]
results = asyncio.gather # minh hoạ
data = asyncio.run(fetch_many(urls))
print(f"Lấy {len(data)} trang")
20 request I/O-bound: tuần tự có thể mất 20×0.5s = 10s; song song chỉ tốn cỡ thời gian của request chậm nhất. Để tránh "bắn" quá nhiều request cùng lúc và bị rate limit, giới hạn đồng thời bằng asyncio.Semaphore:
async def fetch_limited(sem, client, url):
async with sem: # tối đa N request cùng lúc
return await fetch(client, url)
async def fetch_throttled(urls, concurrency=5):
sem = asyncio.Semaphore(concurrency)
async with httpx.AsyncClient() as client:
tasks = [fetch_limited(sem, client, u) for u in urls]
return await asyncio.gather(*tasks)
Nếu muốn lỗi một task không làm hỏng cả nhóm, dùng asyncio.gather(*tasks, return_exceptions=True) — kết quả sẽ chứa cả object exception để bạn xử lý từng cái.
4. Kiểm thử với pytest
Pipeline đáng tin cậy cần test. pytest là chuẩn de facto: hàm bắt đầu bằng test_, dùng assert thuần.
# file: test_pipeline.py
def parse_event(raw: dict) -> dict:
return {"user": raw["user_id"], "action": raw["action"].lower()}
def test_parse_event():
out = parse_event({"user_id": 42, "action": "LOGIN"})
assert out == {"user": 42, "action": "login"}
def test_parse_event_missing_key():
import pytest
with pytest.raises(KeyError):
parse_event({"user_id": 42})
Chạy: pytest -v. Vài kỹ thuật hay dùng:
import pytest
# Tham số hoá: một test, nhiều bộ dữ liệu
@pytest.mark.parametrize("inp,expected", [
("LOGIN", "login"),
("View", "view"),
])
def test_normalize(inp, expected):
assert inp.lower() == expected
# Fixture: chuẩn bị tài nguyên dùng chung
@pytest.fixture
def sample_rows():
return [{"user_id": 1, "action": "X"}, {"user_id": 2, "action": "Y"}]
def test_count(sample_rows):
assert len(sample_rows) == 2
Khi test code gọi API, đừng gọi mạng thật — hãy mock (giả lập) phản hồi để test nhanh và ổn định. Với code async, dùng pytest-asyncio và đánh dấu @pytest.mark.asyncio.
Tóm tắt
- DB-API là nền chung; luôn tham số hoá câu lệnh (
?,:name,%s) để chặn SQL injection, đừng bao giờ ghép chuỗi. - SQLAlchemy Core cho SQL biểu đạt với
engine+ connection pool; ORM ánh xạ bảng thành lớp, làm việc quaSessionvới add/query/commit. - requests gọi REST: đặt
timeout, kiểm tra status code, dùngraise_for_status(); xử lý phân trang và rate limit (429,Retry-After). - httpx giống requests nhưng hỗ trợ async.
- asyncio dùng một event loop,
async/awaitnhường lượt tại điểm chờ — cực hợp với tác vụ I/O-bound;asyncio.gatherchạy nhiều request song song,Semaphoregiới hạn đồng thời. - pytest kiểm thử bằng
assert, fixture và parametrize; mock API thay vì gọi mạng thật.
Tự kiểm tra
- Vì sao ghép chuỗi để dựng SQL nguy hiểm, và tham số hoá khắc phục thế nào?
- Phân biệt vai trò của
Engine,Sessionvàmodeltrong SQLAlchemy ORM. resp.raise_for_status()làm gì, và status 429 nghĩa là gì? Bạn nên phản ứng ra sao?- Khi nào async giúp tăng tốc, khi nào không? Giải thích bằng khái niệm I/O-bound vs CPU-bound.
asyncio.gatherkhác gì so với việcawaittừng coroutine một cách tuần tự?- Vì sao nên mock API trong test thay vì gọi endpoint thật?
Đọc tiếp
- Ôn lại nền tảng tại Python 1 — Giới thiệu.
- Tiếp theo, đào sâu phần SQL (truy vấn nâng cao, JOIN, window function, tối ưu) để khai thác tối đa CSDL mà bạn vừa học cách kết nối từ Python.
Bài viết liên quan
Vì sao Python là ngôn ngữ số một của data engineer: vai trò trong pipeline (ingest/transform/orchestrate), hệ sinh thái thư viện (pandas/polars/pyarrow/sqlalchemy), quản lý môi trường (venv/uv/poetry), và khi nào dùng Python vs SQL/Spark.
Định nghĩa hàm, tham số, *args/**kwargs, lambda, module/package, pip và virtualenv.
Lớp, kế thừa, đa hình, dunder methods, dataclass, type hints và nguyên tắc viết code sạch.
Vì sao Python, cài đặt, chạy script vs REPL, biến và kiểu dữ liệu cơ bản, quy ước code.