Я перенес свои сообщения в собственный блог, потому что 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 em. > для пользователей вашего приложения.
Чтобы улучшить это, есть несколько способов:
- мы могли бы попытаться найти меньшую структуру данных, чем 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
Лучший из двух миров :) !