MultipleObjectsReturned is Django refusing to guess. Your code asked for exactly one row, but the database found two or more. That refusal is useful: choosing an arbitrary record could show the wrong profile, charge the wrong account, or quietly preserve corrupt data.

Why get() raises this exception

  • QuerySet.get() must resolve to exactly one object.

  • Zero matches raise the model’s DoesNotExist exception.

  • More than one match raises the model’s MultipleObjectsReturned exception.

  • The lookup userid=pk is not unique merely because the variable is named pk; uniqueness comes from the field/constraint being queried.

  • A model primary key is unique by definition, while a foreign key commonly appears in many rows.

Questions to answer before choosing a fix

  • Does the domain permit several records for this parent, or are these true duplicates?

  • Which field or field combination is the stable identity?

  • What should zero matches mean to the API client?

  • Could concurrent requests create the same logical record?

  • Which downstream rows or external systems reference the records involved?

Start by measuring the duplicate shape

Django shellpython
from django.db.models import Count
from helloapp.models import UserInfo
 
duplicates = (
    UserInfo.objects
    .values("user_id")
    .annotate(row_count=Count("id"))
    .filter(row_count__gt=1)
    .order_by("user_id")
)
 
for group in duplicates:
    print(group)

This query diagnoses; it does not mutate

  • values("user_id") groups by the actual database-facing foreign-key ID. Adapt the field name to the model.

  • Count("id") measures how many rows share each user.

  • Filtering after annotation retains only groups with more than one row.

  • Inspect the full rows, timestamps, ownership, and business meaning before calling any row a duplicate.

  • Run production investigations through approved read-only access and protect personal data in exported output.

Path A: many UserInfo rows are valid

helloapp/views.py (function-based DRF view)python
from rest_framework.decorators import api_view
from rest_framework.response import Response
 
from .models import UserInfo
from .serializers import UserInfoSerializer
 
 
@api_view(["GET"])
def user_info_for_user(request, user_id):
    rows = UserInfo.objects.filter(user_id=user_id).order_by("id")
    serializer = UserInfoSerializer(rows, many=True)
    return Response(serializer.data)

A collection has collection semantics

  • filter() returns a lazy QuerySet and does not raise when zero or many rows match.

  • many=True tells DRF the serializer is receiving an iterable rather than one model instance.

  • An explicit order_by() makes response order deterministic.

  • No matches normally produce HTTP 200 with an empty JSON array for a filtered collection.

  • Add pagination when a user can accumulate many records; do not return an unbounded history.

  • Authorize access to the requested user instead of trusting a URL ID supplied by the caller.

Prefer filtering a resource collection

helloapp/views.py (ViewSet)python
from rest_framework import viewsets
 
 
class UserInfoViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = UserInfoSerializer
 
    def get_queryset(self):
        queryset = UserInfo.objects.order_by("id")
        user_id = self.request.query_params.get("user_id")
        if user_id is not None:
            queryset = queryset.filter(user_id=user_id)
        return queryset

List and detail URLs now mean different things

  • GET /api/user-info/?user_id=7 is a filtered collection and may return zero, one, or many records.

  • GET /api/user-info/42/ retrieves one UserInfo resource by its unique primary key.

  • ReadOnlyModelViewSet exposes list/retrieve behavior without write actions.

  • Validate query parameters with a filter backend or serializer when filters become complex.

  • Scope the base queryset for tenant/user permissions before applying caller-controlled filters.

Contract cases for the collection endpoint

  • No matching rows returns the documented empty result.

  • One matching row still appears inside a collection representation.

  • Several rows have deterministic order and pagination metadata where applicable.

  • Malformed filter values receive a clear client error.

  • Unauthorized cross-user and cross-tenant filters cannot disclose existence or content.

Path B: exactly one row should exist

If each user owns exactly one profile, the schema should say so. Application checks alone have a race: two requests can both observe “no row” and insert. A database uniqueness constraint is the authoritative guard.

helloapp/models.py (one-to-one relationship)python
from django.conf import settings
from django.db import models
 
 
class UserInfo(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name="user_info",
    )
    display_name = models.CharField(max_length=120)

OneToOneField expresses the domain directly

  • A one-to-one field is a unique foreign key with relationship access designed for one related object.

  • settings.AUTH_USER_MODEL supports projects with a custom user model.

  • on_delete=models.CASCADE is a product decision: deleting a user deletes its UserInfo row. Choose intentionally.

  • The database prevents two UserInfo rows from referencing the same user after the migration succeeds.

  • Existing duplicates must be resolved before adding this constraint.

Use UniqueConstraint for a composite rule

helloapp/models.py (one row per user and kind)python
class UserInfo(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    kind = models.CharField(max_length=40)
    value = models.CharField(max_length=200)
 
    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["user", "kind"],
                name="uniq_user_info_kind",
            ),
        ]

Constrain the smallest real identity

  • This model permits several rows per user but only one row for each (user, kind) pair.

  • A named UniqueConstraint documents intent and produces a database constraint.

  • Choose fields from domain identity, not merely from values that happen to be unique in today’s sample data.

  • Consider case normalization, null behavior, conditional constraints, and database support when the rule is more nuanced.

  • Keep model validation for friendly errors, but rely on the database to close concurrency races.

