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
DoesNotExistexception.More than one match raises the model’s
MultipleObjectsReturnedexception.The lookup
userid=pkis not unique merely because the variable is namedpk; 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
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
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=Truetells 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
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 querysetList and detail URLs now mean different things
GET /api/user-info/?user_id=7is a filtered collection and may return zero, one, or many records.GET /api/user-info/42/retrieves oneUserInforesource by its unique primary key.ReadOnlyModelViewSetexposes 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.
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_MODELsupports projects with a custom user model.on_delete=models.CASCADEis 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
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
UniqueConstraintdocuments 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
Back up the database and test the migration on a production-like copy.
Enumerate every duplicate group and define an auditable winner/merge rule with the data owner.
Update foreign keys or dependent records that refer to rows being merged.
Delete or archive redundant rows inside a reviewed data migration or controlled maintenance job.
Add the database constraint only after the cleanup query returns no conflicts.
Deploy code that handles the new integrity error safely under concurrent writes.
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
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, createdConvenience still depends on the constraint
update_or_create()returns(object, created)and updates defaults when the lookup already exists.transaction.atomickeeps this service operation within one transaction.The database uniqueness constraint is what prevents two committed rows under concurrency.
Be prepared for
IntegrityErrorin 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
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_404converts 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
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
TransactionTestCaseor 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
MultipleObjectsReturnedand 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
OneToOneFieldorUniqueConstraint, then keepget().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
Django QuerySet get() documents the zero-, one-, and multiple-match behavior.
Django model constraints documents
UniqueConstraintand validation.Django one-to-one relationships shows database-modeled single related objects.
Django transactions explains
atomic()and transaction behavior.DRF serializers explains
many=True.DRF filtering covers queryset and request-driven collection filtering.
Comments and corrections