django-pghistory

repository·main·Indexed 19 days ago

https://github.com/ambitioneng/django-pghistory

History tracking for Django and Postgres that uses Postgres triggers to capture all changes, including bulk operations and raw SQL. It provides structured history models, an optional Django admin integration, and the ability to aggregate events across models using the pghistory.models.Events proxy model. Compatible with Python 3.10-3.14, Django 4.2-6.0, Psycopg 2-3, and Postgres 14-18.

Tokens
16.5K
Snippets
47
Records
67
Agent score
68%

What's inside django-pghistory

  1. Optimize performance with statement-level triggers

    main

    By default, django-pghistory uses row-level triggers. While this is highly efficient because it happens within the database instance without extra round-trips, a bulk update (e.g., Model.objects.update(...)) over many elements will result in many internal database operations.

    To reduce the impact of bulk operations, use level=pghistory.Statement in the pghistory.track decorator to leverage statement-level triggers.

    @pghistory.track(level=pghistory.Statement)
    class MyModel(models.Model):
        ...
  2. Understand connection pooling and session pinning

    main

    django-pghistory uses PostgreSQL set_config to propagate request/context to the database. If you use a connection pooler like PgBouncer (in transaction or statement mode) or RDS Proxy, this can cause session pinning.

    Session pinning keeps a client bound to a single backend connection because the pooler assumes the set_config state might affect query results. While this does not pin read statements, it can significantly increase the number of backend connections required in write-heavy systems. Monitor your pooler's pinning metrics and backend connection counts after enabling pghistory.

  3. Understand the structure of auto-generated Event Models

    main

    When you use pghistory.track[], an event model is automatically generated (e.g., TrackedModel becomes TrackedModelEvent). This model contains metadata fields and snapshots of the original model's data.

    Metadata Fields (pgh_*):

    • pgh_id: The primary key (AutoField).
    • pgh_obj: An unconstrained foreign key to the tracked model.
    • pgh_label: An identifying label (e.g., "insert", "update").
    • pgh_context: A foreign key to pghistory.Context containing additional application context.
    • pgh_created_at: Timestamp of the event.

    Snapshot Fields:

    • All other fields are copied from the original model.
    • Integrity Protection: To prevent issues, primary keys are stripped, unique constraints are removed, and foreign keys are unconstrained (no database constraints) by default.
    class TrackedModelEvent(pghistory.models.Event):
        pgh_id = models.AutoField(primary_key=True)
        pgh_obj = models.ForeignKey(TrackedModel, on_delete=models.DO_NOTHING, related_name="event", db_constraint=False)
        pgh_label = models.TextField()
        pgh_context = models.ForeignKey("pghistory.Context", null=True, on_delete=models.DO_NOTHING, related_name="+", db_constraint=False)
        pgh_created_at = models.DatetimeField(auto_now_add=True)
    
        # Snapshot fields
        id = models.IntegerField()
        int_field = models.IntegerField()
        char_field = models.CharField(max_length=16)
        user = models.ForeignKey("auth.User", on_delete=models.DO_NOTHING, db_constraint=False)
  4. How event models are generated

    main

    When you use @pghistory.track() on a Django model, django-pghistory automatically generates a corresponding event model.

    For example, tracking a model named TrackedModel will generate an event model named TrackedModelEvent. This event model contains:

    • All fields from the original tracked model.
    • Additional fields prefixed with pgh_ used to reference the tracked object, distinguish event types, and provide context.

    Note: Event models are created dynamically and added to your models module. While you won't see them explicitly declared in your models.py file, they will appear in your Django migrations.

    @pghistory.track()
    class TrackedModel(models.Model):
        int_field = models.IntegerField()
        text_field = models.TextField()
  5. Denormalize context using ContextJSONField

    main

    By default, event models use a foreign key to a central pghistory.Context table. For high-performance requirements or database partitioning, you can denormalize this context directly into the event model using pghistory.ContextJSONField().

    Configuration Options:

    • Global: Set settings.PGHISTORY_CONTEXT_FIELD = pghistory.ContextJSONField() in your Django settings.
    • Per-model: Pass context_field=pghistory.ContextJSONField() to pghistory.track() or pghistory.create_event_model().

    When denormalized, a JSONField named pgh_context is created on the event model. The pgh_context_id field (used to group events) defaults to a UUIDField. You can override this field type using pghistory.ContextUUIDField() or by setting settings.PGHISTORY_CONTEXT_ID_FIELD globally.

    # Example of per-model denormalization
    @pghistory.track(context_field=pghistory.ContextJSONField())
    class MyModel(models.Model):
        ...
  6. Performance considerations for the `Events` proxy model

    main

    The pghistory.models.Events proxy model uses a Common Table Expression (CTE) to provide an aggregate view across all event tables.

    Performance Notes:

    • Postgres Version: Postgres 12+ optimizes filters on CTEs. On earlier versions, filtering Events directly may be slow.
    • Scale: Aggregating many large event tables via the proxy model is inherently slow.
    • Best Practice: For efficient filtering of large datasets, use the special model manager methods described in the Aggregating Events and Diffs guide instead of direct filtering on the Events model.
  7. How trackers and events work together

    main

    A tracker is a high-level abstraction used to monitor model events. It sits on top of database triggers.

    • Trackers: These define what to watch for (e.g., inserts, updates, or deletes). For example, the standard @pghistory.track() decorator uses both an InsertEvent tracker and an UpdateEvent tracker.
    • Events: An event is the actual historical record stored by a tracker. When a tracker detects a change, it saves a version of the model into an event model.

    Users can customize tracking by:

    1. Specifying trackers directly in @pghistory.track().
    2. Overriding settings.PGHISTORY_DEFAULT_TRACKERS to change global behavior.
    3. Adding conditions using pghistory.AnyChange or pghistory.Q to only track specific changes.
  8. Use the `pghistory.models.Events` proxy model to aggregate history

    main

    The pghistory.models.Events proxy model provides a unified view of all event tables in your database. Instead of querying individual event tables for each tracked model, you can use this model to query history across your entire application. It uses a PostgreSQL Common Table Expression (CTE) with UNION ALL to combine event tables and window functions to compute diffs.

    Key fields available on the Events model:

    • pgh_slug: Unique identifier across all event tables.
    • pgh_model: The event model label (e.g., app_label.ModelName).
    • pgh_id: The primary key of the event.
    • pgh_created_at: Timestamp of event creation.
    • pgh_label: The event label.
    • pgh_data: The raw event data.
    • pgh_diff: The diff against the previous event of the same model and object.
    • pgh_context_id: The context UUID.
    • pgh_context: The context JSON.
    • pgh_obj_model: The model of the tracked object.
    • pgh_obj_id: The primary key of the tracked object.
    import pghistory.models
    
    # Assuming User is a tracked model
    print(pghistory.models.Events.objects.order_by("pgh_created_at").values())
    # Returns a list of dictionaries containing the unified event data and diffs
  9. How triggers work in django-pghistory

    main

    django-pghistory uses Postgres triggers to reliably capture historical changes. Because Django does not natively support triggers, the library uses django-pgtrigger to manage them.

    Key characteristics of triggers in this system:

    • Installation: Like database indices, triggers are installed via migrations and attached to specific database tables.
    • Execution: They are functions that run directly within the database after INSERT, UPDATE, or DELETE operations.
    • Data Access: Triggers have access to both the old and new versions of the rows being modified.
    • Conditional Logic: Triggers can be configured to execute only when specific properties of the modified rows are met.
  10. Track Many-To-Many field changes

    main

    To track changes in many-to-many relationships (like adding or removing a user from a group), you must track the "through" model of that relationship.

    When tracking through models, you often need to set obj_field=None because Django does not allow foreign keys to auto-generated through models. This tells pghistory to ignore creating a reference to the object in the event model, allowing you to capture the relationship changes via specific labels.

    Example of tracking group additions and removals:

    from django.contrib.auth.models import User
    import pghistory
    
    @pghistory.track(
        pghistory.AfterInsert("group.add"),
        pghistory.BeforeDelete("group.remove"),
        obj_field=None,
    )
    class UserGroups(User.groups.through):
        class Meta:
            proxy = True
  11. Upgrade to Version 2

    main

    To upgrade to version 2, most users can simply run python manage.py makemigrations to generate the necessary trigger migrations.

    Important Requirements:

    • Ensure django-pgtrigger>=4.5 is installed to avoid migration-related bugs.

    Special Case: Third-party models If you are tracking third-party models (like Django's built-in User model), you must register trackers on proxy models. If you do not use proxy models for third-party tracking, trigger migrations may be created outside of your project's migration directory.

    python manage.py makemigrations
  12. Backfill historical events

    main

    To create history records for existing data, you can use manual event tracking.

    For large datasets, use bulk creation. You can access the correct event model via the pgh_event_model attribute on your tracked model class. If you have multiple trackers on a single model, use the dictionary syntax: model.pgh_event_models["label"].

    # Example of accessing the event model for bulk creation
    from myapp.models import MyModel
    
    EventModel = MyModel.pgh_event_model
    EventModel.objects.bulk_create([
        # ... event instances
    ])