Chuyển tới nội dung chính

🐍 Ngày 46 Python 365 ngày | Xây dựng ứng dụng web với Django

🗓 Ngày 46: Django — framework "pin kèm mọi thứ"​

Hôm qua bạn học Flask: nhỏ, linh hoạt, nhưng bạn phải tự chọn mọi thứ. Hôm nay ta đi ngược lại: Django mang sẵn ORM, admin, authentication, migrations, form, bảo mật. Triết lý của Django là "batteries included" — pin đã kèm trong hộp.


🎯 Mục tiêu bài học​

  • Hiểu kiến trúc MTV (Model – Template – View) của Django.
  • Tạo project và app đúng chuẩn.
  • Định nghĩa Model và chạy migrations.
  • Đọc/ghi dữ liệu bằng Django ORM.
  • Bật Django Admin để quản trị dữ liệu miễn phí.
  • Viết View + Template hiển thị dữ liệu.

Dự án thực hành: Ứng dụng quản lý danh mục đầu tư — theo dõi các khoản đầu tư và lãi/lỗ.


🧠 Django hoạt động thế nào?​

flowchart LR
A[Browser] -->|URL| B[urls.py]
B --> C[views.py]
C --> D[models.py]
D --> E[(Database)]
C --> F[Template]
F --> A
Thành phầnVai tròTương đương Flask
urls.pyBản đồ URL → view@app.route
views.pyXử lý logic, trả responseHàm route
models.pyĐịnh nghĩa bảng dữ liệuSQLAlchemy model
templates/Giao diệnJinja2
admin.pyGiao diện quản trị tự sinh(không có)
settings.pyCấu hình toàn dự ánapp.config

🧰 Cài đặt và khởi tạo​

python -m venv .venv
.venv\Scripts\activate # Windows

pip install django
django-admin --version # kiểm tra

# Tạo project
django-admin startproject danhmuc .
python manage.py startapp portofolio

Cấu trúc:

danhmuc/
├── manage.py
├── danhmuc/
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── portofolio/
├── models.py
├── views.py
├── urls.py ← tự tạo
├── admin.py
└── migrations/

🔌 Bước 1: Đăng ký app và chạy server​

danhmuc/settings.py:

INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"portofolio", # ← thêm app của bạn
]

LANGUAGE_CODE = "vi"
TIME_ZONE = "Asia/Ho_Chi_Minh"
USE_TZ = True
python manage.py migrate         # tạo bảng hệ thống
python manage.py runserver

Mở http://127.0.0.1:8000 — bạn thấy trang chào mừng Django.


🗄️ Bước 2: Model — định nghĩa dữ liệu bằng Python​

portofolio/models.py:

from django.db import models
from django.contrib.auth.models import User


class TaiSan(models.Model):
"""Một tài sản trong danh mục (cổ phiếu, crypto, vàng...)."""

LOAI_CHON = [
("CP", "Cổ phiếu"),
("CC", "Crypto"),
("VG", "Vàng"),
("TP", "Trái phiếu"),
]

ma = models.CharField("Mã", max_length=20, unique=True)
ten = models.CharField("Tên tài sản", max_length=120)
loai = models.CharField("Loại", max_length=2, choices=LOAI_CHON, default="CP")
ghi_chu = models.TextField("Ghi chú", blank=True)
ngay_tao = models.DateTimeField("Ngày tạo", auto_now_add=True)

class Meta:
verbose_name = "Tài sản"
verbose_name_plural = "Tài sản"
ordering = ["ma"]

def __str__(self):
return f"{self.ma} — {self.ten}"


class GiaoDich(models.Model):
"""Một lần mua/bán."""

LOAI_GD = [("MUA", "Mua"), ("BAN", "Bán")]

tai_san = models.ForeignKey(
TaiSan, on_delete=models.CASCADE, related_name="giao_dich",
verbose_name="Tài sản",
)
loai = models.CharField("Loại giao dịch", max_length=3, choices=LOAI_GD)
so_luong = models.DecimalField("Số lượng", max_digits=18, decimal_places=6)
gia = models.DecimalField("Giá", max_digits=18, decimal_places=2)
phi = models.DecimalField("Phí", max_digits=18, decimal_places=2, default=0)
thoi_diem = models.DateTimeField("Thời điểm")

class Meta:
ordering = ["-thoi_diem"]

@property
def gia_tri(self):
return self.so_luong * self.gia

def __str__(self):
return f"{self.get_loai_display()} {self.so_luong} {self.tai_san.ma}"

Điểm quan trọng:

  • ForeignKey + on_delete — bắt buộc trong Django hiện đại.
  • DecimalField cho tiền — không dùng FloatField cho giá trị tài chính (sai số làm tròn!).
  • choices tự sinh get_<field>_display() và dropdown trong admin.
  • __str__ quyết định cách object hiển thị trong admin.

