Chapter 15: Django Update Model
Grab your second cup of chai because we’re now diving deep into updating models in Django — not just updating data (records), but actually changing the structure of your models themselves (adding fields, changing types, renaming columns, adding relationships, etc.).
This is a very different topic from the previous “update data” lesson.
Today we are talking about:
How to safely evolve your database schema when your app requirements change (adding a new field, making a field nullable, changing max_length, adding indexes, splitting models, etc.)
This process is called migrations — and it’s one of the most powerful (and sometimes most frustrating) features of Django.
We will go very slowly, step by step, like I’m sitting next to you in Gachibowli looking at your screen, explaining every decision, every warning message, and every common panic moment beginners face.
Current Situation (Your Starting Point)
You already have these models in polls/models.py:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Question(models.Model): question_text = models.CharField(max_length=200) pub_date = models.DateTimeField("date published") # ... __str__, was_published_recently, Meta ... class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) # ... __str__, Meta ... |
And you have already run makemigrations and migrate at least once → you have migration file 0001_initial.py.
Now the business requirements change (very realistic scenario):
- We want to add a category field to Question (e.g. “fun”, “politics”, “sports”)
- We want question_text to allow longer questions (change max_length from 200 → 500)
- We want to add an is_active boolean field (default True)
- We want to add a slug field for SEO-friendly URLs (unique per question)
- Later we might rename votes to vote_count
Let’s do all of this safely.
Step 1: Never Edit the Database Manually – Always Use Migrations
Rule #1 in Django (especially in production):
Do NOT touch db.sqlite3 or run SQL directly → Always change models.py → create migration → apply migration
Step 2: Make the Model Changes (Edit polls/models.py)
Add / modify fields:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
from django.db import models from django.utils import timezone from django.utils.text import slugify # ← new import for slug class Question(models.Model): question_text = models.CharField( max_length=500, # ← was 200, now longer verbose_name="Question text", ) pub_date = models.DateTimeField( verbose_name="Date published", default=timezone.now, ) # ── NEW FIELDS ─────────────────────────────────────────────── category = models.CharField( max_length=50, default="general", choices=[ ("general", "General"), ("fun", "Fun"), ("politics", "Politics"), ("sports", "Sports"), ("tech", "Technology"), ], verbose_name="Category", ) is_active = models.BooleanField( default=True, verbose_name="Is active (visible to users)", ) slug = models.SlugField( max_length=100, unique=True, blank=True, # allow blank initially verbose_name="URL slug", ) def save(self, *args, **kwargs): if not self.slug: self.slug = slugify(self.question_text)[:100] super().save(*args, **kwargs) def __str__(self): return self.question_text # ... other methods & Meta ... class Choice(models.Model): # ... existing fields ... # Optional: rename votes → vote_count (we'll do this later) # vote_count = models.IntegerField(default=0) # ← we'll migrate this |
Important notes about changes we made:
- max_length=500 → non-destructive (safe)
- New category with choices= → very nice for admin dropdown
- slug with unique=True → needs careful migration
- Overrode .save() → auto-generate slug (common pattern)
Step 3: Create the Migration File
|
0 1 2 3 4 5 6 |
python manage.py makemigrations polls |
You will probably see something like this:
|
0 1 2 3 4 5 6 7 8 9 10 11 |
Migrations for 'polls': polls/migrations/0002_question_category_question_is_active_question_slug.py - Add field category to question - Add field is_active to question - Add field slug to question - Alter field question_text on question (max_length changed) |
Open the generated file (polls/migrations/0002_….py)
It will look roughly like this:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
class Migration(migrations.Migration): dependencies = [ ('polls', '0001_initial'), ] operations = [ migrations.AddField( model_name='question', name='category', field=models.CharField( choices=[...], default='general', max_length=50, verbose_name='Category', ), ), migrations.AddField( model_name='question', name='is_active', field=models.BooleanField( default=True, verbose_name='Is active (visible to users)', ), ), migrations.AddField( model_name='question', name='slug', field=models.SlugField( blank=True, max_length=100, unique=True, verbose_name='URL slug', ), ), migrations.AlterField( model_name='question', name='question_text', field=models.CharField(max_length=500, verbose_name='Question text'), ), ] |
Very common question: “Why is slug blank=True even though unique=True?”
→ Because existing rows don’t have slug yet → we must allow NULL/blank during migration, then populate them.
Step 4: Data Migration – Populate Slug for Existing Rows
We need to fill slug for old questions before making it non-nullable / enforcing uniqueness fully.
Create an empty data migration:
|
0 1 2 3 4 5 6 |
python manage.py makemigrations --empty polls --name populate_slugs |
Open the new file (e.g. 0003_populate_slugs.py) and edit it:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
from django.db import migrations from django.utils.text import slugify def populate_slugs(apps, schema_editor): Question = apps.get_model('polls', 'Question') for question in Question.objects.all(): if not question.slug: question.slug = slugify(question.question_text)[:100] question.save() class Migration(migrations.Migration): dependencies = [ ('polls', '0002_question_category_question_is_active_question_slug'), ] operations = [ migrations.RunPython(populate_slugs), ] |
Now run:
|
0 1 2 3 4 5 6 |
python manage.py migrate |
→ Django:
- Adds new fields (nullable/blank)
- Runs your Python code to fill slugs
- Applies other changes
Step 5: Make Slug Non-Blank & Enforce Uniqueness Fully (Optional Next Step)
Later you can:
|
0 1 2 3 4 5 6 7 |
# In models.py slug = models.SlugField(max_length=100, unique=True, blank=False) |
Then:
|
0 1 2 3 4 5 6 7 |
python manage.py makemigrations python manage.py migrate |
But first you must ensure no duplicates — otherwise migration will fail.
Step 6: Rename Field Example (votes → vote_count)
Change in model:
|
0 1 2 3 4 5 6 7 |
# votes = models.IntegerField(default=0) ← comment out or remove vote_count = models.IntegerField(default=0, verbose_name="Vote count") |
Run:
|
0 1 2 3 4 5 6 |
python manage.py makemigrations |
Django detects rename → asks you:
|
0 1 2 3 4 5 6 |
Did you rename the polls.Choice.votes field to vote_count ...? [y/N] |
Type y → it creates RenameField operation (preserves data!)
Common Panic Situations & Fixes (Real Stories)
-
“You cannot add a non-nullable field without default”
→ Add default=… or make field null=True, blank=True first
-
“UNIQUE constraint failed” on slug
→ Duplicate slugs → run data migration to make unique (append -2, -3 etc.)
-
“Cannot ALTER TABLE because it has pending trigger” (PostgreSQL)
→ Rare – usually means conflicting migrations → fake migration or squash
-
“Table already exists” after reset
→ Deleted db.sqlite3 but not migrations folder → delete migrations/000* + db.sqlite3 → start fresh
Quick Checklist Before Every Migrate
- Read the migration file (showmigrations + open it)
- Run python manage.py makemigrations –dry-run –verbosity 3 (preview)
- Always backup db in production
- Test on copy of production data if possible
Your Homework
- Add the fields I showed (category, is_active, slug)
- Create & run the data migration for slugs
- Visit admin → see new fields
- Try renaming votes to vote_count
- Update your detail template to show {{ question.category }} and {{ question.slug }}
Tell me:
- “It worked — show me how to add ManyToManyField next”
- “Migration failed with XXX error — here is the message”
- “How to squash migrations (clean up old files)?”
- “Explain makemigrations –merge”
- Or next big topic: “Django Forms & ModelForm”
You’re now officially dangerous with migrations — that’s when real Django development begins! 💪🚀
Keep going — you’re doing awesome! 🇮🇳
