Расширенные модели Django: улучшите свою разработку на Python

Оглавление

Введение

Модели являются основной концепцией фреймворка Django. Согласно философии проектирования моделей от Django, мы должны быть как можно более явными в именовании и функциональности наших полей, и убедиться, что мы включаем всю необходимую функциональность, связанную с нашей моделью, в саму модель, а не в представления или куда-то еще. Если вы уже работали с Ruby on Rails, эти принципы проектирования не покажутся вам новыми, поскольку и Rails, и Django реализуют паттерн Active Record pattern для своих систем объектно-реляционного отображения (ORM) для работы с хранимыми данными.

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

getter/setter/deleter свойства

Как особенность Python, начиная с версии 2.2, использование свойства выглядит как атрибут, но на самом деле является методом. Хотя использование свойства в модели не так уж продвинуто, мы можем использовать некоторые недостаточно используемые возможности свойства Python, чтобы сделать наши модели более мощными.

Если вы используете встроенную аутентификацию Django или настроили свою аутентификацию с помощью AbstractBaseUser, вы, вероятно, знакомы с полем last_login, определенным в модели User, которое представляет собой сохраненную временную метку последнего входа пользователя в ваше приложение. Если мы хотим использовать last_login, но при этом иметь поле с именем last_seen, сохраняемое в кэш чаще, мы можем сделать это довольно просто.

Сначала мы сделаем Python property, который найдет значение в кэше, а если не сможет, то вернет значение из базы данных.

accounts/models.py

from django.contrib.auth.base_user import AbstractBaseUser
from django.core.cache import cache


class User(AbstractBaseUser):
    ...
    
    @property
    def last_seen(self):
        """
        Returns the 'last_seen' value from the cache for a User.
        """
        last_seen = cache.get('last_seen_{0}'.format(self.pk))

        # Check cache result, otherwise return the database value
        if last_seen:
            return last_seen

        return self.last_login

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

Приведенное выше свойство проверяет наш кэш на наличие значения пользователя last_seen, и если оно ничего не находит, то возвращает сохраненное значение пользователя last_login из модели. Ссылка на <instance>.last_seen теперь предоставляет гораздо более настраиваемый атрибут нашей модели за очень простым интерфейсом.

Мы можем расширить это, чтобы включить пользовательское поведение, когда значение присваивается нашему свойству (some_user.last_seen = some_date_time) или когда значение удаляется из свойства (del some_user.last_seen).

...
    
@last_seen.setter
def last_seen(self, value):
    """
    Sets the 'last_seen_[uuid]' value in the cache for a User.
    """
    now = value

    # Save in the cache
    cache.set('last_seen_{0}'.format(self.pk), now)

@last_seen.deleter
def last_seen(self):
    """
    Removes the 'last_seen' value from the cache.
    """
    # Delete the cache key
    cache.delete('last_seen_{0}'.format(self.pk))
    
...

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

В аналогичном случае использования библиотека python-social-auth, инструмент для управления аутентификацией пользователей с помощью сторонних платформ, таких как GitHub и Twitter, будет создавать и управлять обновлением информации в вашей базе данных на основе информации с платформы, с которой пользователь вошел в систему. В некоторых случаях возвращаемая информация не будет совпадать с полями в нашей базе данных. Например, библиотека python-social-auth при создании пользователя передаст аргумент ключевого слова fullname. Если, возможно, в нашей базе данных мы использовали full_name в качестве имени атрибута, то мы можем оказаться в затруднительном положении.

@property
def fullname(self) -> str:
    return self.full_name

@fullname.setter
def fullname(self, value: str):
    self.full_name = value

Теперь, когда python-social-auth сохраняет данные пользователя fullname в нашей модели (new_user.fullname = 'Some User'), мы будем перехватывать их и сохранять в поле нашей базы данных full_name вместо этого.

through модельные отношения

Отношения "многие ко многим" в Django - отличный способ простой обработки сложных отношений между объектами, но они не дают нам возможности добавлять пользовательские атрибуты к создаваемым ими intermediate models. По умолчанию это просто включает идентификатор и две ссылки на внешний ключ, чтобы соединить объекты вместе.

Используя параметр Django ManyToManyField through, мы можем сами создать эту промежуточную модель и добавить любые дополнительные поля, которые посчитаем нужными.

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

accounts/models.py

import uuid

from django.contrib.auth.base_user import AbstractBaseUser
from django.db import models
from django.utils.timezone import now


