UUIDv7 в Django

Перевод!!! Первоисточник здесь.

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

Что такое UUIDv7?

Как и другие UUID, UUIDv7 — это 128-битный уникальный идентификатор, используемый для маркировки данных. Вы, вероятно, знакомы с UUIDv4 — стандартным выбором для генерации случайных идентификаторов. Поскольку UUIDv4 полностью случайны, вероятность дублирования крайне мала — это отлично. Однако у UUIDv4 есть недостаток: они не поддерживают естественную сортировку, что может замедлить работу, если использовать их в качестве индекса.

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

Структура UUIDv7

0190163d-8694-739b-aea5-966c26f8ad91
└─timestamp─┘ │└─┤ │└───rand_b─────┘
             ver │var
              rand_a
  Bit Size Description
Timestamp 48 Unix timestamp in milliseconds (sortable by generation time)
Version 4 Version (set to 7 for UUIDv7)
Random Part A 12 Random component (rand_a), for uniqueness within the same millisecond
Variant 2 Defines the UUID layout as RFC 4122 compliant
Random Part B 62 Additional random component (rand_b), providing further uniqueness

Структура UUIDv7

import os
import time
import uuid

def uuidv7() -> uuid.UUID:
    """
    Генерирует UUIDv7.
    """
    # Генерируем 16 случайных байт в качестве основы UUID
    value = bytearray(os.urandom(16))

    # Получаем текущую временную метку в миллисекундах с Unix-эпохи
    timestamp = int(time.time() * 1000)

    # Вставляем временную метку (48 бит) в UUID
    value[0] = (timestamp >> 40) & 0xFF
    value[1] = (timestamp >> 32) & 0xFF
    value[2] = (timestamp >> 24) & 0xFF
    value[3] = (timestamp >> 16) & 0xFF
    value[4] = (timestamp >> 8) & 0xFF
    value[5] = timestamp & 0xFF

    # Устанавливаем версию и вариант
    value[6] = (value[6] & 0x0F) | 0x70  # версия = 7
    value[8] = (value[8] & 0x3F) | 0x80  # вариант RFC 4122

    return uuid.UUID(bytes=bytes(value))

Использование UUIDv7 в Django

Чтобы использовать UUIDv7 в Django, можно создать собственное поле, расширяющее встроенный UUIDField, и генерировать UUIDv7 с помощью функции выше.

Шаг 1: Создайте собственное поле UUID

app/fields.py

import os
import time
import uuid
from django.core.exceptions import ValidationError
from django.db import models

def uuidv7() -> uuid.UUID:
    """
    Генерирует UUIDv7.
    """
    value = bytearray(os.urandom(16))
    timestamp = int(time.time() * 1000)

    value[0] = (timestamp >> 40) & 0xFF
    value[1] = (timestamp >> 32) & 0xFF
    value[2] = (timestamp >> 24) & 0xFF
    value[3] = (timestamp >> 16) & 0xFF
    value[4] = (timestamp >> 8) & 0xFF
    value[5] = timestamp & 0xFF

    value[6] = (value[6] & 0x0F) | 0x70
    value[8] = (value[8] & 0x3F) | 0x80

    return uuid.UUID(bytes=bytes(value))

class UUIDField(models.UUIDField):
    """
    Пользовательское поле UUID, поддерживающее разные версии UUID.
    """

    def __init__(self, primary_key: bool = True, version: int | None = None, editable: bool = False, *args, **kwargs):
        if version:
            if version == 2:
                raise ValidationError("UUID версии 2 не поддерживается.")
            if version < 1 or version > 7:
                raise ValidationError("Версия UUID должна быть от 1 до 7.")

            version_map = {
                1: uuid.uuid1,
                3: uuid.uuid3,
                4: uuid.uuid4,
                5: uuid.uuid5,
                7: uuidv7,
            }
            kwargs.setdefault("default", version_map[version])
        else:
            kwargs.setdefault("default", uuid.uuid4)

        kwargs.setdefault("editable", editable)
        kwargs.setdefault("primary_key", primary_key)

        super().__init__(*args, **kwargs)

Это пользовательское поле UUIDField принимает необязательный аргумент version. Если передать version=7, оно будет использовать функцию uuidv7; в противном случае по умолчанию используется uuid.uuid4. При необходимости можно настроить версию по умолчанию или расширить словарь для поддержки других версий.

Шаг 2: Используйте пользовательское поле в моделях

app/models.py

from django.db import models
from app.fields import UUIDField

class MyModel(models.Model):
    id = UUIDField(primary_key=True, version=7)
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

Всё готово! Теперь у вас есть модель Django с UUIDv7 в качестве первичного ключа. В зависимости от ваших задач, вы можете использовать его как индекс для ускорения поиска или сортировки по времени создания.

Тестирование UUIDv7 в оболочке Django

После выполнения миграций и создания нескольких экземпляров MyModel вы можете проверить сгенерированные UUID в оболочке Django:

>>> from app.models import MyModel
>>> ids = MyModel.objects.all().values_list('id', flat=True)
>>> for id in ids:
...     print(id)
...
UUID('01927d8c-1645-7ab9-86b6-d351a982a38f')
UUID('0192628e-ef00-7c4f-bdd6-811ff48f0e9b')
UUID('01927d8b-de00-7e2a-9cf7-b86dcb6d7c44')
UUID('01927db9-1b51-7844-94a7-349f372bdc00')
UUID('01927dee-f946-79dc-aac0-ec2ebbd740c4')
UUID('01927e08-c241-7b1b-bf0a-a63413abbdf2')
UUID('0192628a-de38-75f8-8b18-c43496533ad8')