🔄 Bước 3: Migrations — biến model thành bảng​

python manage.py makemigrations portofolio
python manage.py migrate

Kết quả:

Migrations for 'portofolio':
portofolio/migrations/0001_initial.py
- Create model TaiSan
- Create model GiaoDich

Quy trình bất di bất dịch:

Sửa models.py  →  makemigrations  →  migrate

⚠️ Không bao giờ sửa file trong migrations/ bằng tay. Chúng là lịch sử thay đổi schema — sửa tay sẽ gây lệch giữa các máy.


👑 Bước 4: Django Admin — giao diện quản trị miễn phí​

python manage.py createsuperuser
# nhập username, email, password

portofolio/admin.py:

from django.contrib import admin
from .models import TaiSan, GiaoDich


@admin.register(TaiSan)
class TaiSanAdmin(admin.ModelAdmin):
list_display = ("ma", "ten", "loai", "ngay_tao")
list_filter = ("loai",)
search_fields = ("ma", "ten")


@admin.register(GiaoDich)
class GiaoDichAdmin(admin.ModelAdmin):
list_display = ("tai_san", "loai", "so_luong", "gia", "thoi_diem")
list_filter = ("loai", "tai_san")
date_hierarchy = "thoi_diem"
autocomplete_fields = ("tai_san",)

Mở http://127.0.0.1:8000/admin — bạn có CRUD đầy đủ, tìm kiếm, lọc. Đây là lý do lớn nhất nhiều người chọn Django.


🐍 Bước 5: Django ORM — thao tác dữ liệu không cần SQL​

Mở shell:

python manage.py shell
from portofolio.models import TaiSan, GiaoDich
from django.utils import timezone
from decimal import Decimal

# --- CREATE ---
btc = TaiSan.objects.create(ma="BTC", ten="Bitcoin", loai="CC")
fpt = TaiSan.objects.create(ma="FPT", ten="FPT Corporation", loai="CP")

GiaoDich.objects.create(
tai_san=btc, loai="MUA",
so_luong=Decimal("0.5"), gia=Decimal("62000"),
thoi_diem=timezone.now(),
)

# --- READ ---
TaiSan.objects.all() # tất cả
TaiSan.objects.filter(loai="CC") # lọc
TaiSan.objects.get(ma="BTC") # một bản ghi (lỗi nếu 0 hoặc >1)
TaiSan.objects.exclude(loai="CP") # loại trừ
TaiSan.objects.filter(ten__icontains="bit") # tìm gần đúng, không phân biệt hoa thường
TaiSan.objects.order_by("-ngay_tao")[:5] # sắp xếp + giới hạn

# --- UPDATE ---
btc.ten = "Bitcoin (BTC)"
btc.save()

TaiSan.objects.filter(loai="CC").update(ghi_chu="Tài sản biến động cao")

# --- DELETE ---
TaiSan.objects.filter(ma="XYZ").delete()

# --- Truy vấn ngược qua related_name ---
btc.giao_dich.all() # tất cả giao dịch của BTC

# --- Aggregate ---
from django.db.models import Sum, Count, Avg
GiaoDich.objects.aggregate(tong=Sum("gia"), so_lenh=Count("id"))

# --- Tránh N+1 query ---
GiaoDich.objects.select_related("tai_san").all()

Lookup phổ biến:

LookupÝ nghĩaVí dụ
__exactBằng (mặc định)gia__exact=100
__gt / __ltLớn/nhỏ hơngia__gt=50000
__gte / __lteLớn/nhỏ hơn hoặc bằngso_luong__lte=1
__contains / __icontainsChứa chuỗiten__icontains="bank"
__inNằm trong danh sáchloai__in=["CP","CC"]
__isnullLà NULLghi_chu__isnull=True
__rangeTrong khoảnggia__range=(100, 500)

🖥️ Bước 6: View và URL​

portofolio/views.py:

from django.shortcuts import render, get_object_or_404
from django.db.models import Sum, F, DecimalField, ExpressionWrapper, Count
from .models import TaiSan, GiaoDich


def danh_sach_tai_san(request):
tai_san = TaiSan.objects.annotate(so_giao_dich=Count("giao_dich"))
return render(request, "portofolio/danh_sach.html", {
"tai_san": tai_san,
"tieu_de": "Danh mục tài sản",
})


def chi_tiet_tai_san(request, ma):
ts = get_object_or_404(TaiSan, ma=ma)
giao_dich = ts.giao_dich.select_related("tai_san").all()

