Я перенес свои сообщения в собственный блог, потому что Medium становится все менее и менее удобным для читателей (платный доступ, невозможность выделить код и т. Д.). Чтобы прочитать эту статью в более приятном и дружественном контексте, прочтите ее в моем личном блоге и подписывайтесь на меня в Twitter, чтобы получать уведомления!

Https://titouangalopin.com/auto-increment-is-the-devil-using-uuids-in-symfony-and-doctrine/

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

В сущности Doctrine ORM это обычно выглядит так:

/**
 * @var int|null
 *
 * @ORM\Id
 * @ORM\GeneratedValue
 * @ORM\Column(type="integer", options={"unsigned": true})
 */
private $id;

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

Более того, используя автоматически увеличивающееся значение в ваших URL-адресах, вы даете возможность пользователям легко удалять весь ваш веб-сайт, написав простой скрипт для доступа к / users / 1, / users / 2, / users / 3,…

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

Вот где полезны UUID.

Использование UUID с Doctrine

UUID или универсально уникальные идентификаторы - это способ создания (почти) уникальных чисел, где бы они ни были сгенерированы, без необходимости в центральном органе для синхронизации уникальности (т. Е. без необходимости в базе данных знать количество строк перед генерацией значения).

UUID чрезвычайно полезны во многих контекстах и ​​по многим причинам, особенно потому, что существует 5 версий UUID для различных сценариев использования. Посетите https://en.wikipedia.org/wiki/Universally_unique_identifier, чтобы узнать о них больше.

В нашем контексте UUID - отличный способ избежать отображения автоматически увеличивающихся чисел в наших URL-адресах: вместо / users / 1 / tgalopin мы могли бы иметь URL-адреса типа / users / c11ed9b0-e060 –4aec-b513-e17c24df2c70 / tgalopin.

Чтобы использовать UUID с Doctrine, я рекомендую вам использовать пакет Ramsey UUID Doctrine: https://github.com/ramsey/uuid-doctrine. Этот пакет позволит вам настроить поля Doctrine как UUID, сохраняя их наилучшим образом в вашей базе данных. Более того, если вы используете Symfony Flex, вам даже не нужно ничего настраивать, поскольку рецепт сделает это за вас!

После установки вы сможете создавать такие поля:

/**
 * The internal primary identity key.
 *
 * @var UuidInterface|null
 *
 * @ORM\Column(type="uuid", unique=true)
 */
protected $uuid;

И заполните это поле различными версиями UUID, например версией 4 (случайный UUID):

$this->uuid = Uuid::uuid4();

Проблемы UUID и способы их решения

Хотя UUID - отличный способ получить уникальные идентификаторы, которые сложно удалить, при их использовании по-прежнему возникают две основные проблемы:

  • потенциальная потеря производительности при использовании UUID в качестве первичных ключей
  • отсутствие читабельности полученных URL-адресов

Производительность первичных ключей UUID

Если вы используете UUID в качестве первичных ключей и если ваше хранилище базы данных не может обрабатывать их должным образом (вы должны использовать PostgreSQL;)), вы получите строки в качестве первичных ключей. Наличие строк в фильтрах WHERE, индексах и запросах соединения - большая проблема производительности из-за размера и сложности структуры данных.

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

Чтобы использовать этот шаблон в Doctrine, я создаю следующую черту в большинстве своих приложений:

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\UuidInterface;

trait EntityIdTrait
{
    /**
     * The unique auto incremented primary key.
     *
     * @var int|null
     *
     * @ORM\Id
     * @ORM\Column(type="integer", options={"unsigned": true})
     * @ORM\GeneratedValue
     */
    protected $id;

    /**
     * The internal primary identity key.
     *
     * @var UuidInterface
     *
     * @ORM\Column(type="uuid", unique=true)
     */
    protected $uuid;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getUuid(): UuidInterface
    {
        return $this->uuid;
    }
}
// In another entity:
class User
{
    use EntityIdTrait;
    // ...
}

Читаемость URL-адресов

При использовании UUID в URL-адресах большая часть URL-адреса больше не читается пользователем. Хотя это не является серьезным недостатком, наличие URL-адреса типа / user / 1 / tgalopin определенно намного лучше, чем наличие / user / c11ed9b0-e060–4aec-b513-e17c24df2c70 / tgalopin для пользователей вашего приложения.

Чтобы улучшить это, есть несколько способов:

  • мы могли бы попытаться найти меньшую структуру данных, чем UUID (но поддержка UUID действительно велика среди многих языков программирования)
  • мы могли бы использовать только часть UUID (но мы рискуем столкнуться с множеством конфликтов)
  • или мы можем закодировать UUID в формате, более подходящем для URL-адресов

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

Формат, о котором вы, возможно, подумали, читая предыдущий абзац, - это base64. Это отличный формат для представления данных в более компактном виде, чем шестнадцатеричный, но мне не понравилась возможность иметь =, + и / в моих идентификаторах: похоже, это не соответствовало потребности в удобочитаемости URL.

Вот почему я посмотрел на base32: base32 имеет меньше символов и, следовательно, немного длиннее, чем base64, но он гораздо больше подходит для URL, поскольку он состоит только из буквенно-цифровых символов.

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

UuidEncoder, который использует расширение GMP для кодирования и декодирования UUID:

<?php

namespace App\Doctrine;

use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;

class UuidEncoder
{
    public function encode(UuidInterface $uuid): string
    {
        return gmp_strval(
            gmp_init(
                str_replace('-', '', $uuid->toString()),
                16
            ),
            62
        );
    }

    public function decode(string $encoded): ?UuidInterface
    {
        try {
            return Uuid::fromString(array_reduce(
                [20, 16, 12, 8],
                function ($uuid, $offset) {
                    return substr_replace($uuid, '-', $offset, 0);
                },
                str_pad(
                    gmp_strval(
                        gmp_init($encoded, 62),
                        16
                    ), 
                    32, 
                    '0', 
                    STR_PAD_LEFT
                )
            ));
        } catch (\Throwable $e) {
            return null;
        }
    }
}

Расширение Twig для создания ссылок:

<?php

namespace App\Twig;

use App\Doctrine\UuidEncoder;
use Ramsey\Uuid\UuidInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

class UuidExtension extends AbstractExtension
{
    private $encoder;

    public function __construct(UuidEncoder $encoder)
    {
        $this->encoder = $encoder;
    }

    public function getFunctions(): array
    {
        return [
            new TwigFunction(
                'uuid_encode',
                [$this, 'encodeUuid'], 
                ['is_safe' => ['html']]
            ),
        ];
    }

    public function encodeUuid(UuidInterface $uuid): string
    {
        return $this->encoder->encode($uuid);
    }
}

Признак репозитория, позволяющий легко найти объект по закодированному UUID (обратите внимание, что свойство должно быть заполнено репозиторием с помощью признака):

<?php

namespace App\Repository;

use App\Doctrine\UuidEncoder;

trait RepositoryUuidFinderTrait
{
    /**
     * @var UuidEncoder
     */
    protected $uuidEncoder;

    public function findOneByEncodedUuid(string $encodedUuid)
    {
        return $this->findOneBy([
            'uuid' => $this->uuidEncoder->decode($encodedUuid)
        ]);
    }
}

Этот набор инструментов позволяет мне получать URL-адреса, подобные этому:

/ users / 3xv5LDIdusDxM77x0MW8bI / tgalopin

Лучший из двух миров :) !