DevGang
Авторизоваться

Объединяем наборы запросов в Django с разными моделями

Сегодня произошел такой кейс, что мне нужно было иметь наборы запросов, которые содержали объекты из разных моделей. У Django есть изящный фреймворк «contenttypes framework», который довольно хорошо справляется с данной задачей. Поэтому хочу поделиться с вами моим сегодняшним опытом и надеюсь, что это будет вам полезно и интересно. 

ПРИМЕЧАНИЕ. Если вы умеете создавать свои модели с нуля, то я бы не назвал это лучшим подход. Перейдите к 5 шагу и вы узнаете почему.

1. Модели

Давайте рассморим данные модели:

class Bmw(models.Model):
   series = models.CharField(max_length=50)
   created = models.DateTimeField()
   class Meta:
       ordering = ['-created']
   def __str__(self):
        return "{0} - {1}".format(self.series, self.created.date())
class Tesla(models.Model):
   series = models.CharField(max_length=50)
   created = models.DateTimeField()
   class Meta:
       ordering = ['-created']
   def __str__(self):
       return "{0} - {1}".format(self.series, self.created.date())

Примечание:
Очевидно, что в этом случае мы можем иметь родительский класс с общими свойствами, но для простоты мы не будем его усложнять.

2. Запросы

Мы можем получить список Bmw и Teslas таким образом:

>>> Bmw.objects.filter()
[, ]
>>> Tesla.objects.filter()
[, ]

Но что, если мы хотим объединить два набора запросов, скажем, мы хотим отобразить все автомобили на странице нашего дилера по дате создания. Итак, мы хотим получить что-то вроде:

[, , , ]

Как мы можем сделать это? Ниже приведены два варианта.

3. Использование цепочки из itertools

Первым вариантом реализации станет использавание itertools chain.

from itertools import chain
def get_all_cars():
   bmws = Bmw.objects.filter()
   teslas = Tesla.objects.filter()
   cars_list = sorted(
       chain(bmws, teslas),
       key=lambda car: car.created, reverse=True)
   return cars_list

Здесь мы получаем набор запросов для Bmws и набор запросов из Teslas, и передаем их в функцию цепочки, которая объединяет эти две итерации и создает новый итератор. Затем мы передаем этот список в функцию сортировки и указываем, что хотим отсортировать его по дате создания. Наконец, мы говорим, что хотим, чтобы заказ был отменен. Вот результат:

[, , , ]

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

Примечание: Обратите внимание, что типы объектов здесь имеют тип Bmw и Tesla.

4. Сontenttypes фреймворк

Django contenttypes framework - действительно хороший вариант реализации для данного варианта использования.

Выдержка из документации:

В основе приложения contenttypes лежит модель ContentType, который находится по адресу django.contrib.contenttypes.models.ContentType. Экземпляры ContentType представляют и хранят информацию о моделях, установленных в вашем проекте, а новые экземпляры ContentType автоматически создаются при установке новых моделей.

Я советую вам ознакомиться с этим более подробно об этом.

5. Content Types в наших моделях

В документации сказано:

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

Таким образом, мы добавляем новую модель в наш модуль модели под названием автомобиль.

class Car(models.Model):
   content_type = models.ForeignKey(ContentType)
   object_id = models.PositiveIntegerField()
   content_object = GenericForeignKey('content_type', 'object_id')
   created = models.DateTimeField()
   class Meta:
       ordering = ['-created']
   def __str__(self):
       return "{0} - {1}".format(self.content_object.series,
                                 self.created.date())

Затем мы обновляем наши модели и определяем обработчик сохранения post save handler.

def create_car(sender, instance, created, **kwargs):
   """
   Post save handler to create/update car instances when
   Bmw or Tesla is created/updated
   """
   content_type = ContentType.objects.get_for_model(instance)
   try:
       car= Car.objects.get(content_type=content_type,
                            object_id=instance.id)
   except Car.DoesNotExist:
       car = Car(content_type=content_type, object_id=instance.id)
   car.created = instance.created
   car.series = instance.series
   car.save()

Добавляем обработчик сохранения post save записи в нашу модель Tesla.

class Tesla(models.Model):
   series = models.CharField(max_length=50)
   created = models.DateTimeField()
   class Meta:
       ordering = ['-created']
   def __str__(self):
       return "{0} - {1}".format(self.series, self.created.date())
post_save.connect(create_car, sender=Tesla)

(и аналогичным образом добавляем для модели Bmw, для краткости я не стал привозить код). Так что теперь каждый раз, когда экземпляр Tesla или Bmw создается или обновляется, обновляется соответствующий экземпляр модели Car.

Примечание:
Если вы можете самостоятельно настроить модели, вы можете улучшить нынешнюю архитектуру, в котором мы храним такие типы автомобилей, как «Tesla», «BMW», в отдельной таблице и имеем внешний ключ для таблицы «Автомобили». «contenttypes framework» может пригодиться, если у вас нет возможности изменять существующие модели.

6. Используем contenttypes framework в запросах

Таким образом выглядит обновленный запрос, использующий contentypes framework, который мы только что настроили. Обратите внимание, что у нас оба объекта Bmw и Tesla возвращаются как экземпляры Car.

>>> Car.objects.filter()
[, , , ]

Здесь мы вернули объекты автомобилей, и далее мы получаем фактический тип автомобиля, который содержит экземпляр Car:

>>> car = Car.objects.first()
>>> car.content_object

>>> car.content_object.series
u'Tesla Series 2'

7. Резюме

Хотя этот подход требует дополнительной таблицы для больших наборов запросов, код будет намного более чистым и менее связным. 

#Python #Django
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

Поделитесь своим опытом, расскажите о новом инструменте, библиотеке или фреймворке. Для этого не обязательно становится постоянным автором.

Попробовать

Напиши статью и выиграй годовую подписку на Яндекс плюс или лицензию от Jet Brains

Участвовать