# Tổng giá trị đã mua
tong_mua = giao_dich.filter(loai="MUA").aggregate(
t=Sum(ExpressionWrapper(
F("so_luong") * F("gia"),
output_field=DecimalField(max_digits=24, decimal_places=6),
))
)["t"] or 0

return render(request, "portofolio/chi_tiet.html", {
"tai_san": ts,
"giao_dich": giao_dich,
"tong_mua": tong_mua,
})

portofolio/urls.py:

from django.urls import path
from . import views

app_name = "portofolio"

urlpatterns = [
path("", views.danh_sach_tai_san, name="danh_sach"),
path("tai-san/<str:ma>/", views.chi_tiet_tai_san, name="chi_tiet"),
]

danhmuc/urls.py:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path("admin/", admin.site.urls),
path("danh-muc/", include("portofolio.urls")),
]

🎨 Bước 7: Template​

portofolio/templates/portofolio/base.html:

{% load static %}
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Danh mục đầu tư{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/style.css' %}">
</head>
<body>
<nav>
<a href="{% url 'portofolio:danh_sach' %}">Danh mục</a> |
<a href="/admin/">Quản trị</a>
</nav>
<main>{% block noi_dung %}{% endblock %}</main>
</body>
</html>

portofolio/templates/portofolio/danh_sach.html:

{% extends "portofolio/base.html" %}
{% block title %}{{ tieu_de }}{% endblock %}

{% block noi_dung %}
<h1>{{ tieu_de }}</h1>

{% if tai_san %}
<table>
<thead>
<tr><th>Mã</th><th>Tên</th><th>Loại</th><th>Số giao dịch</th><th></th></tr>
</thead>
<tbody>
{% for ts in tai_san %}
<tr>
<td><strong>{{ ts.ma }}</strong></td>
<td>{{ ts.ten }}</td>
<td>{{ ts.get_loai_display }}</td>
<td>{{ ts.so_giao_dich }}</td>
<td><a href="{% url 'portofolio:chi_tiet' ts.ma %}">Xem</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>Chưa có tài sản nào. Hãy thêm trong <a href="/admin/">trang quản trị</a>.</p>
{% endif %}
{% endblock %}

💡 Khác biệt với Flask: Django template không cho gọi hàm có tham số ({{ obj.method(arg) }}), và thẻ {% url 'app:name' %} thay cho url_for. Django ưu tiên an toàn hơn là linh hoạt.


🧪 Bài tập thực hành​

  1. Thêm model DanhMuc để nhóm nhiều tài sản lại (ManyToManyField).
  2. Viết view /thong-ke/ hiển thị tổng giá trị danh mục, tổng lãi/lỗ.
  3. Thêm chức năng thêm giao dịch bằng Django Form (forms.ModelForm).
  4. Dùng ListView và DetailView (class-based views) thay cho function view.
  5. Thêm tìm kiếm theo mã tài sản qua query string ?q=.
  6. Cấu hình django-debug-toolbar và tìm hiểu một vấn đề N+1 query.

⚠️ Lỗi thường gặp​

LỗiNguyên nhânCách sửa
no such tableChưa migratepython manage.py migrate
TemplateDoesNotExistSai cấu trúc thư mụcCần app/templates/app/file.html
AppRegistryNotReadyImport model quá sớmImport trong hàm thay vì đầu file
TemplateSyntaxError: Invalid block tagGõ sai cú phápDjango dùng {% %}, không dùng ( )
Reverse for 'x' not foundSai name= trong urls.pyKiểm tra app_name + tên route
Decimal + float lỗiTrộn float với DecimalDùng Decimal("...") cho mọi giá trị tiền

📝 Ghi chú​

  • Django ORM lazy: QuerySet chưa chạy cho tới khi bạn lặp/list()/len(). Hiểu điều này để tránh query thừa.
  • select_related vs prefetch_related: select_related cho ForeignKey/OneToOne (JOIN), prefetch_related cho ManyToMany (query riêng).
  • settings.py chứa SECRET_KEY — khi deploy phải đọc từ biến môi trường và đặt DEBUG = False.
  • CSRF: Django bật sẵn. Mọi form POST cần {% csrf_token %}.
  • Django migrations không thay thế backup. Trước khi migrate trên production, hãy backup database.

🎯 Tóm tắt ngày 46​

✅ Hiểu kiến trúc MTV: Model → Template → View ✅ Tạo project/app, model với ForeignKey, choices, DecimalField ✅ Chạy migration và hiểu tại sao không sửa tay ✅ Bật Django Admin có tìm kiếm/lọc/CRUD ✅ Thành thạo lookup ORM cơ bản và aggregate ✅ Viết view + template với {% url %} và {% extends %}


📚 Học tiếp​