Как обеспечивается уникальность данных?
Что проверяют
Понимаете ли вы, что уникальность гарантирует только база, а проверка в коде нужна для удобства пользователя. Главный уточняющий вопрос: что будет, если два запроса с одинаковым email придут одновременно.
Как отвечать
Идите по уровням, начиная с того, который реально держит инвариант:
- ограничения в базе: первичный ключ и
UNIQUEconstraint или уникальный индекс. Они работают при любом числе потоков, инстансов и путей записи в таблицу; - идентификаторы: sequence (
bigserial,IDENTITY) илиUUID.UUID.randomUUID()даёт случайный v4, а для первичного ключа часто выбирают упорядоченный по времени v7, чтобы вставки не разбрасывались по всему B-tree индексу; - обработка конфликта: в Spring нарушение ограничения приходит как
DataIntegrityViolationException, его ловят и превращают в понятный ответ 409. В PostgreSQL для идемпотентной вставки естьINSERT ... ON CONFLICT DO NOTHINGилиDO UPDATE; - нормализация: email сравнивают без учёта регистра, поэтому индекс строят по
lower(email)или берут типcitext; - частичный индекс: при мягком удалении уникальность нужна только среди живых строк:
CREATE UNIQUE INDEX ... WHERE deleted_at IS NULL; - в памяти: в коллекциях уникальность держит
HashSet, и работает она, только если у класса согласованыequalsиhashCode.
Проверку existsByEmail перед save оставляют ради понятного сообщения, но не как защиту.
Где спотыкаются
Думают, что @Transactional спасает от гонки «проверил, потом вставил». На уровне READ COMMITTED обе транзакции увидят, что записи нет, и обе вставят. Спасает только ограничение в базе.
Полагаются на @Column(unique = true). Аннотация влияет лишь на генерацию DDL, и если схему ведут миграциями Flyway или Liquibase, ограничения в базе может не оказаться вовсе.
Забывают про NULL: обычный уникальный индекс в PostgreSQL пропускает несколько строк с NULL. Если так не нужно, помогает NOT NULL или NULLS NOT DISTINCT (с PostgreSQL 15).