class User(AbstractBaseUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    …

class Group(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    members = models.ManyToManyField(User, through='Membership')

class Membership(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    group = models.ForeignKey(Group, on_delete=models.CASCADE)
    joined = models.DateTimeField(editable=False, default=now)

В приведенном выше примере мы все еще используем ManyToManyField для обработки отношений между пользователем и группой, но, передав модель Membership с помощью ключевого аргумента through, мы можем добавить в модель наш пользовательский атрибут joined для отслеживания времени начала членства в группе. Эта модель through является стандартной моделью Django, она просто требует первичный ключ (здесь мы используем UUID), и два внешних ключа для объединения объектов вместе.

Используя тот же шаблон из трех моделей, мы можем создать простую базу данных подписки для нашего сайта:

import uuid

from django.contrib.auth.base_user import AbstractBaseUser
from django.db import models
from django.utils.timezone import now


class User(AbstractBaseUser):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    ...

class Plan(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=50, unique=True, default='free')
    subscribers = models.ManyToManyField(User, through='Subscription', related_name='subscriptions', related_query_name='subscriptions')

class Subscription(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    plan = models.ForeignKey(Plan, on_delete=models.CASCADE)
    created = models.DateTimeField(editable=False, default=now)
    updated = models.DateTimeField(auto_now=True)
    cancelled = models.DateTimeField(blank=True, null=True)

Здесь мы можем отслеживать, когда пользователь впервые подписался, когда он обновил свою подписку, и, если мы добавили пути кода для этого, когда пользователь отменил свою подписку на наше приложение.

Использование моделей through с моделями ManyToManyField - это отличный способ добавить больше данных к нашим промежуточным моделям и обеспечить более глубокий опыт для наших пользователей без особых дополнительных усилий.

Обычно в Django, когда вы делаете подкласс модели (сюда не входят абстрактные модели) в новый класс, фреймворк создает новые таблицы базы данных для этого класса и связывает их (через OneToOneField) с родительскими таблицами базы данных. Django называет это "многотабличным наследованием", и это отличный способ повторно использовать существующие поля и структуры моделей и добавлять в них свои собственные данные. "Не повторяйтесь", как гласит философия дизайна Django.

Пример наследования нескольких таблиц:

from django.db import models

class Vehicle(models.Model):
    model = models.CharField(max_length=50)
    manufacturer = models.CharField(max_length=80)
    year = models.IntegerField(max_length=4)

class Airplane(Vehicle):
    is_cargo = models.BooleanField(default=False)
    is_passenger = models.BooleanField(default=True)

В этом примере создаются таблицы базы данных vehicles_vehicle и vehicles_airplane, связанные внешними ключами. Это позволяет нам использовать существующие данные, которые находятся внутри vehicles_vehicle, добавляя при этом наши собственные атрибуты, специфичные для автомобиля, к каждому подклассу, vehicle_airplane, в данном случае.

В некоторых случаях нам может вообще не понадобиться хранить дополнительные данные. Вместо этого мы можем изменить поведение родительской модели, возможно, добавив метод, свойство или менеджер модели. Именно здесь и проявляются преимущества proxy models. Proxy models позволяют нам изменить поведение Python модели без изменения базы данных .

vehicles/models.py

from django.db import models

class Car(models.Model):
    vin = models.CharField(max_length=17)
    model = models.CharField(max_length=50)
    manufacturer = models.CharField(max_length=80)
    year = models.IntegerField(max_length=4)
    ...

class HondaManager(models.Manager):
    def get_queryset(self):
        return super(HondaManager, self).get_queryset().filter(model='Honda')

class Honda(Car):
    objects = HondaManager()
    
    class Meta:
        proxy = True
    
    @property
    def is_domestic(self):
        return False
    
    def get_honda_service_logs(self):
        ...

Proxy models объявляются так же, как и обычные модели. В нашем примере мы сообщаем Django, что Honda является proxy model, устанавливая атрибут proxy класса Honda Meta в True. Я добавил пример свойства и заглушки метода, но вы можете видеть, что мы добавили пользовательский менеджер моделей к нашей Honda proxy model.

Это гарантирует, что всякий раз, когда мы запрашиваем объекты из базы данных, используя нашу модель Honda, мы получаем обратно только Car экземпляры, где model= 'Honda'. Прокси-модели облегчают нам быстрое добавление настроек поверх существующих моделей, использующих те же данные. Если мы удалим, создадим или обновим любой экземпляр Car с помощью нашей модели Honda или менеджера, он будет сохранен в базе данных vehicles_car точно так же, как если бы мы использовали родительский (Car) класс.

Завершение

Если вам уже удобно работать с классами в Python, то вы будете чувствовать себя как дома с моделями Django: наследование, множественное наследование, переопределение методов и интроспекция. Все эти модели являются частью того, как был разработан объектно-реляционный отображатель Django.

Наследование многотабличности и ручное определение промежуточных таблиц для SQL-соединений не обязательно являются базовыми концепциями, но реализуются просто с помощью ноу-хау Django и Python. Возможность использовать возможности языка и фреймворка наряду друг с другом - одна из причин, по которой Django является популярным веб-фреймворком

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

Вернуться на верх