Flask CRUD SQLAlchemy: Create, Read, Update, Delete

• Python • By TryzTech Team
FlaskPythonBackendWeb Development
Bagikan

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.

Lanjutkan membaca topik yang masih satu konteks.

Jangan Ketinggalan Info Terbaru

Dapatkan artikel teknologi, tips, dan insights menarik langsung ke email Kamu.