Skip to content

Cross-DB User FK Pattern

Problem

Hardhat Flow uses per-tenant MySQL databases for all business data (bids, jobs, photos, activity, etc.) and a single platform database (default) for accounts and companies.

accounts.User lives only in the platform DB.

Every tenant model that references a user (e.g. Photo.uploaded_by, Job.project_manager, ActivityLog.performed_by) uses db_constraint=False on the FK because MySQL cannot enforce a FK across databases. However, db_constraint=False does not prevent Django from emitting a SQL JOIN when you call select_related("uploaded_by") — Django generates the join against whichever database the outer queryset was sent to (the tenant DB), and MySQL raises 1146 Table '<tenant>.accounts_user' doesn't exist.

The same error happens when code reads obj.uploaded_by lazily (or obj.user, obj.performed_by, etc.) if the instance was loaded from a tenant DB — Django issues a SELECT … FROM accounts_user WHERE id = ? against the tenant DB alias.

Rule

Never use select_related or lazy descriptor access for a cross-DB FK.

Platform app labels (those in PLATFORM_APP_LABELS in apps/companies/router.py): accounts, companies, admin, auth, contenttypes, sessions.

Any FK from a non-platform model to one of these models is cross-DB.

Canonical Fix

Use fetch_users_by_ids from apps.accounts.db_utils:

from apps.accounts.db_utils import fetch_users_by_ids

# 1. Collect FK column values (no descriptor traversal)
user_ids = [obj.uploaded_by_id for obj in photo_list]

# 2. Batch-fetch from the platform DB
user_map = fetch_users_by_ids(user_ids)  # dict[UUID, User]

# 3. Pass the map through serializer context
PhotoSerializer(photo_list, many=True, context={"user_map": user_map, ...})

Inside a serializer field:

def get_uploaded_by_name(self, obj):
    user = (self.context.get("user_map") or {}).get(obj.uploaded_by_id)
    if not user:
        return None
    return f"{user.first_name} {user.last_name}".strip() or user.email

For accessing the ID without a join, use the FK column directly (obj.uploaded_by_id), not obj.uploaded_by.id — the latter touches the descriptor and fires a cross-DB lookup.

What NOT to do

# BAD — emits JOIN against tenant DB
Photo.objects.filter(...).select_related("uploaded_by")

# BAD — lazy cross-DB lookup when photo is from tenant DB
name = photo.uploaded_by.first_name

# BAD — also triggers descriptor
id_field = serializers.UUIDField(source="uploaded_by.id")

Affected models

Tenant model Cross-DB FK
Photo uploaded_byaccounts.User
Job project_manager, supervisoraccounts.User
Bid project_manager, supervisoraccounts.User
JobUpdate created_by, archived_byaccounts.User
JobWorkerAssignment useraccounts.User
ActivityLog performed_byaccounts.User
Update useraccounts.User