Database Migrations
This guide explains how to work with Django database migrations in ZineCore2.
Prerequisites
- ✅ ZineCore2 server installed
- ✅ PostgreSQL database created
- ✅ Virtual environment activated
- ✅ Understand basic SQL and Django models
Understanding Migrations
Migrations are version-controlled schema changes that transform your database from one state to another.
Why migrations?
- Track database schema changes over time
- Apply changes consistently across environments
- Rollback changes if needed
- Collaborate safely with multiple developers
Initial Setup
Apply All Migrations
On a fresh database, apply all existing migrations:
cd backend
DJANGO_SETTINGS_MODULE=zinecore.settings.development \
../.venv/bin/python manage.py migrate
Expected output:
Operations to perform:
Apply all migrations: admin, auth, authtoken, catalog, agents, holdings, repositories, geography, vocabularies, contenttypes, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
...
Applying catalog.0001_initial... OK
Applying agents.0001_initial... OK
Applying holdings.0001_initial... OK
Applying repositories.0001_initial... OK
Check Migration Status
See which migrations have been applied:
DJANGO_SETTINGS_MODULE=zinecore.settings.development \
../.venv/bin/python manage.py showmigrations
Output:
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
auth
[X] 0001_initial
...
catalog
[X] 0001_initial
[X] 0002_add_indexes
- [X] — Applied
- [ ] — Not applied
Creating Migrations
When to Create Migrations
Create a migration whenever you change a model:
- Add a field
- Remove a field
- Change a field type
- Add/remove indexes
- Add/remove constraints
- Rename a field or model
Step 1: Modify Models
Edit the model in the appropriate app:
# catalog/models.py
class Zine(TimestampedModel):
# Existing fields...
title = models.CharField(max_length=500)
# NEW: Add a field
isbn = models.CharField(
max_length=13,
blank=True,
help_text="ISBN-13 identifier"
)
Step 2: Generate Migration
cd backend
DJANGO_SETTINGS_MODULE=zinecore.settings.development \
../.venv/bin/python manage.py makemigrations catalog
Output:
Migrations for 'catalog':
catalog/migrations/0003_zine_isbn.py
- Add field isbn to zine
Migration file created: catalog/migrations/0003_zine_isbn.py
Step 3: Review Migration
cat catalog/migrations/0003_zine_isbn.py
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('catalog', '0002_add_indexes'),
]
operations = [
migrations.AddField(
model_name='zine',
name='isbn',
field=models.CharField(blank=True, help_text='ISBN-13 identifier', max_length=13),
),
]
Check:
- ✅ Correct field name and type
- ✅ Correct dependencies
- ✅ Safe operations (won't lose data)
Step 4: Apply Migration
DJANGO_SETTINGS_MODULE=zinecore.settings.development \
../.venv/bin/python manage.py migrate catalog
Output:
Operations to perform:
Apply all migrations: catalog
Running migrations:
Applying catalog.0003_zine_isbn... OK
Step 5: Verify
Check the database:
DJANGO_SETTINGS_MODULE=zinecore.settings.development \
../.venv/bin/python manage.py dbshell
\d catalog_zine
-- Should show 'isbn' column
Or via Django shell:
from catalog.models import Zine
zine = Zine.objects.first()
zine.isbn = '9781234567890'
zine.save()
Common Migration Scenarios
Add a Field (Non-Breaking)
Safe: Existing records continue to work.
# Add optional field
extra_notes = models.TextField(blank=True)
python manage.py makemigrations
python manage.py migrate
Add a Required Field
Problem: Existing records don't have a value.
Solution 1: Provide a default:
# Model
publication_status = models.CharField(
max_length=50,
default='published' # Default for existing records
)
Solution 2: Use a two-step migration:
Step 1: Add field as optional:
publication_status = models.CharField(max_length=50, blank=True, null=True)
python manage.py makemigrations
python manage.py migrate
Step 2: Populate existing records:
# Data migration
from catalog.models import Zine
Zine.objects.all().update(publication_status='published')
Step 3: Make field required:
publication_status = models.CharField(max_length=50, default='published')
python manage.py makemigrations
python manage.py migrate
Remove a Field
Warning: This deletes data!
# Remove field from model
class Zine(TimestampedModel):
# isbn = models.CharField(...) # REMOVED
title = models.CharField(max_length=500)
python manage.py makemigrations catalog
# Confirm deletion when prompted
python manage.py migrate
Best practice: Backup database first!
Rename a Field
Django can't auto-detect renames. Use RenameField operation:
Option 1: Let Django detect (usually creates AddField + RemoveField)
# Before
old_name = models.CharField(max_length=100)
# After
new_name = models.CharField(max_length=100)
python manage.py makemigrations
# Django creates: AddField new_name + RemoveField old_name
Option 2: Manual rename (preserves data)
python manage.py makemigrations --empty catalog --name rename_field
Edit migration:
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('catalog', '0003_previous_migration'),
]
operations = [
migrations.RenameField(
model_name='zine',
old_name='old_name',
new_name='new_name',
),
]
python manage.py migrate
Change Field Type
Risky: May require data conversion.
Example: Change CharField to TextField:
# Before
abstract = models.CharField(max_length=500)
# After
abstract = models.TextField() # No max_length
python manage.py makemigrations catalog
python manage.py migrate
Safe conversions:
- CharField → TextField ✅
- IntegerField → BigIntegerField ✅
Risky conversions:
- TextField → CharField ❌ (may truncate)
- CharField → IntegerField ❌ (may fail if non-numeric)
Data Migrations
What are Data Migrations?
Schema migrations change table structure. Data migrations change table contents.
Use cases:
- Populate new fields with calculated values
- Transform existing data
- Clean up old data
Create Data Migration
python manage.py makemigrations --empty catalog --name populate_isbn
Edit migration:
from django.db import migrations
def populate_isbn(apps, schema_editor):
"""Populate ISBN field from identifier array"""
Zine = apps.get_model('catalog', 'Zine')
for zine in Zine.objects.all():
# Extract ISBN from identifier array
for identifier in zine.identifier:
if identifier.startswith('ISBN'):
zine.isbn = identifier.replace('ISBN:', '').strip()
zine.save()
break
def reverse_populate_isbn(apps, schema_editor):
"""Reverse migration (optional)"""
Zine = apps.get_model('catalog', 'Zine')
Zine.objects.all().update(isbn='')
class Migration(migrations.Migration):
dependencies = [
('catalog', '0003_zine_isbn'),
]
operations = [
migrations.RunPython(populate_isbn, reverse_populate_isbn),
]
Apply:
python manage.py migrate catalog
Migration Best Practices
DO:
✅ Test migrations in development first
# Development
DJANGO_SETTINGS_MODULE=zinecore.settings.development python manage.py migrate
# Then production
DJANGO_SETTINGS_MODULE=zinecore.settings.production python manage.py migrate
✅ Backup database before migrations
pg_dump -U zinecore2 -d zinecore2 > backup-pre-migration.sql
✅ Keep migrations small and focused
One migration = one logical change
✅ Write reversible migrations
Provide reverse operations when possible.
✅ Review auto-generated migrations
Don't blindly apply — check the migration file first.
DON'T:
❌ Edit applied migrations
Once applied, migrations are immutable. Create a new migration instead.
❌ Delete migrations
Breaks migration history. Use migrate zero to unapply if needed.
❌ Manually edit the database
Always use migrations for schema changes.
❌ Skip migrations
Apply migrations in order — don't skip intermediate steps.
Rollback Migrations
Rollback Last Migration
# See current migrations
python manage.py showmigrations catalog
# Rollback last migration
python manage.py migrate catalog 0002_previous_migration
Rollback All Migrations for an App
python manage.py migrate catalog zero
Warning: This drops all tables for the app!
Rollback and Reapply
Useful for testing:
# Rollback
python manage.py migrate catalog 0002
# Reapply
python manage.py migrate catalog
Troubleshooting
"No migrations to apply"
Cause: All migrations already applied.
Solution: Check if there are pending model changes:
python manage.py makemigrations --dry-run
"Migration is being applied before its dependency"
Cause: Incorrect migration dependencies.
Solution: Edit migration file and fix dependencies:
class Migration(migrations.Migration):
dependencies = [
('catalog', '0003_correct_previous_migration'), # Fix this
]
"Column already exists"
Cause: Database out of sync with migrations.
Solution: Mark migration as applied without running:
python manage.py migrate catalog 0003 --fake
Warning: Only use --fake if you're certain the database is already in the correct state.
"Cannot drop table: still referenced by foreign key"
Cause: Foreign key constraints prevent table deletion.
Solution: Drop dependent tables first, or create a migration that removes foreign keys before dropping table.
"IntegrityError: NOT NULL constraint failed"
Cause: Adding required field to table with existing data.
Solution: Use two-step migration (add optional, populate, make required).
Production Deployment
Deployment Checklist
Before deploying migrations to production:
- Migrations tested in development
- Migrations tested on staging (with production-like data)
- Database backup created
- Downtime scheduled (if needed)
- Rollback plan prepared
Zero-Downtime Migrations
For large tables, use these patterns:
Step 1: Add field as optional (no downtime)
new_field = models.CharField(max_length=100, blank=True, null=True)
Deploy and migrate.
Step 2: Populate field (background job, no downtime)
# Management command or celery task
from catalog.models import Zine
for zine in Zine.objects.filter(new_field__isnull=True):
zine.new_field = calculate_value(zine)
zine.save()
Step 3: Make field required (no downtime if all populated)
new_field = models.CharField(max_length=100, default='')
Deploy and migrate.
Monitoring
After production migration:
# Check migration status
python manage.py showmigrations
# Check for errors
tail -f /var/log/zinecore2/error.log
# Test API
curl http://api.example.com/api/zines/
Next Steps
- Add Custom Fields — Extend models with custom fields
- Architecture: Models — Understanding model structure
- Deploy to Production — Production deployment guide