Clean duplicates before applying the constraint

  1. Back up the database and test the migration on a production-like copy.

  2. Enumerate every duplicate group and define an auditable winner/merge rule with the data owner.

  3. Update foreign keys or dependent records that refer to rows being merged.

  4. Delete or archive redundant rows inside a reviewed data migration or controlled maintenance job.

  5. Add the database constraint only after the cleanup query returns no conflicts.

  6. Deploy code that handles the new integrity error safely under concurrent writes.

  7. Rerun duplicate detection and application tests after deployment.

Migration rollout gates

  • A dry run reports exactly which groups would change without mutating them.

  • The merge rule is deterministic and reviewed by someone who owns the data meaning.

  • Rollback or restore steps are tested before the production window.

  • Write traffic is controlled if the cleanup and constraint cannot be applied atomically.

  • Post-migration counts, constraints, dependent references, and API behavior are verified.

Create or update the one object safely

helloapp/services.pypython
from django.db import transaction
 
from .models import UserInfo
 
 
@transaction.atomic
def set_display_name(*, user, display_name):
    info, created = UserInfo.objects.update_or_create(
        user=user,
        defaults={"display_name": display_name},
    )
    return info, created

Convenience still depends on the constraint

  • update_or_create() returns (object, created) and updates defaults when the lookup already exists.

  • transaction.atomic keeps this service operation within one transaction.

  • The database uniqueness constraint is what prevents two committed rows under concurrency.

  • Be prepared for IntegrityError in highly concurrent workflows and retry only when the operation is safe and bounded.

  • Signals and side effects should be designed so retries do not send duplicate emails or events.

Return the right DRF error behavior

helloapp/views.py (unique lookup)python
from django.shortcuts import get_object_or_404
from rest_framework.decorators import api_view
from rest_framework.response import Response
 
 
@api_view(["GET"])
def user_profile(request, user_id):
    info = get_object_or_404(UserInfo, user_id=user_id)
    return Response(UserInfoSerializer(info).data)

This assumes uniqueness has already been enforced

  • get_object_or_404 converts no match into HTTP 404.

  • It is not a duplicate-resolution strategy; a non-unique lookup can still expose MultipleObjectsReturned.

  • Use a unique primary key or a database-enforced unique field for detail endpoints.

  • Do not catch every exception and return 404; that hides database and programming failures.

  • Object-level authorization must still be checked before returning the serialized record.

Test both the contract and the invariant

helloapp/tests/test_user_info.pypython
from django.contrib.auth import get_user_model
from django.db import IntegrityError, transaction
from django.test import TestCase
 
from helloapp.models import UserInfo
 
 
class UserInfoConstraintTests(TestCase):
    def test_one_user_cannot_have_two_profiles(self):
        user = get_user_model().objects.create_user(username="ada")
        UserInfo.objects.create(user=user, display_name="Ada")
 
        with self.assertRaises(IntegrityError):
            with transaction.atomic():
                UserInfo.objects.create(user=user, display_name="Duplicate")

The nested transaction keeps the test usable

  • The test asserts the database invariant rather than only serializer behavior.

  • An inner atomic() savepoint contains the expected integrity failure so Django’s test transaction is not left broken.

  • Add API tests for an empty collection, multiple results, unique detail lookup, permissions, ordering, and pagination.

  • Use TransactionTestCase or database-appropriate concurrency tests when validating true race behavior.

  • Run tests against the same database engine used in production when constraint semantics matter.

Production signals worth watching

  • Count MultipleObjectsReturned and integrity failures by endpoint without logging sensitive payloads.

  • Alert on a renewed duplicate-group query after the constraint migration.

  • Track 409/400-class conflict responses separately from unexpected 500 errors.

  • Correlate concurrency failures with retries so a retry loop cannot amplify load.

  • Retain an audit trail for manual merges and exceptional data repairs.

Fast diagnostic map

  • Multiple rows are expected: use filter(), many=True, deterministic ordering, pagination, and collection URL semantics.

  • Only one row should exist: clean data, add OneToOneField or UniqueConstraint, then keep get().

  • The lookup is supposed to use one record ID: query the primary key rather than a non-unique user foreign key.

  • Duplicates appear only under load: enforce uniqueness in the database and handle the resulting integrity race.

  • `.first()` makes the error disappear: the ambiguity still exists; define the selection rule or fix the invariant.

  • A DRF detail endpoint returns 500: verify its lookup field is unique and do not translate data corruption into a misleading 404.

  • Constraint migration fails: duplicate rows remain or the intended uniqueness rule does not match real production data.

A durable completion checklist

  • The team has decided whether the relationship is one-to-many or one-to-one.

  • Duplicate groups were measured before code or data changes.

  • Collection endpoints return arrays and detail endpoints use unique identities.

  • Ordering and pagination are explicit for multi-row results.

  • The database enforces every true uniqueness invariant.

  • Existing data was migrated with a reviewed and auditable merge rule.

  • Concurrent creation/update behavior is tested and integrity errors are handled intentionally.

  • Permissions prevent callers from selecting another user’s records.

  • Tests cover zero, one, many, duplicate, and race-relevant cases.

Official Django and DRF references