Form khai báo field thủ công, không gắn với model — hợp với search, filter, contact form, multi-step wizard. ModelForm sinh field tự động từ model và có sẵn save() ghi DB — hợp với CRUD trực tiếp một model.
class ContactForm(forms.Form):
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea, max_length=2000)
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'body', 'tags'] # whitelist, KHÔNG dùng '__all__'
widgets = {'body': forms.Textarea(attrs={'rows': 10})}
def clean_title(self):
title = self.cleaned_data['title']
if Post.objects.filter(title=title).exists():
raise ValidationError('Title đã tồn tại')
return titleQuy tắc đơn giản: input không map 1-1 với model (nhiều bảng, field tính toán) → dùng Form. CRUD chuẩn một model → dùng ModelForm, tiết kiệm code và tự động đồng bộ với validator của model.
Cẩn thận: đừng bao giờ dùng fields = '__all__' ở ModelForm cho input người dùng — đó là cửa mở cho mass-assignment. Hôm nay model thêm field is_staff hay field internal nào đó là attacker có thể set giá trị qua form public mà bạn không hề biết.
Form declares fields manually, not tied to a model — fits search, filters, contact forms, multi-step wizards. ModelForm generates fields from a model, ships with save() to write to the DB — fits direct CRUD for one model.
class ContactForm(forms.Form):
email = forms.EmailField()
message = forms.CharField(widget=forms.Textarea, max_length=2000)
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'body', 'tags'] # whitelist, NEVER use '__all__'
widgets = {'body': forms.Textarea(attrs={'rows': 10})}
def clean_title(self):
title = self.cleaned_data['title']
if Post.objects.filter(title=title).exists():
raise ValidationError('Title already exists')
return titleUse Form when input does not map 1-to-1 to a model (multi-table, computed fields). Use ModelForm for standard CRUD — saves code and aligns with model validators.
Pitfall: Never use fields = '__all__' for user-facing ModelForm — that opens the door to mass-assignment. You silently expose is_staff, is_superuser, or internal fields the day they are added to the model.