Django REST Framework: Как получить объект по имени?

В работе с Django REST Framework (DRF) часто возникает потребность получения детальной информации не просто по первичному ключу (PK), который используется по умолчанию, а по другому уникальному идентификатору, например, по имени, slug’у или какому-либо коду.

Введение в Django REST Framework и получение объектов

Краткий обзор Django REST Framework

Django REST Framework — это мощный и гибкий инструментарий для быстрого построения Web API на основе Django. Он предоставляет множество готовых решений для сериализации данных, аутентификации, авторизации, управления правами доступа и, конечно же, удобные дженерики (Generic APIView, ViewSets) для реализации стандартных операций CRUD (Create, Retrieve, Update, Delete).

По умолчанию, дженерики DRF, такие как RetrieveAPIView или ModelViewSet, ожидают получения идентификатора объекта в URL-параметрах, который по умолчанию соответствует первичному ключу модели (pk).

Задача: получение объекта по имени (или другому уникальному полю)

Стандартный URL для получения объекта выглядит примерно так: /api/items/1/, где 1 — это PK. Однако, для удобства клиента API или по требованиям бизнеса, может потребоваться доступ к ресурсу по более "человекочитаемому" или знаковому идентификатору, например: /api/products/apple/ или /api/users/john_doe/.

Наша задача — настроить ViewSet или APIView таким образом, чтобы он корректно находил и возвращал объект модели, используя в качестве ключа для поиска не PK, а значение другого уникального поля, такого как name, slug или username.

Предполагаемые знания и подготовка

Материал рассчитан на разработчиков, уже знакомых с основами Django и Django REST Framework: созданием моделей, сериализаторов, URL-маршрутов и базовым использованием ViewSet’ов или Generic APIView. Предполагается, что у вас есть работающий проект Django с установленным DRF.

Настройка сериализатора и ViewSet

Прежде чем настроить получение по имени, у нас должна быть модель, сериализатор для нее и ViewSet.

Определение модели Django

Допустим, у нас есть модель Product с полями name (уникальное), description и price.

# models.py

from django.db import models

class Product(models.Model):
    # Имя продукта, должно быть уникальным для поиска
    name: str = models.CharField(max_length=255, unique=True)
    description: str = models.TextField(blank=True)
    price: float = models.DecimalField(max_digits=10, decimal_places=2)

    def __str__(self) -> str:
        return self.name

    class Meta:
        verbose_name = "Продукт"
        verbose_name_plural = "Продукты"

Важно, чтобы поле, по которому мы будем искать (в данном случае name), имело свойство unique=True в модели. Это гарантирует, что по данному значению будет найден единственный объект, что соответствует логике получения одного конкретного ресурса (retrieve operation).

Создание сериализатора для модели

Создадим простой ModelSerializer для нашей модели Product.

# serializers.py

from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ('id', 'name', 'description', 'price')
        # Можно явно указать read_only_fields, если нужно
        # read_only_fields = ('id',)

Сериализатор определяет, как данные модели будут преобразованы в формат, пригодный для API (например, JSON) и обратно.

Реализация ViewSet для обработки запросов

Для удобства часто используют ModelViewSet, который объединяет логику для всех стандартных операций (list, retrieve, create, update, destroy). Начнем с базовой реализации.

# views.py

from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer

class ProductViewSet(viewsets.ModelViewSet):
    # Определение QuerySet'а для выборки объектов
    queryset = Product.objects.all()
    # Указание сериализатора для преобразования данных
    serializer_class = ProductSerializer
    # По умолчанию lookup_field равен 'pk'

Теперь этот ViewSet может обрабатывать запросы на /api/products/ (список и создание) и /api/products/<pk>/ (получение, обновление, удаление). Наша цель — изменить поведение /api/products/<identifier>/, чтобы <identifier> использовал поле name вместо pk.

Получение объекта по имени: различные подходы

Существует несколько способов заставить DRF искать объект по другому полю.

Использование lookup_field в ViewSet

Самый простой и рекомендуемый способ для большинства случаев — это указать атрибут lookup_field в вашем ViewSet.

# views.py (продолжение)

from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    # DRF будет искать объект по полю 'name'
    lookup_field = 'name'
    # Optionally, also define the lookup_url_kwarg
    # lookup_url_kwarg = 'product_name'

Если вы просто указываете lookup_field = 'name', DRF ожидает, что в URL будет присутствовать параметр с тем же именем, что и поле (name), например, /api/products/apple/. Если вы хотите использовать другое имя параметра в URL (например, /api/products/apple/ где apple передается как product_name), вы можете дополнительно указать lookup_url_kwarg = 'product_name'.

Это наиболее чистый и предпочтительный подход, так как DRF автоматически обрабатывает поиск объекта (вызывая queryset.get(**{self.lookup_field: self.kwargs[self.lookup_url_kwarg]})) и обработку случая, когда объект не найден (возвращая 404).

