Di tutorial sebelumnya, kita sudah belajar mengelola database SQLite dengan modul sqlite3 bawaan Python. Menulis query SQL mentah memang fleksibel, tapi begitu aplikasi makin besar, kode jadi rawan error karena salah ketik nama kolom atau lupa menutup tanda kutip. Di sinilah SQLAlchemy masuk: library paling populer di ekosistem Python untuk bekerja dengan database lewat pendekatan Object Relational Mapping (ORM). Dalam tutorial ini kamu akan belajar SQLAlchemy dari nol sampai bisa melakukan CRUD lengkap beserta relasi antar tabel.

Apa Itu SQLAlchemy dan Kenapa Kamu Perlu Menggunakannya?

SQLAlchemy adalah toolkit SQL dan ORM untuk Python. ORM memungkinkan kamu memetakan tabel database menjadi class Python, dan baris data menjadi objek. Dengan kata lain, kamu berinteraksi dengan database menggunakan sintaks Python biasa, bukan string SQL. Beberapa keuntungan utamanya:

  • Aman dari SQL injection — parameter query otomatis di-escape.
  • Portable — ganti database dari SQLite ke PostgreSQL cukup dengan mengubah satu baris koneksi.
  • Lebih mudah dibaca — relasi antar tabel diekspresikan lewat atribut objek, bukan JOIN manual.
  • Terintegrasi baik dengan framework seperti Flask dan FastAPI.

Instalasi SQLAlchemy

Instalasi cukup dengan satu perintah. Pastikan kamu sudah berada di dalam virtual environment (kamu bisa pakai uv atau venv):

bash
pip install sqlalchemy

Kita akan menggunakan SQLite pada tutorial ini karena tidak butuh server eksternal — SQLAlchemy otomatis bisa menggunakannya tanpa install tambahan.

Membuat Engine dan Koneksi ke Database

Langkah pertama adalah membuat engine, yaitu objek yang mengelola koneksi ke database. URL koneksi mengikuti pola dialek+driver://. Untuk SQLite, kita cukup menulis nama file database-nya:

python
from sqlalchemy import create_engine

# echo=True menampilkan query SQL yang dijalankan (berguna saat belajar)
engine = create_engine("sqlite:///perpustakaan.db", echo=False)

Mendefinisikan Model dengan Declarative Base

Model adalah class Python yang merepresentasikan tabel. Kita akan membuat dua tabel dengan relasi one-to-many: satu Penulis bisa punya banyak Buku. Gunakan style SQLAlchemy 2.0 dengan Mapped dan mapped_column agar lebih eksplisit dan terdukung type checker:

python
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship


class Base(DeclarativeBase):
    pass


class Penulis(Base):
    __tablename__ = "penulis"

    id: Mapped[int] = mapped_column(primary_key=True)
    nama: Mapped[str] = mapped_column(String(100), nullable=False)

    # relasi: satu penulis punya banyak buku
    buku = relationship("Buku", back_populates="penulis")

    def __repr__(self):
        return f"<Penulis(nama='{self.nama}')>"


class Buku(Base):
    __tablename__ = "buku"

    id: Mapped[int] = mapped_column(primary_key=True)
    judul: Mapped[str] = mapped_column(String(200), nullable=False)
    tahun_terbit: Mapped[int]
    penulis_id: Mapped[int] = mapped_column(ForeignKey("penulis.id"))

    # relasi: setiap buku punya satu penulis
    penulis = relationship("Penulis", back_populates="buku")

    def __repr__(self):
        return f"<Buku(judul='{self.judul}')>"

Perhatikan ForeignKey("penulis.id") pada kolom penulis_id. Ini yang mendefinisikan relasi antar tabel di level database, sedangkan relationship() memungkinkan kita mengakses objek terkait lewat atribut Python (misalnya buku.penulis.nama).

Membuat Tabel di Database

