Написать пост

Расширение модели пользователя в Django: сравнение нескольких стратегий с примерами кода

Аватар Ярослав Сарницкий

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

Обложка поста Расширение модели пользователя в Django: сравнение нескольких стратегий с примерами кода

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

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

В этой статье мы сравним различные способы расширения стандартной модели пользователя в Django.

Способы расширения существующей модели пользователей

Существует четыре разных способа расширения существующей модели пользователя, о которых пойдет речь в статье:

  • использование прокси-модели;
  • использование связи один-к-одному с пользовательской моделью;
  • создание модели пользователя с помощью расширения класса AbstractBaseUser;
  • создание модели пользователя с помощью расширения класса AbstractUser.

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

Использование прокси-модели

Прокси-модель — это модель, отнаследованная от существующей модели без создания новой таблицы в базе данных. Она используется для изменения поведения существующей модели, например, задания сортировки по умолчанию или добавления новых методов, не затрагивающих схему базы данных.

Когда следует использовать прокси-модель?

Прокси-модель используется для расширения существующей модели пользователя, когда в базе данных не нужно хранить дополнительную информацию, а нужно добавить базовой модели дополнительные методы или изменить ее Manager, управляющий запросами к базе данных.

Это то, что мне нужно! Перейти к коду!

Использование связи один-к-одному с пользовательской моделью

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

Когда следует использовать связи один-к-одному?

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

Это то, что мне нужно! Перейти к коду!

Создание модели пользователя через расширение AbstractBaseUser

Это совершенно новая модель пользователя, которая наследуется от AbstractBaseUser. Ее грамотная интеграция требует дополнительных усилий и обновления некоторых связей через settings.py. При выборе этого варианта настоятельно рекомендуется провести все манипуляции с моделями до начала работы над проектом, так как это повлияет на всю схему базы данных. Использование этого способа в готовом проекте может вызвать проблемы при внедрении новой модели.

Когда следует использовать этот способ?

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

Это то, что мне нужно! Перейти к коду!

Создание модели пользователя через расширение AbstractUser

Этот способ также подразумевает создание новой модели пользователя, но которая наследуется уже от AbstractUser. Здесь актуальны все те же замечания, что и для пункта выше: необходимость дополнительных усилий для внедрения и обновления некоторых связей через settings.py, сложности при интеграции в готовый проект.

Когда следует использовать этот способ?

Используется только тогда, когда вас вполне устраивает, как работает аутентификация в Django и вы ничего не хотите в ней менять, но тем не менее, вам нужно добавить дополнительную информацию непосредственно в пользовательскую модель (User), причем без создания дополнительного класса (как в варианте с прокси-моделью).

Это то, что мне нужно! Перейти к коду!

Расширение модели пользователя через прокси-модель

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

Вот как это можно сделать:

			from django.contrib.auth.models import User
from .managers import PersonManager

class Person(User):
    objects = PersonManager()

    class Meta:
        proxy = True
        ordering = ('first_name', )

    def do_something(self):
        ...
		

В приведенном выше примере создается прокси-модель с именем Person. Тот факт, что она является прокси-моделью, указывается внутри класса Meta: proxy = True.

Сама прокси-модель в этом примере используется для переопределения сортировки по умолчанию, назначения нового Manager и определения нового метода do_something.

Отметим, что User.objects.all() и Person.objects.all() будут обращаться к одной и той же таблице базы данных. Единственное различие заключается в поведении, которое определяется для прокси-модели. Вот и все.

Расширение модели пользователя с помощью связи один-к-одному

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

Стоит понимать, что использование этой стратегии приводит к дополнительным запросам или объединениям для получения связанных данных. По сути, когда создается запрос к связанным данным, Django делает дополнительный запрос. Но этого можно избежать в большинстве случаев. Мы к этому вернемся чуть позже.

Обычно в Django такие модели называют Profile:

			from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)
		

А теперь установим сигналы для Profile на автоматическое создание/обновление, когда мы создаем/обновляем стандартную модель пользователя (User):

			from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()
		

Как можно видеть, основная нагрузка — это добавление вызовов create_user_profile и save_user_profile всякий раз, когда происходит сохранение (в том числе создание) объекта. Этот вид сигнала называется post_save.

Дадим еще несколько поясняющих примеров. Можно использовать следующий код в шаблоне:

			<h2>{{ user.get_full_name }}</h2>
<ul>
  <li>Имя пользователя: {{ user.username }}</li>
  <li>Местоположение: {{ user.profile.location }}</li>
  <li>Дата рождения: {{ user.profile.birth_date }}</li>
</ul>
		

Или такой внутри представления:

			def update_profile(request, user_id):
    user = User.objects.get(pk=user_id)
    user.profile.bio = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit...'
    user.save()
		

Вообще говоря, вам никогда не придется вызывать метод сохранения профиля. Все делается методами User.

Отдельно обсудим вопрос использования форм. Можно использовать более одной формы сразу. Как в этом примере:

			# forms.py

class UserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'email')

class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ('url', 'location', 'company')
		
			# views.py

