How-To Guides

Database Migrations

How to create and apply Django 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

Migrations applied! Continue to Custom Fields to extend the data model.
Copyright ©2026 ZineCore2 Contributors,