2025, Oct 20 05:00

How to Avoid Django ForeignKey _id Conflicts When Merging Multiple Sources into One Relation

Learn why Django ForeignKey reserves the fk_name_id column, causing origin_id clashes, and how to consolidate multiple FKs with properties and QuerySets.

When you try to consolidate multiple foreign keys into a single logical “source” and expose it as a standard Django relation, you quickly hit an edge of the ORM. The goal is clear: read from a unified view and still access related records as if it were a regular ForeignKey and reverse relation. The reality is that ForeignKey internals reserve column names in a way that blocks this pattern. Here is how the clash appears, why it happens, and what you can safely do about it today.

Problem setup

Consider a content model that can point to one of two upstream entities. Each upstream type is separate, but you also expose a database view that merges them into a single row set. You want to navigate from the unified source to its items and from an item back to the source in a Django-native way.

class EntryRecord(models.Model):
    uid = models.UUIDField(default=uuid4, editable=False, unique=True, primary_key=True)
    manuscript_id: UUID | None
    manuscript = models.ForeignKey[
        "Manuscript"
    ](
        "Manuscript",
        on_delete=models.CASCADE,
        related_name="entries",
        null=True,
        blank=True,
    )
    hub_id: UUID | None
    hub = models.ForeignKey[
        "Hub"
    ](
        "Hub",
        on_delete=models.CASCADE,
        related_name="entries",
        null=True,
        blank=True,
    )

Next, a view fuses the upstream sources into one table-like model. You want to treat this as a single origin for all entries.

class Origin(pg.View):
    sql = """
        SELECT ...
        FROM manuscripts
        UNION ALL
        SELECT ...
        FROM hubs
        ;
    """
    key = models.UUIDField(unique=True, primary_key=True)
    # ...

To bind an entry to the unified origin, the straightforward attempt is to generate a single origin_id based on the two possible foreign keys, and then declare a ForeignKey to the view using that column.

class EntryRecord(models.Model):
    # ... fields above ...
    origin_id = models.GeneratedField(
        expression=Coalesce(F("manuscript_id"), F("hub_id")),
        output_field=models.UUIDField(),
        db_persist=False,
    )
    origin = models.ForeignKey[
        "Origin"
    ](
        "Origin",
        on_delete=models.DO_NOTHING,
        related_name="entries",
        db_column="origin_id",
        to_field="key",
        null=True,
        editable=False,
    )

At migration time, Django raises an error:

(models.E006) The field 'origin' clashes with the field 'origin_id' from model 'EntryRecord'.

Why this happens

In Django, every ForeignKey automatically manages a corresponding “<name>_id” attribute and database column. Declaring a concrete model field with the same name as that internal attribute creates a naming conflict. In other words, a ForeignKey named origin will implicitly expect an origin_id column; introducing your own origin_id field collides with that internal contract, and Django refuses to continue.

That’s the core limitation you’re running into: you can’t have a user-defined field whose name clashes with the implicit “<fk_field>_id” managed by ForeignKey.

A pragmatic way forward

If you must keep the consolidated lookup while avoiding the clash, use Python-level accessors. Properties are not full ORM relations, but they let you express the intent without violating the naming rules. This is a compromise that preserves queryability where it matters most and keeps the codebase understandable.

First, expose a read-only origin_id and origin on the item model. Then, on the unified view model, expose a property that returns a QuerySet of items associated via either branch key. This gives you a near-identical ergonomic experience for reads, including familiar query chaining.

class EntryRecord(models.Model):
    uid = models.UUIDField(default=uuid4, editable=False, unique=True, primary_key=True)
    manuscript_id: UUID | None
    manuscript = models.ForeignKey[
        "Manuscript"
    ](
        "Manuscript",
        on_delete=models.CASCADE,
        related_name="entries",
        null=True,
        blank=True,
    )
    hub_id: UUID | None
    hub = models.ForeignKey[
        "Hub"
    ](
        "Hub",
        on_delete=models.CASCADE,
        related_name="entries",
        null=True,
        blank=True,
    )
    @property
    def origin_id(self) -> UUID | None:
        return self.manuscript_id or self.hub_id
    @property
    def origin(self):
        oid = self.origin_id
        if not oid:
            return None
        try:
            return Origin.objects.get(pk=oid)
        except Origin.DoesNotExist:
            return None
class Origin(pg.View):
    sql = """
        SELECT ...
        FROM manuscripts
        UNION ALL
        SELECT ...
        FROM hubs
        ;
    """
    key = models.UUIDField(unique=True, primary_key=True)
    @property
    def entries(self):
        return EntryRecord.objects.filter(
            Q(manuscript_id=self.key) | Q(hub_id=self.key)
        )

With this structure, calling origin.entries.all() works because entries returns a QuerySet. On the item side, origin and origin_id remain discoverable attributes, just not ORM relations. This avoids the internal ForeignKey naming conflict entirely.

Why it’s worth understanding

This limitation stems from how Django maps relation fields to implicit “<fk>_id” columns and related descriptors. It’s easy to forget that those names are reserved, especially when composing virtual columns such as GeneratedField. Knowing this constraint helps avoid time-consuming migrations that will never pass validation and keeps the data model predictable.

There are other paths in the ecosystem. A GenericForeignKey could address the polymorphic reference at the expense of some ORM autocomplete. An alternative is django-polymorphic, which can provide more graceful behavior if introducing a third-party dependency is an option. Weigh those trade-offs against your environment and tooling expectations.

Takeaway

If you try to consolidate multiple FK branches into one ForeignKey backed by a generated “_id” column, Django will block you due to its reserved naming for relation storage. When you only need read-side navigation and a clean API surface, lean on @property accessors to expose a unified origin and its related items. Keep ForeignKey field names clear of any conflicting “<name>_id” fields you define yourself, and reserve the ORM relation for cases where you can map to a single, non-ambiguous column.

The article is based on a question from StackOverflow by Thomas and an answer by alexander-shelton.