Flask CRUD SQLAlchemy: Create, Read, Update, Delete
CRUD adalah singkatan dari Create, Read, Update, dan Delete. Empat operasi ini terdengar sederhana, tetapi di sinilah route, form, validasi, database, dan template mulai bekerja sebagai satu alur utuh.
Kita akan membuat CRUD note dengan model Note dan blueprint notes_bp dari pembahasan sebelumnya. Setiap perubahan data diakhiri dengan redirect, sehingga refresh browser tidak mengulang operasi terakhir.
Route untuk Empat Operasi
Simpan route berikut di routes/notes.py:
# File: routes/notes.py
from flask import Blueprint, flash, redirect, render_template, request, url_for
from models import Note, db
notes_bp = Blueprint("notes", __name__, url_prefix="/notes")
@notes_bp.get("")
def index():
query = db.select(Note).order_by(Note.id.desc())
notes = db.session.execute(query).scalars().all()
return render_template("notes/index.html", notes=notes)
@notes_bp.post("")
def store():
title = request.form.get("title", "").strip()
if not title:
flash("Judul note wajib diisi.", "error")
return redirect(url_for("notes.index"))
note = Note(title=title)
db.session.add(note)
db.session.commit()
flash("Note berhasil dibuat.", "success")
return redirect(url_for("notes.index"))
@notes_bp.get("/<int:note_id>/edit")
def edit(note_id):
note = db.get_or_404(Note, note_id)
return render_template("notes/edit.html", note=note)
@notes_bp.post("/<int:note_id>")
def update(note_id):
note = db.get_or_404(Note, note_id)
title = request.form.get("title", "").strip()
if not title:
flash("Judul note wajib diisi.", "error")
return redirect(url_for("notes.edit", note_id=note.id))
note.title = title
db.session.commit()
flash("Note berhasil diperbarui.", "success")
return redirect(url_for("notes.index"))
@notes_bp.post("/<int:note_id>/delete")
def destroy(note_id):
note = db.get_or_404(Note, note_id)
db.session.delete(note)
db.session.commit()
flash("Note berhasil dihapus.", "success")
return redirect(url_for("notes.index"))
Route index() menjalankan query dan mengurutkan ID terbesar lebih dulu, sehingga note terbaru muncul di atas. .scalars().all() mengubah hasil SQLAlchemy menjadi list object Note yang siap dikirim ke template.
Pada store(), object baru perlu ditambahkan dengan db.session.add() sebelum commit. Sementara itu, update() tidak memanggil add() lagi karena object note sudah dimuat dan dipantau oleh session. Mengubah note.title lalu melakukan commit sudah cukup.
db.get_or_404(Note, note_id) mencari row berdasarkan primary key. Jika tidak ditemukan, Flask langsung mengembalikan halaman 404. Tanpa penanganan ini, kode bisa mencoba membaca attribute dari None dan berakhir sebagai error 500.
Form List dan Delete
Template index menampilkan form create, daftar note, link edit, dan tombol delete:
<!-- File: templates/notes/index.html -->
<h1>Notes</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<p class="message {{ category }}">{{ message }}</p>
{% endfor %}
{% endwith %}
<form method="post" action="{{ url_for('notes.store') }}">
<label for="title">Judul note</label>
<input id="title" name="title" maxlength="120" required>
<button type="submit">Tambah</button>
</form>
<ul>
{% for note in notes %}
<li>
{{ note.title }}
<a href="{{ url_for('notes.edit', note_id=note.id) }}">Edit</a>
<form method="post"
action="{{ url_for('notes.destroy', note_id=note.id) }}">
<button type="submit">Hapus</button>
</form>
</li>
{% else %}
<li>Belum ada note.</li>
{% endfor %}
</ul>
Delete sengaja memakai form POST, bukan link GET. Request GET seharusnya aman dibuka tanpa mengubah data. Memang HTML belum mendukung method DELETE secara langsung, jadi route web tradisional sering memakai POST untuk aksi delete.
Halaman Edit
Form edit menampilkan judul lama sebagai nilai awal:
<!-- File: templates/notes/edit.html -->
<h1>Edit Note</h1>
<form method="post" action="{{ url_for('notes.update', note_id=note.id) }}">
<label for="title">Judul note</label>
<input id="title" name="title" value="{{ note.title }}" maxlength="120" required>
<button type="submit">Simpan perubahan</button>
</form>
<a href="{{ url_for('notes.index') }}">Kembali</a>
Jinja akan melakukan escaping pada note.title sebelum memasukkannya ke HTML. Meski begitu, backend tetap harus memvalidasi panjang dan aturan isi karena escaping hanya menangani keamanan tampilan, bukan kualitas data.
Commit, Error, dan Transaksi
Setiap route di atas hanya melakukan satu commit. Ini membuat batas transaksinya mudah dipahami: seluruh perubahan berhasil bersama-sama atau gagal sebelum response sukses dikirim.
Pada aplikasi yang lebih serius, tangani error database dan panggil db.session.rollback() sebelum melanjutkan request lain. Tanpa rollback, session yang baru mengalami kegagalan dapat tetap berada dalam keadaan error.
Form create dan delete juga perlu token CSRF sebelum dipakai di production. Method POST mencegah penghapusan lewat link biasa, tetapi belum membuktikan bahwa request benar-benar berasal dari form aplikasi Kamu.
Coba lengkapi versi ini dengan field body, halaman detail, dan konfirmasi sebelum delete. Setelah itu uji ID yang tidak ada. CRUD yang sehat bukan hanya bisa menyimpan data; ia juga memberi respons yang masuk akal saat input kosong atau resource sudah tidak ditemukan.
Artikel Terkait
Lanjutkan membaca topik yang masih satu konteks.
Flask Authentication: Login, Session, dan Logout
Buat authentication Flask dengan password hash, login session, current user, protected route, dan logout tanpa menyimpan password mentah.
Flask SQLAlchemy: Koneksi Database dan Model
Hubungkan Flask ke SQLite dengan Flask-SQLAlchemy, buat model dan tabel, lalu pahami proses add, commit, serta query data.
Struktur Project Flask: Blueprint, Template, dan Config
Rapikan struktur project Flask dengan application factory, Blueprint, folder route, template, static files, dan config yang terpisah.
Flask Static Files: CSS, JavaScript, dan Asset
Atur static files Flask untuk CSS, JavaScript, dan gambar, lalu panggil setiap asset dengan url_for agar path tetap mudah dirawat.