@login_required
@transaction.atomic
def update_profile(request):
    if request.method == 'POST':
        user_form = UserForm(request.POST, instance=request.user)
        profile_form = ProfileForm(request.POST, instance=request.user.profile)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, _('Ваш профиль был успешно обновлен!'))
            return redirect('settings:profile')
        else:
            messages.error(request, _('Пожалуйста, исправьте ошибки.'))
    else:
        user_form = UserForm(instance=request.user)
        profile_form = ProfileForm(instance=request.user.profile)
    return render(request, 'profiles/profile.html', {
        'user_form': user_form,
        'profile_form': profile_form
    })
		
			<!-- profile.html -->

<form method="post">
  {% csrf_token %}
  {{ user_form.as_p }}
  {{ profile_form.as_p }}
  <button type="submit">Сохранить изменения</button>
</form>
		

А теперь о дополнительных запросах к базе данных.

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

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

			users = User.objects.all().select_related('profile')
		

Расширение модели пользователя с помощью наследования AbstractBaseUser

Это самый сложный вариант, старайтесь избегать его любой ценой. Однако иногда это невозможно.

Допустим, нам нужно использовать адрес электронной почты в качестве логина и использование username совершенно бесполезно. Также у нас нет необходимости в использовании флага is_staff, поскольку мы не будем использовать админку Django.

В таком случае пользовательскую модель можно определить так:

			from __future__ import unicode_literals

from django.db import models
from django.core.mail import send_mail
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import ugettext_lazy as _

from .managers import UserManager


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_('email'), unique=True)
    first_name = models.CharField(_('name'), max_length=30, blank=True)
    last_name = models.CharField(_('surname'), max_length=30, blank=True)
    date_joined = models.DateTimeField(_('registered'), auto_now_add=True)
    is_active = models.BooleanField(_('is_active'), default=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_full_name(self):
        '''
        Возвращает first_name и last_name с пробелом между ними.
        '''
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        '''
        Возвращает сокращенное имя пользователя.
        '''
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        '''
        Отправляет электронное письмо этому пользователю.
        '''
        send_mail(subject, message, from_email, [self.email], **kwargs)
		

В коде все сделано так, чтобы сохранить новую пользовательскую модель как можно ближе к существующей модели пользователя. Поскольку мы наследуем от AbstractBaseUser, нужно определить несколько свойств и методов:

  • USERNAME_FIELD — строка, описывающая имя поля в модели пользователя, которая используется как идентификатор. Поле должно быть уникальным (то есть иметь значение unique=True, установленное в его определении);
  • REQUIRED_FIELDS — список имен полей, которые будут запрашиваться при создании пользователя через команду управления createsuperuser;
  • is_active — логический атрибут, указывающий, является ли пользователь активным;
  • get_full_name() — более длинный формальный идентификатор для пользователя. В этом примере будем использовать полное имя пользователя, но это может быть любая строка, которая идентифицирует пользователя;
  • get_short_name() — короткий «неофициальный идентификатор» пользователя. В нашем примере — имя пользователя.

Также нужно определить UserManager. Это связано с тем, что существующий менеджер определяет методы create_user и create_superuser.

А вот как выглядит UserManager, удовлетворяющий перечисленным выше требованиям:

			from django.contrib.auth.base_user import BaseUserManager

class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        """
        Создает и сохраняет пользователя с введенным им email и паролем.
        """
        if not email:
            raise ValueError('email должен быть указан')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(email, password, **extra_fields)
		

Он удаляет существующий UserManager, а также имя пользователя и свойство is_staff.

Теперь последний шаг. Нужно обновить settings.py, а именно свойство AUTH_USER_MODEL:

			AUTH_USER_MODEL = 'core.User'
		

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

Как ссылаться на эту модель? Есть два пути. Рассмотрим модель с названием Course:

			from django.db import models
from testapp.core.models import User

class Course(models.Model):
    slug = models.SlugField(max_length=100)
    name = models.CharField(max_length=100)
    tutor = models.ForeignKey(User, on_delete=models.CASCADE)
		

В коде все отлично, но если вы создаете многоразовое приложение, которое хотите сделать доступным для сообщества, настоятельно рекомендуется использовать следующую стратегию:

			from django.db import models
from django.conf import settings

class Course(models.Model):
    slug = models.SlugField(max_length=100)
    name = models.CharField(max_length=100)
    tutor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
		

Расширение модели пользователя с помощью наследования AbstractUser

Здесь все довольно просто, поскольку класс django.contrib.auth.models.AbstractUser обеспечивает полную реализацию пользователя по умолчанию как абстрактную модель:

			from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)
		

Затем вам как и в предыдущем способе нужно обновить settings.py, определяя свойство AUTH_USER_MODEL:

			AUTH_USER_MODEL = 'core.User'
		

Это нужно сделать перед началом работ над проектом, так как это повлияет на всю схему базы данных. Также старайтесь создавать внешние ключи для модели пользователя, импортируя параметры из from django.conf import settings и ссылаясь на  settings.AUTH_USER_MODEL вместо прямого обращения к новой пользовательской модели.

Выводы

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

Следите за новыми постами
Следите за новыми постами по любимым темам
49К открытий49К показов