Переопределение get_object() для кастомизации поиска

Если логика поиска более сложна или требует дополнительных проверок, вы можете переопределить метод get_object() в вашем ViewSet или Generic APIView.

# views.py (продолжение)

from rest_framework import viewsets
from rest_framework.generics import get_object_or_404
from .models import Product
from .serializers import ProductSerializer

class ProductViewSet(viewsets.ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    lookup_field = 'name' # Можно оставить для получения имени из URL по умолчанию

    def get_object(self) -> Product:
        """
        Извлекает объект Product, используя поле 'name' из URL.
        """
        # Получаем значение параметра из URL. Используем lookup_field
        # в качестве имени ключа словаря self.kwargs по умолчанию.
        # Если вы использовали lookup_url_kwarg, то ключ был бы другим.
        name = self.kwargs.get(self.lookup_field)

        # Ищем объект в QuerySet'е. get_object_or_404 вернет объект
        # или вызовет исключение Http404 (которое DRF преобразует в 404 response).
        obj: Product = get_object_or_404(self.get_queryset(), name=name)

        # Проверяем права доступа на полученный объект (опционально)
        self.check_object_permissions(self.request, obj)

        return obj

Этот подход дает полный контроль над процессом получения объекта. Вы можете добавить логирование, более сложную логику фильтрации или другие проверки перед возвратом объекта.

Применение фильтров Django для поиска по имени

Хотя это не стандартный способ получения (retrieve) одного объекта по URL-параметру в RetrieveAPIView или ModelViewSet, фильтрация очень полезна для поиска объектов в списке (ListAPIView).

Вы можете использовать django-filter или ручную фильтрацию в ListAPIView для поиска по имени, например: /api/products/?name=apple.

# views.py (пример ListAPIView с фильтрацией)

from rest_framework import generics
from .models import Product
from .serializers import ProductSerializer

class ProductListView(generics.ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

    def get_queryset(self):
        """
        Ограничивает QuerySet по параметру name из GET-запроса.
        """
        queryset = super().get_queryset()
        # Получаем значение параметра 'name' из GET-запроса
        name = self.request.query_params.get('name', None)
        if name is not None:
            # Применяем фильтр к QuerySet'у
            queryset = queryset.filter(name__iexact=name) # iexact для регистронезависимого поиска
        return queryset
Реклама

Важно: Этот подход используется для списков объектов, а не для получения одного объекта по URL-параметру в Retrieve-операции. Хотя теоретически можно вернуть один объект из списка, стандартный Retrieve-endpoint (/api/products/apple/) более явно соответствует RESTful-принципам для получения конкретного ресурса.

Обработка ошибок и исключений

Корректная обработка ошибок — ключевой аспект любого API.

Ситуация: Объект не найден (404 Not Found)

Если объект с указанным именем не существует, API должен вернуть статус 404 Not Found. При использовании lookup_field или get_object_or_404, DRF делает это автоматически. Если вы пишете кастомную логику без get_object_or_404, убедитесь, что вы обрабатываете исключение ObjectDoesNotExist (которое наследуется от Http404) и возвращаете соответствующий ответ.

Пример ручной обработки:

# views.py (фрагмент get_object)

from django.core.exceptions import ObjectDoesNotExist
from rest_framework.response import Response
from rest_framework import status

# ... внутри custom get_object ...
try:
    obj = self.get_queryset().get(name=name)
except ObjectDoesNotExist:
    # Возвращаем 404 ответ вручную
    # Альтернатива: raise Http404("Product not found")
    return Response({"detail": "Продукт с таким именем не найден."}, status=status.HTTP_404_NOT_FOUND)
# ... остальная логика ...

Валидация входных данных (например, проверка на наличие имени)

В случае получения объекта по имени (Retrieve), валидация имени как таковая обычно происходит на уровне URL-маршрута или в самой логике поиска (например, если имя — пустая строка). Однако, если вы используете поле имени для создания или обновления, сериализатор автоматически выполнит валидацию unique=True (если оно указано в модели) и другие ограничения полей.

Для кастомной валидации в сериализаторе:

# serializers.py (фрагмент ProductSerializer)

class ProductSerializer(serializers.ModelSerializer):
    # ... Meta класс ...

    def validate_name(self, value: str) -> str:
        """
        Кастомная валидация для поля name.
        """
        if not value:
            raise serializers.ValidationError("Имя продукта не может быть пустым.")
        # Дополнительные проверки, если нужны
        # if ' ' in value: raise serializers.ValidationError("Имя не должно содержать пробелов.")
        return value

Возврат информативных сообщений об ошибках

При возникновении ошибок, DRF по умолчанию возвращает стандартные сообщения и коды статуса (например, 404 Not Found, 400 Bad Request). Если вы перехватываете исключения или выполняете кастомную валидацию, старайтесь возвращать информативные сообщения в теле ответа, чтобы клиент API мог понять причину ошибки.

Пример сообщения об ошибке при 404, возвращаемого по умолчанию DRF или с помощью get_object_or_404:

{
    "detail": "Не найдено."
}

Или более кастомное (как в примере ручной обработки выше):

{
    "detail": "Продукт с таким именем не найден."
}

Пример рабочего кода и тестирование

Соберем все вместе для демонстрации использования lookup_field.

Полный пример кода ViewSet и сериализатора

Предполагая, что у вас есть модель Product как описано выше:

# models.py

from django.db import models

class Product(models.Model):
    name: str = models.CharField(max_length=255, unique=True)
    description: str = models.TextField(blank=True)
    price: float = models.DecimalField(max_digits=10, decimal_places=2)

    def __str__(self) -> str:
        return self.name

# serializers.py

from rest_framework import serializers
from .models import Product

class ProductSerializer(serializers.ModelSerializer):
    class Meta:
        model = Product
        fields = ('id', 'name', 'description', 'price')

# views.py

from rest_framework import viewsets
from .models import Product
from .serializers import ProductSerializer

class ProductViewSet(viewsets.ModelViewSet):
    # QuerySet для выборки всех продуктов
    queryset = Product.objects.all()
    # Сериализатор для преобразования данных
    serializer_class = ProductSerializer
    # !!! Указываем, что поиск осуществляется по полю 'name'
    lookup_field = 'name'

# urls.py (в вашем Django приложении)

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

# Создаем роутер и регистрируем наш ViewSet
router = DefaultRouter()
router.register(r'products', ProductViewSet)

# URL-шаблоны для приложения
urlpatterns = [
    # Подключаем URL'ы, сгенерированные роутером
    path('api/', include(router.urls)),
    # DRF с lookup_field='name' сгенерирует URL типа /api/products/{name}/ для деталей
]

# urls.py (в корне проекта, если требуется)

# from django.contrib import admin
# from django.urls import path, include

# urlpatterns = [
#     path('admin/', admin.site.urls),
#     path('', include('your_app_name.urls')), # Подключаем URL'ы нашего приложения
# ]

В этом примере DRF автоматически создаст маршруты, где для детальных операций (retrieve, update, destroy) будет использоваться параметр name вместо pk, например: /api/products/apple/.

Тестирование API endpoint’а с использованием Postman или curl

Предположим, у вас есть продукт с name='Orange' в базе данных.

Получение списка продуктов:

curl http://127.0.0.1:8000/api/products/

Получение продукта по имени:

curl http://127.0.0.1:8000/api/products/Orange/

Ожидаемый ответ (при наличии продукта):

{
    "id": 123, 
    "name": "Orange",
    "description": "Сочный апельсин",
    "price": "0.99"
}

Попытка получить несуществующий продукт:

curl http://127.0.0.1:8000/api/products/NonExistentProduct/

Ожидаемый ответ:

{
    "detail": "Не найдено."
}

(или кастомное сообщение, если переопределяли get_object)

Postman предоставляет удобный графический интерфейс для выполнения этих запросов и просмотра ответов.

Альтернативные способы тестирования (Django REST Framework Test Client)

Для автоматизированного тестирования API в рамках тестового набора Django можно использовать APIClient из DRF.

# tests.py

from django.test import TestCase
from rest_framework.test import APIClient
from rest_framework import status
from .models import Product

class ProductAPITestCase(TestCase):
    def setUp(self) -> None:
        # Создаем тестовый клиент API
        self.client = APIClient()
        # Создаем тестовые данные в базе
        self.product1 = Product.objects.create(
            name="Apple", description="Красное яблоко", price=1.50
        )
        self.product2 = Product.objects.create(
            name="Banana", description="Желтый банан", price=0.75
        )

    def test_get_product_by_name(self) -> None:
        """
        Тестирование получения продукта по его имени.
        """
        # Делаем GET-запрос по имени продукта
        response = self.client.get(f'/api/products/{self.product1.name}/')

        # Проверяем статус ответа
        self.assertEqual(response.status_code, status.HTTP_200_OK)

        # Проверяем, что полученные данные соответствуют ожидаемым
        self.assertEqual(response.data['name'], self.product1.name)
        self.assertEqual(float(response.data['price']), float(self.product1.price)) # Сравниваем как float

    def test_get_nonexistent_product_by_name(self) -> None:
        """
        Тестирование запроса несуществующего продукта по имени.
        """
        response = self.client.get('/api/products/NonExistentProduct/')

        # Проверяем, что получили статус 404
        self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
        # Проверяем стандартное сообщение DRF о ненахождении
        self.assertEqual(response.data['detail'], 'Не найдено.')

Использование APIClient позволяет полностью автоматизировать проверку поведения вашего API в различных сценариях, включая получение объектов по не-PK полям и обработку ошибок 404.


Добавить комментарий