Setelah model didefinisikan, buat tabelnya dengan create_all(). Perintah ini hanya membuat tabel yang belum ada, jadi aman dijalankan berulang kali:

python
Base.metadata.create_all(engine)

Tips: create_all() tidak akan memperbarui struktur tabel yang sudah ada. Untuk manajemen skema yang lebih matang (migration), gunakan Alembictool migration resmi dari proyek SQLAlchemy.

Operasi CRUD: Create, Read, Update, Delete

Semua operasi database dilakukan melalui Session, yaitu unit kerja yang mengelola transaksi. Pola yang disarankan adalah menggunakan sessionmaker dan context manager agar session otomatis tertutup dengan benar. Mari kita buat data pertama:

python
from sqlalchemy.orm import sessionmaker

SessionLocal = sessionmaker(bind=engine)

with SessionLocal() as session:
    # CREATE: tambah penulis dan buku
    penulis = Penulis(nama="Andrea Hirata")
    session.add(penulis)
    session.commit()  # simpan penulis dulu agar dapat id

    buku = Buku(judul="Laskar Pelangi", tahun_terbit=2005, penulis=penulis)
    session.add(buku)
    session.commit()

    print(f"Buku tersimpan: {buku}")

Penting: perubahan hanya tersimpan ke database setelah kamu memanggil session.commit(). Kalau lupa, data akan hilang begitu session ditutup.

Sekarang mari baca, perbarui, dan hapus data:

python
from sqlalchemy import select

with SessionLocal() as session:
    # READ: ambil semua buku
    semua_buku = session.scalars(select(Buku)).all()
    for b in semua_buku:
        print(f"{b.judul} oleh {b.penulis.nama}")

    # UPDATE: ubah tahun terbit
    buku = session.scalars(select(Buku).where(Buku.judul == "Laskar Pelangi")).one()
    buku.tahun_terbit = 2005
    session.commit()

    # DELETE: hapus buku
    session.delete(buku)
    session.commit()

Query Data dengan Filter dan Ordering

SQLAlchemy 2.0 menganjurkan gaya query select() yang mirip SQL asli. Kamu bisa menggabungkan filter, pengurutan, dan limit dengan mudah:

python
with SessionLocal() as session:
    # Filter: buku terbit tahun 2000 ke atas, urut berdasarkan judul
    hasil = session.scalars(
        select(Buku)
        .where(Buku.tahun_terbit >= 2000)
        .order_by(Buku.judul)
        .limit(10)
    ).all()

    for b in hasil:
        print(b.judul, b.tahun_terbit)

    # Hitung jumlah buku
    total = session.scalar(select(func.count()).select_from(Buku))
    print(f"Total buku: {total}")

Relasi Antar Tabel (One-to-Many)

Berkat relationship() yang sudah kita definisikan di model, mengambil data relasi jadi sangat natural — tanpa menulis JOIN sama sekali:

python
with SessionLocal() as session:
    penulis = session.scalars(
        select(Penulis).where(Penulis.nama == "Andrea Hirata")
    ).one()

    # akses semua buku milik penulis ini
    for b in penulis.buku:
        print(f"- {b.judul} ({b.tahun_terbit})")

    # dan sebaliknya: dari buku ke penulisnya
    buku = session.scalars(select(Buku)).first()
    print(buku.penulis.nama)

SQLAlchemy otomatis menjalankan query tambahan yang dibutuhkan saat kamu mengakses penulis.buku atau buku.penulis — ini disebut lazy loading.

Kesimpulan

Selamat! Kamu sudah memahami fondasi SQLAlchemy ORM: membuat engine, mendefinisikan model, melakukan CRUD, menulis query dengan select(), dan mengelola relasi antar tabel. Keterampilan ini sangat berguna ketika membangun API, misalnya dengan FastAPI yang sering dipadukan dengan SQLAlchemy. Untuk memastikan kode kamu bebas bug, biasakan menulis test dengan pytest. Selamat berkoding!