Skip to main content

Models & Migrations

Maybern uses Django’s ORM with custom base classes and field types. This guide covers model design patterns and migration best practices.

Base Model Classes

All models should inherit from one of these base classes:

TimeStampedModel

Base model with created_at and updated_at fields. Use for non-tenant-specific data.

CustomerTimeStampedModel

Adds customer_id for multi-tenancy. Use for all tenant-specific models.

AuditableModel

Enables audit logging via the DB manager. Use when you need change tracking.

VersionModel

For type-2 versioning (maintaining historical records). Use for versioned entities.

Model Example

from django.db import models
from server.apps.core.models import fields as model_fields
from server.apps.core.models.customer_time_stamped import CustomerTimeStampedModel


class MyEntity(CustomerTimeStampedModel):
    """
    Model represents a business entity.
    Document the purpose of each model clearly.
    """
    id = model_fields.MUUIDField(
        primary_key=True,
        editable=False,
        prefix=IdPrefixTableCode.MY_ENTITY.value,
    )
    
    name = model_fields.TextModelField()
    description = model_fields.TextModelField(blank=True, default="")
    
    # Define relationships with clear on_delete behavior and related_name
    parent = model_fields.ForeignKeyModelField(
        "my_app.ParentEntity",
        on_delete=models.CASCADE,
        related_name="children",
        null=True,
    )
    
    # Document constraints
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["name", "parent"],
                name="unique_entity_name_per_parent",
            ),
        ]

Custom Field Types

All ORM fields are wrapped in server.apps.core.models.fields:
Field TypeDescription
MUUIDFieldUUID with prefix (e.g., INVS_abc123)
TextModelFieldText field with standard validation
ForeignKeyModelFieldForeign key with sensible defaults
DecimalModelFieldDecimal with precision settings
DateModelFieldDate field
BooleanModelFieldBoolean field

Benefits of Wrapped Fields

  • Automatic conversion to dataclass fields
  • Built-in validation
  • Consistent defaults across the codebase
  • Custom serialization behavior

Field Decorators

Special decorators for sensitive data:
Marks a field as containing sensitive data:
from server.apps.core.models.decorators import sensitive

class User(CustomerTimeStampedModel):
    @sensitive
    ssn = model_fields.TextModelField()
Sensitive fields are:
  • Automatically converted to SecureString
  • Excluded from logs
  • Masked in error messages

Model Managers and QuerySets

Managers

Custom managers wrap ORM functionality:
# Base managers
BaseTimeStampedModelManager      # Core querying logic
TimeStampedModelManager          # For non-tenant models
CustomerTimeStampedModelManager  # For tenant models (adds customer filtering)
VersionableModelManager          # For versioned models

QuerySets

Custom querysets provide read query overrides:
TimeStampedQuerySet              # For TimeStampedModels
CustomerTimeStampedQuerySet      # For CustomerTimeStampedModels
VersionCustomerTimeStampedQuerySet  # For VersionableModels

Versioned Models

For entities that need historical tracking (type-2 models):
from server.apps.core.models.version import VersionModel, VersionableModel


class FeeRateVersion(VersionModel):
    """Version record for fee rates."""
    fee_rate = VersionableModel()
    rate = model_fields.DecimalModelField()
    effective_date = model_fields.DateModelField()


class FeeRate(VersionableModel):
    """Parent model for fee rate versions."""
    version_model = FeeRateVersion
    # ... base fields
This creates:
  • Parent model with current state
  • Version model with historical records
  • Automatic version management

Constraints

Custom constraints beyond Django’s built-in options:
from server.apps.core.models.constraints import require_exactly_one_non_null_constraint

class FeatureFlagOverride(CustomerTimeStampedModel):
    user = model_fields.ForeignKeyModelField(User, null=True)
    customer = model_fields.ForeignKeyModelField(Customer, null=True)
    
    class Meta:
        constraints = [
            require_exactly_one_non_null_constraint(
                fields=["user", "customer"],
                name="exactly_one_override_target",
            ),
        ]

Migration Best Practices

1

Generate migrations

After modifying models:
just makemigrations
2

Review the migration

Always review generated migrations for:
  • Correct field types
  • Index creation
  • Data migrations needed
3

Run migrations locally

just migrate
4

Test the migration

Ensure the migration:
  • Runs without errors
  • Is reversible (when possible)
  • Handles existing data correctly
Never modify migrations after they’ve been pushed to main. Create a new migration instead.

Data Migrations

For complex data changes, create a data migration:
# Generated migration file
from django.db import migrations

def forward_migration(apps, schema_editor):
    MyModel = apps.get_model("my_app", "MyModel")
    for obj in MyModel.objects.filter(status="old"):
        obj.status = "new"
        obj.save()

def reverse_migration(apps, schema_editor):
    MyModel = apps.get_model("my_app", "MyModel")
    for obj in MyModel.objects.filter(status="new"):
        obj.status = "old"
        obj.save()

class Migration(migrations.Migration):
    dependencies = [...]
    
    operations = [
        migrations.RunPython(forward_migration, reverse_migration),
    ]

Model Design Principles

Models should only define:
  • Fields and relationships
  • Constraints and indexes
  • Meta options
Business logic belongs in services, not models.
  • CustomerTimeStampedModel for most tenant data
  • AuditableModel when you need change tracking
  • VersionModel for historical records
Always specify:
  • on_delete behavior
  • related_name for reverse lookups
  • null=True when optional
Create indexes for:
  • Foreign key fields (automatic)
  • Fields used in filters
  • Fields used in ordering
  • Unique constraint fields

Next Steps