Menghubungkan Flask dengan database melalui Flask-SQLAlchemy 3.x: mendefinisikan models dan relationships, memahami session scope dan commit/rollback, serta membangun operasi CRUD penuh dengan SQLite atau PostgreSQL.

Sampai episode 9, semua data aplikasi kita hidup di memori — hilang setiap server restart. Untuk aplikasi sungguhan, data harus persisten: tersimpan di database dan bisa dibaca kapan saja. Episode 10 memperkenalkan SQLAlchemy, ORM (Object-Relational Mapper) paling populer di Python, dalam balutan resmi Flask-SQLAlchemy. Kita akan membahas models dan relationships, memahami session scope — salah satu konsep yang paling sering disalahpahami — dan membangun CRUD yang menjadi fondasi semua fitur data di series ini.
SQLAlchemy menerjemahkan objek Python menjadi baris tabel dan sebaliknya. Alih-alih menulis SQL mentah SELECT * FROM users WHERE email = 'a@b.c';, kalian menulis User.query.filter_by(email="a@b.c").first(). Keuntungannya: portability (satu kode untuk SQLite, PostgreSQL, MySQL), type safety (hasil query jadi objek), dan keamanan — query dibangun lewat parameter binding, sehingga terhindar dari SQL injection (detail di episode 17).
Install lalu daftarkan ke aplikasi dengan pola extension dari episode 9:
pip install "flask-sqlalchemy>=3.1"from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()from app.extensions import db
def create_app(config) -> Flask:
app = Flask(__name__)
app.config.from_object(config)
db.init_app(app)
from app import models
with app.app_context():
db.create_all()
return appdb.create_all() membuat tabel dari semua model yang terdaftar — cukup untuk development. Untuk versioning schema yang benar, kita pindah ke Alembic di episode 11.
Model adalah class Python yang memetakan satu tabel. Contoh user model:
from datetime import datetime
from app.extensions import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def __repr__(self) -> str:
return f"<User {self.username}>"Setiap db.Column punya tipe (Integer, String, DateTime) dan constraint (unique, nullable, primary_key). Model ini adalah satu-satunya tempat "skema" kalian didefinisikan — database mengikuti model, bukan sebaliknya.
Hubungan antar tabel dinyatakan dengan db.relationship:
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(120), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
author = db.relationship("User", back_populates="posts")
# tambahkan di User:
# posts = db.relationship("Post", back_populates="author", lazy="dynamic")db.ForeignKey("user.id") menyimpan referensi di level kolom; db.relationship menghadirkan kenyamanan di level objek — post.author dan user.posts berfungsi tanpa query manual. lazy="dynamic" membuat user.posts menjadi query (bisa di-chaining, di-filter).
Ini konsep yang paling sering disalahpahami. SQLAlchemy memakai pola unit of work: semua perubahan dikumpulkan di session, lalu dikirim sekali ke database dengan commit().
@app.post("/users")
def create_user() -> tuple[dict, int]:
data = request.get_json()
user = User(username=data["username"], email=data["email"])
db.session.add(user)
db.session.commit()
return jsonify({"id": user.id, "username": user.username}), 201db.session.add() menandai objek untuk disimpan; db.session.commit() mengirim semua perubahan sekaligus. Kalau ada error di tengah, db.session.rollback() mengembalikan session ke kondisi bersih.
Warning
Jangan memakai db.session sebagai global yang dibagikan antar thread — session Flask-SQLAlchemy otomatis scoped ke request saat ini, tetapi kalian tetap harus commit/rollback dengan disiplin di akhir request. Kebiasaan buruk: memanggil commit() di dalam loop untuk setiap objek. Satu commit per request untuk perubahan batch; ukur ulang jika butuh per-object (kasus jarang).
from flask import Blueprint, jsonify, request
from app.extensions import db
from app.models.user import User
main_bp = Blueprint("main", __name__)
@main_bp.get("/users")
def list_users() -> dict:
users = db.session.scalars(
db.select(User).order_by(User.created_at.desc())
).all()
return jsonify([{"id": u.id, "username": u.username} for u in users])
@main_bp.get("/users/<int:user_id>")
def get_user(user_id: int) -> tuple[dict, int]:
user = db.session.get(User, user_id)
if user is None:
return jsonify({"error": "user tidak ditemukan"}), 404
return jsonify({"id": user.id, "username": user.username})
@main_bp.post("/users")
def create_user() -> tuple[dict, int]:
data = request.get_json(silent=True) or {}
if not data.get("username") or not data.get("email"):
return jsonify({"error": "username dan email wajib"}), 400
user = User(username=data["username"], email=data["email"])
db.session.add(user)
db.session.commit()
return jsonify({"id": user.id, "username": user.username}), 201Endpoint PUT dan DELETE mengikuti pola yang sama: db.session.get(User, user_id) untuk memuat, mutasi atribut, db.session.commit() — atau db.session.delete(user) untuk menghapus. Perhatikan pola modern db.select(User) (SQLAlchemy 2.0+) dibanding User.query gaya lama. Untuk aplikasi production, validasi data (episode 12), penanganan duplicate/constraint error, dan pagination (episode 14) akan menyempurnakan kode ini.
Sepanjang series ini kalian bisa memilih database:
| Kriteria | SQLite | PostgreSQL |
|---|---|---|
| Setup | Tanpa server, satu file | Butuh server/container |
| Cocok untuk | Development, lab, prototipe | Produksi, data banyak, multi-user |
| Konkurensi tulis | Terbatas (satu writer) | Baik (MVCC) |
| Fitur lanjutan | Minim | JSONB, full-text, extensions |
Satu kode model, dua backend berbeda — ubah database hanya dengan mengganti URI di config (DevelopmentConfig memakai sqlite:///dev.db, ProductionConfig membaca DATABASE_URL dari environment). Mulai lab dengan SQLite, dan saat deploy (episode 22) tinggal set DATABASE_URL ke PostgreSQL.
commit(): perubahan tidak pernah sampai ke database — paling umum di pemula.db.create_all() tidak menambah kolom baru: untuk skema berubah, wajib migrations (episode 11).user.posts per user dalam loop memicu query per objek — pakai joinedload/selectinload saat perlu.email melempar IntegrityError; tangkap dan kembalikan respons 409 yang benar.Pada episode 10 ini, kalian telah menghubungkan Flask dengan database sungguhan.
Inti yang harus dibawa pulang:
db = SQLAlchemy() + db.init_app(app) di factory.db.relationship dan db.ForeignKey menghadirkan relasi.db.session.add() → commit() → rollback() saat error; satu commit per request.db.select() modern; validasi manual untuk sementara.Di episode 11 selanjutnya, kita menata skema yang berubah-ubah: migrations dengan Alembic — Flask-Migrate, init/migrate/upgrade, data migration, dan versioning schema yang benar untuk produksi. Sampai jumpa di episode 11!