Framework Guide

Fixing Django GPT Mess: Purge Duplicated Models

Redundant models created migration conflicts; we normalized the schema and streamlined the codebase.

January 15, 2025 5 min read

The problem

A healthcare startup's Django project had 147 database migrations that took 12 minutes to run. The database contained User, AppUser, UserProfile, CustomerUser, and AccountUser models - all storing essentially the same data. Every new feature created migration conflicts requiring manual resolution. The 84GB PostgreSQL database was 70% duplicate data across redundant tables.

How AI created this issue

Different developers had asked ChatGPT to create models for various features over 6 months. Each time, ChatGPT generated new models without checking existing ones:


# Developer 1: "Create a user model for authentication"
# ChatGPT response:
class AppUser(models.Model):
    email = models.EmailField(unique=True)
    username = models.CharField(max_length=150)
    password = models.CharField(max_length=128)
    created_at = models.DateTimeField(auto_now_add=True)

# Developer 2: "I need a customer profile model"
# ChatGPT response:
class CustomerUser(models.Model):
    email_address = models.EmailField(unique=True)
    full_name = models.CharField(max_length=255)
    password_hash = models.CharField(max_length=255)
    registration_date = models.DateTimeField(auto_now_add=True)
    
class UserProfile(models.Model):
    user_email = models.EmailField(unique=True)
    display_name = models.CharField(max_length=200)
    bio = models.TextField(blank=True)

# Developer 3: "Add user accounts for billing"
# ChatGPT response:
class AccountUser(models.Model):
    account_email = models.EmailField(unique=True)
    account_name = models.CharField(max_length=255)
    encrypted_password = models.CharField(max_length=255)
    signup_timestamp = models.DateTimeField(auto_now_add=True)

ChatGPT never asked "Do you already have a user model?" It generated new models each time, creating a maze of foreign keys, duplicate data, and conflicting business logic spread across multiple tables.

The solution

  1. Model audit and mapping: Created a comprehensive map of all models and their relationships
  2. Data consolidation plan: Designed a single, extensible user model:
    
    # Consolidated user model
    from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
    
    class User(AbstractBaseUser, PermissionsMixin):
        email = models.EmailField(unique=True, db_index=True)
        full_name = models.CharField(max_length=255)
        is_active = models.BooleanField(default=True)
        is_staff = models.BooleanField(default=False)
        date_joined = models.DateTimeField(auto_now_add=True)
        
        # Profile fields (previously in separate models)
        bio = models.TextField(blank=True)
        avatar_url = models.URLField(blank=True)
        
        # Billing fields (previously in AccountUser)
        stripe_customer_id = models.CharField(max_length=255, blank=True)
        subscription_status = models.CharField(max_length=50, default='free')
        
        USERNAME_FIELD = 'email'
        REQUIRED_FIELDS = ['full_name']
        
        class Meta:
            db_table = 'users'
            indexes = [
                models.Index(fields=['email', 'is_active']),
                models.Index(fields=['date_joined']),
            ]
  3. Migration strategy: Created a careful data migration preserving all relationships:
    
    # Data migration to consolidate users
    def consolidate_users(apps, schema_editor):
        User = apps.get_model('accounts', 'User')
        AppUser = apps.get_model('legacy', 'AppUser')
        CustomerUser = apps.get_model('legacy', 'CustomerUser')
        
        # Map and migrate all user data
        for app_user in AppUser.objects.all():
            User.objects.update_or_create(
                email=app_user.email,
                defaults={
                    'full_name': app_user.username,
                    'date_joined': app_user.created_at,
                    # Map other fields...
                }
            )
  4. Foreign key updates: Systematically updated all references to point to the new model
  5. Cleanup migrations: Squashed 147 migrations down to 23 essential ones

The results

  • Database size reduced from 84GB to 31GB (63% reduction)
  • Migration time dropped from 12 minutes to 47 seconds
  • Query performance improved 4.2x with proper indexes
  • Zero migration conflicts in 6 months (was 2-3 per week)
  • Development velocity increased 35% with cleaner data model
  • Test suite runs 3x faster without redundant model operations

The team implemented a "model registry" where all Django models are documented. New developers must check existing models before creating new ones. They learned that AI tools generate code in isolation - human architects must maintain the bigger picture.

Ready to fix your codebase?

Let us analyze your application and resolve these issues before they impact your users.

Get Diagnostic Assessment →