You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django REST Framework URL无法映射到模型实例视图问题

问题:Django REST Framework + MongoDB 获取单条数据返回404,get_object方法未执行

我有一个基于Django + DRF的应用,使用MongoDB(Djongo/pymongo)作为数据库,API路径设计为/api/v1/<object name>[/id]。目前GET /api/v1/whs可以正常返回Wh对象列表,但调用GET /api/v1/whs/640444031170f24828f324bc/时,不仅get_object方法里的打印语句没执行,还返回404错误,且该ObjectId对应的文档确实存在于数据库中。返回内容如下:

HTTP 404 Not Found
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "detail": "Not found."
}

相关代码

settings.py 核心配置

INSTALLED_APPS = [
    ...
    'rest_framework',
    ...
]

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

models.py

from django.db import models
from djongo.models import fields
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.hashers import make_password

CHARFIELD_MAX_LENGTH = 1024

class AbstractModel(models.Model):
    _id = fields.ObjectIdField()
    external_id = models.CharField(max_length = CHARFIELD_MAX_LENGTH, null = True, blank = True,)
    options = models.JSONField(null = True, blank = True,)
    created_at = models.DateTimeField(auto_now_add = True, null = False, blank = False,)
    updated_at = models.DateTimeField(null = True, blank = True,)

    class Meta:
        abstract = True
        ordering = ['created_at']

    def id(self):
        return self._id

class Wh(AbstractModel):
    name = models.CharField(max_length = CHARFIELD_MAX_LENGTH, null = False, blank = False,)
    address = models.CharField(max_length = CHARFIELD_MAX_LENGTH, null = False, blank = False,)
    map = models.JSONField( null = False, blank = False,)
    parent_wh = models.ForeignKey('self', on_delete = models.CASCADE, related_name = 'nested_whs', null = True, blank = True,)
    operator = models.ForeignKey('User', on_delete = models.CASCADE, related_name = 'whs', null = True, blank = True,)

    class Meta(AbstractModel.Meta):
        abstract = False
        verbose_name = 'wh'
        verbose_name_plural = 'whs'

    def __str__(self):
        return '[Wh' + self.name + ']'

class User(AbstractUser, AbstractModel):
    class Meta(AbstractUser.Meta):
        abstract = False
        verbose_name = 'user'
        verbose_name_plural = 'users'

    def __str__(self):
        return self.username

serializers.py

from .models import *
from rest_framework import serializers

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        exclude = ['password']

class WhSerializer(serializers.HyperlinkedModelSerializer):
    operator = UserSerializer()
    class Meta:
        model = Wh
        fields = '__all__'

views.py

from django.http import HttpResponseRedirect
from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext as _
from .models import *
from .forms import *
from django.contrib.auth.decorators import login_required
from bson import ObjectId
from rest_framework import viewsets
from rest_framework import permissions
from rest_framework.generics import RetrieveAPIView
from .serializers import *

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]

class WhViewSet(viewsets.ModelViewSet):
    queryset = Wh.objects.all()
    serializer_class = WhSerializer
    permission_classes = [permissions.IsAuthenticated]

class WhDetail(RetrieveAPIView):
    queryset = Wh.objects.all()
    serializer_class = WhSerializer

    def get_object(self):
        id = self.kwargs.get('id')
        print(id)
        return get_object_or_404(self.get_queryset(), _id=ObjectId(id))

urls.py

from django.urls import include, path
from rest_framework import routers
from . import views

router = routers.DefaultRouter()
router.register(r'whs', views.WhViewSet)
router.register(r'users', views.UserViewSet)

urlpatterns = [
    path('', views.home, name='home'),
    path('wh/<str:id>', views.wh, name='wh'),
    path('api/v1/', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
    path('api/v1/whs/<str:id>', views.WhDetail.as_view(), name='whs'),
]

问题原因及解决办法

核心问题

  1. 路由优先级冲突:DefaultRouter会自动为WhViewSet生成详情路由(/api/v1/whs/<pk>/),且include(router.urls)在你手动添加的路由前面,导致请求被WhViewSet先拦截,你的WhDetail视图根本没被触发。
  2. 主键字段不匹配:DRF默认用pk作为URL参数名查询数据,但你的模型主键是MongoDB的_id,WhViewSet无法正确将URL中的pk映射到_id字段,导致查询失败返回404。

最优解决方案(无需额外视图)

直接修改WhViewSet,适配MongoDB的_id主键,删除冗余的WhDetail视图和手动路由:

1. 更新views.py中的WhViewSet

class WhViewSet(viewsets.ModelViewSet):
    queryset = Wh.objects.all()
    serializer_class = WhSerializer
    permission_classes = [permissions.IsAuthenticated]
    lookup_field = '_id'  # 指定模型主键字段为_id
    lookup_value_regex = '[0-9a-fA-F]{24}'  # 匹配MongoDB ObjectId的正则规则

    def get_object(self):
        # 手动将URL中的_id转换为bson.ObjectId类型
        queryset = self.get_queryset()
        filter_kwargs = {self.lookup_field: ObjectId(self.kwargs[self.lookup_field])}
        obj = get_object_or_404(queryset, **filter_kwargs)
        self.check_object_permissions(self.request, obj)
        return obj

2. 清理urls.py

删除手动添加的path('api/v1/whs/<str:id>', views.WhDetail.as_view(), name='whs'),,保留router自动生成的路由即可。

备选方案(保留自定义视图)

如果一定要用WhDetail,需调整路由顺序让自定义路由先匹配:

urlpatterns = [
    path('', views.home, name='home'),
    path('wh/<str:id>', views.wh, name='wh'),
    path('api/v1/whs/<str:id>', views.WhDetail.as_view(), name='whs'),  # 移到router路由前面
    path('api/v1/', include(router.urls)),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

同时更新WhDetail视图,指定主键字段:

class WhDetail(RetrieveAPIView):
    queryset = Wh.objects.all()
    serializer_class = WhSerializer
    lookup_field = '_id'
    lookup_value_regex = '[0-9a-fA-F]{24}'

    def get_object(self):
        id = self.kwargs.get(self.lookup_field)
        print(id)
        return get_object_or_404(self.get_queryset(), _id=ObjectId(id))

额外注意

  • 若使用HyperlinkedModelSerializer,需确保序列化器能正确处理_id作为主键,或改用ModelSerializer(无需超链接字段时更简洁)。
  • 确认请求用户拥有单条数据的访问权限,虽然列表接口正常,但单条数据的权限校验逻辑可能不同。

内容的提问来源于stack exchange,提问作者omega royal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 22:07:21