django-pgschemas

repository·master·Indexed 41 days ago

https://github.com/lorinkoz/django-pgschemas

A Django package for implementing multi-tenancy using PostgreSQL schemas. It supports both static and dynamic tenants with routing via URL, session, or headers. Key features include schema-aware management commands with parallel execution, a `cloneschema` utility for duplicating schemas, and a core API for active schema management including `activate()`, `activate_public()`, and `get_current_schema()`. Version 1.3.2.

Tokens
19.2K
Snippets
57
Records
86
Agent score
49%

What's inside django-pgschemas

  1. Overview of django-pgschemas

    master

    django-pgschemas is a Django package that leverages PostgreSQL schemas to implement data multi-tenancy within a single project. It is a fork of django-tenants that introduces several key architectural improvements:

    • Tenant Types: Supports both static tenants (which can have their own dedicated apps and urlconf) and dynamic tenants.
    • Routing Options: Tenants can be identified and routed via URL (using subdomains or subfolders on a shared subdomain), Session, or Headers.
    • Schema Usage: The public schema is intended strictly for shared data across all tenants, rather than main site data. The package discourages using the search path for table "overriding."
    • Management Commands: Supports running management commands across multiple schemas using wildcards, with options for sequential or parallel execution via multithreading.
  2. What is django-pgschemas and how does it work?

    master

    django-pgschemas is a package designed to support data multi-tenancy within a single Django project using PostgreSQL schemas.

    It leverages the PostgreSQL "search path" mechanism to separate data. A search path (e.g., ['tenant1', 'public']) tells Postgres the order in which to look for tables. If a table exists in both tenant1 and public, Postgres will return the one in tenant1.

    Key behaviors:

    • Multi-tenancy Emulation: The package maps incoming HTTP requests to specific schemas by setting the database search path accordingly.
    • Schema Isolation: Schemas act as a layer of separation between the database and tables, allowing multiple schemas to contain identical table structures.
    • Manual Control: Beyond automatic request-based switching, the package provides an API to manually change the search path for schema-specific tasks outside the standard request/response cycle.
  3. Understand the multi-tenancy approach of django-pgschemas

    master

    The django-pgschemas package implements a semi-isolated approach to multi-tenancy. Instead of using separate databases (isolated) or a single shared schema (shared), it uses a shared database with separate schemas.

    In this model, each tenant is assigned its own PostgreSQL schema. This approach offers a compromise between simplicity and performance:

    • Simplicity: Requires minimal changes to existing Django code and involves managing only one database.
    • Performance: Leverages shared database connections, buffers, and memory.

    Limitations: This approach may face scalability challenges if you have thousands of tenants, as there is currently no built-in way to implement tenant sharding.

  4. Activate a schema to access tenant data

    master

    By default, only the public schema is in the search path. Attempting to query models belonging to apps not in the public schema will result in a ProgrammingError.

    To access data within a specific schema, you must activate it using one of two methods:

    1. Using Schema.create(name): Use this context manager to temporarily switch the search path to a specific schema name.
    2. Using a Tenant instance: Use the tenant object itself as a context manager to activate its associated schema.

    Tenant activation is also handled automatically during the request/response cycle via tenant routing.

    from django_pgschemas.schema import Schema
    from tenants.models import Tenant
    from blog.models import BlogEntry
    from customers.models import Product
    
    # Option 1: Activate by schema name
    with Schema.create("blog"):
        BlogEntry.objects.all()
    
    # Option 2: Activate via a Tenant instance
    tenant1 = Tenant.objects.first()
    with tenant1:
        Product.objects.all()
  5. Use only static tenants (no dynamic tenants)

    master

    If your application does not require dynamic tenant creation, you can omit the default key from the TENANTS configuration. In this mode, no TenantModel or DomainModel is required, and reference schemas cannot be used.

    # settings.py
    TENANTS = {
        "public": {
            "APPS": [
                "django.contrib.contenttypes",
                "django.contrib.staticfiles",
                "django_pgschemas",
                "tenants",
            ],
        },
        "www": {
            "APPS": [
                "django.contrib.auth",
                "django.contrib.sessions",
                "main",
            ],
            "DOMAINS": ["mydomain.com"],
            "URLCONF": "main.urls",
        },
        "blog": {
            "APPS": [
                "django.contrib.auth",
                "django.contrib.sessions",
                "blog",
            ],
            "DOMAINS": ["blog.mydomain.com", "help.mydomain.com"],
            "URLCONF": "blog.urls",
        }
    }
  6. How routing works in django-pgschemas

    master

    Routing is the process of determining which tenant an incoming request belongs to and activating that tenant for the remainder of the request/response cycle.

    This is achieved using middleware that augments the Django request object. Once a routing middleware is active, the request object will contain a tenant property. This property holds either an instance of your tenant model or an instance of the django_pgschemas.schema.Schema class.

  7. Distinguish between schemas and tenants

    master

    While the terms are often used interchangeably in the documentation, they have distinct meanings in the context of this package:

    • Tenant: A subset of isolated data belonging to a specific customer or entity.
    • Schema: The underlying PostgreSQL mechanism used to isolate tenant data.

    Note: Every tenant is represented by a schema, but not every schema is a tenant. For example, the public schema is a shared schema that contains data accessible across all tenants and is not considered a tenant itself.

  8. Understand the Schema and RoutingInfo abstractions

    master

    The Schema class is the base representation of a Postgres schema. It includes a schema_name and an optional routing attribute of type RoutingInfo.

    RoutingInfo contains metadata about how a tenant was identified (e.g., via domain, session, or headers). While this is typically populated automatically by middleware during web requests, it may be None in other contexts like management commands.

    Supported routing types include:

    • DomainInfo: Contains the domain and an optional folder.
    • SessionInfo: Contains a reference string.
    • HeadersInfo: Contains a reference string.
    class Schema:
        schema_name: str
        routing: RoutingInfo = None
    
    # RoutingInfo can be:
    # DomainInfo(domain='example.com', folder='subfolder')
    # SessionInfo(reference='session_id')
    # HeadersInfo(reference='header_value')
    # None
  9. Manage static vs. dynamic tenants

    master

    The package supports two types of tenant management depending on whether the tenant is known at development time or created at runtime:

    • Static Tenants: These are well-defined, enterprise-level sites (e.g., a central payment or signup portal). They are defined via Django settings.
    • Dynamic Tenants: These are customer-specific sites created as users sign up for your SaaS. They are stored in specific tables within the database.
  10. Provide tenants as Pytest fixtures

    master

    You can create a fixture to retrieve a specific tenant for use in individual tests. If you want the tenant to be automatically activated for the duration of the test, use the tenant object as a context manager within the fixture using yield.

    # Standard tenant fixture
    @pytest.fixture
    def tenant(db):
        return Tenant.objects.get(schema_name="tenant1")
    
    # Tenant fixture that automatically activates the schema for the test
    @pytest.fixture
    def tenant(db):
        with (tenant := Tenant.objects.get(schema_name="tenant1")):
            yield tenant
  11. Configure tenant-aware caching

    master

    To ensure cache keys are unique to each tenant, use django_pgschemas.contrib.cache.make_key as the KEY_FUNCTION in your Django CACHES setting. This prevents data leakage between tenants by incorporating the tenant identifier into the generated cache keys.

    CACHES = {
        "default": {
            "KEY_FUNCTION": "django_pgschemas.contrib.cache.make_key",
        }
    }
  12. Prepare a reference schema with createrefschema and runschema

    master

    After setting CLONE_REFERENCE in your settings, use these commands to initialize your template schema (e.g., named sample):

    1. Create the schema: python manage.py createrefschema (creates the schema and applies migrations).
    2. Load data into the reference schema: Use runschema to run commands like loaddata specifically on that schema.
    3. Interact with the reference schema: Use runschema shell to run a Django shell within the context of the reference schema.
    python manage.py createrefschema
    python manage.py runschema loaddata customers.products -s sample
    python manage.py runschema shell -s sample