Перевод статьи Using Django Check Constraints to Prevent the Storage of The Empty String, опубликованный сайтом webdevblog.ru. В статье рассказывается об варианте использования Django класса CheckConstraint
для создания ограничения в базе данных.
По умолчанию модель CharField хранит любую строку, поддерживаемую базой данных. Сюда входит и пустая строка, что часто неуместно.
Например, представьте, что у нас есть модель Team, представляющая группу, к которой могут принадлежать пользователи. Начнем с того, дадим ему уникальное имя name:
from django.db import models class Team(models.Model): name = models.CharField(max_length=120) class Meta: constraints = [ models.UniqueConstraint( name="%(app_label)s_%(class)s_name_unique", fields=["name"], ), ]
Хотя это определение модели кажется разумным, оно позволяет создавать записи с пустым name. Такой командный объект может нарушить работу других частей нашего приложения. Например, ссылки могут не отображаться или могут возникнуть непредвиденные ошибки из-за неправильного name.
Мы могли бы попытаться предотвратить создание команд с пустыми именами с помощью проверки формы, но это не предотвратит создание неверных данных с помощью других процессов, таких как массовый импорт. Чтобы гарантировать, что все данные в нашей системе действительны, нам нужно ограничение базы данных (database constraint).
Здесь мы хотим добавить минимальное ограничение базы данных, которое гарантирует, что длина name
больше нуля. Давайте рассмотрим как это сделать.
Во-первых, нам нужно зарегистрировать функцию Length
для использования с CharField. Это сделает его доступным для преобразование или как поиск по умолчанию, такой как <field>__gt для «поле больше чем». Для регистрации функции требуется только один вызов функции:
from django.db import models from django.db.models.functions import Length models.CharField.register_lookup(Length)
Мы можем добавить это в начало файла наших моделей. В Django есть много таких функций, которые мы можем зарегистрировать как дополнительные поиски / преобразования — ознакомьтесь с другими ссылками на register_lookup () в документации по функциям базы данных.
Во-вторых, мы добавляем наше новое ограничение в Meta.constraints. Для этого используем объект Q (), чтобы представить, какие данные действительны — если длина имени больше 0:
from django.db import models from django.db.models.functions import Length models.CharField.register_lookup(Length) class Team(models.Model): name = models.CharField(max_length=120) class Meta: constraints = [ models.UniqueConstraint( name="%(app_label)s_%(class)s_name_unique", fields=["name"], ), models.CheckConstraint( name="%(app_label)s_%(class)s_name_not_empty", check=models.Q(name__length__gt=0), ), ]
Затем мы запустили makemigrations, чтобы создать новую миграцию:
$ ./manage.py makemigrations core Migrations for 'core': example/core/migrations/0002_team_core_team_name_not_empty.py - Create constraint core_team_name_not_empty on model team
Эта миграция состоит только из одного шага с добавлением нового ограничения:
from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ("core", "0001_initial"), ] operations = [ migrations.AddConstraint( model_name="team", constraint=models.CheckConstraint( check=models.Q(name__length__gt=0), name="core_team_name_not_empty", ), ), ]
В-третьих, мы можем написать тест. Мне нравится делать это для всех проверочных ограничений, чтобы убедиться, что они делают то, что я имел в виду. Здесь наш тест пытается создать плохую команду и проверить (assert), что база данных вызывает IntegrityError с упоминанием имени нашего ограничения:
from django.db import IntegrityError from django.test import TestCase from example.core.models import Team class TeamTests(TestCase): def test_name_non_empty(self): constraint_name = "core_team_name_not_empty" with self.assertRaisesMessage(IntegrityError, constraint_name): Team.objects.create(name="")
В-четвертых, прежде чем мы сможем развернуть наше изменение, нам нужно убедиться, что в нашей производственной базе данных нет команды с пустым именем. Если такая группа действительно существует, база данных не сможет применить миграцию, поскольку ограничения применяются ко всем данным.
Мы можем сделать это с помощью оболочки:
$ ./manage.py shell Python 3.9.1 (default, Jan 21 2021, 09:04:53) Type 'copyright', 'credits' or 'license' for more information IPython 7.19.0 -- An enhanced Interactive Python. Type '?' for help. In [1]: from example.core.models import Team In [2]: Team.objects.filter(name="").count() Out[2]: 0
Отлично, нулевой результат. Если бы такая команда существовала, мы бы хотели ее исправить, возможно, переименовав или удалив ее.
В-пятых, желательно добавить дополнительную проверку, ориентированную на пользователя, например в формах или конечных точках API. Это может стать хорошим сообщением об ошибке для пользователя, если он попытается создать такую команду. Без такой проверки попытки использования пустой строки завершатся сбоем:
In [3]: Team.objects.create(name="") --------------------------------------------------------------------------- IntegrityError Traceback (most recent call last) ... IntegrityError: CHECK constraint failed: core_team_name_not_empty
В формах и DRF CharFields мы можем сделать это, объявив min_length = 1.
Теперь вы можете хранить только валидные данные.