В таблице пользователей удалённые записи сохраняются для аудита. Как обеспечить уникальность логина только для активных записей на уровне хранилища?
Нужно задать условное уникальное ограничение: уникальность логина должна проверяться только для строк, соответствующих условию активной записи. Такая проверка должна выполняться самим хранилищем атомарно, чтобы параллельные операции не создали двух активных пользователей с одним логином.
Обычное уникальное ограничение распространяется на все строки таблицы. Оно хорошо подходит, когда значение должно быть уникальным на протяжении всей жизни записи, но не учитывает бизнес-условия вроде активности, принадлежности к группе или текущего статуса.
Потребность в условной уникальности возникает, когда данные нельзя физически удалять из-за аудита, восстановления или юридических требований. При этом старые записи не должны блокировать повторное использование логина.
Если установить обычное уникальное ограничение на логин, удалённая запись продолжит занимать это значение. Новый пользователь не сможет использовать логин, хотя активного пользователя с таким логином уже нет.
Если убрать ограничение и проверять уникальность в приложении отдельным чтением, возникает состояние гонки: два параллельных запроса могут одновременно убедиться, что логин свободен, а затем оба создать активную запись. В результате инвариант будет нарушен.
Основной вариант — частичный уникальный индекс или эквивалентное условное уникальное ограничение, включающее только активные строки. При создании или активации пользователя хранилище проверяет конфликт лишь среди записей с активным статусом.
Удалённая запись остаётся в таблице, но не участвует в проверке уникальности. Если её восстанавливают, операция должна снова пройти условную проверку; при занятом логине восстановление должно завершиться конфликтом, а не молча нарушить правило.
Ключевое свойство решения — атомарность проверки и записи внутри хранилища. Приложение всё равно должно корректно преобразовывать конфликт ограничения в понятную бизнес-ошибку и учитывать возможную конкуренцию при смене статуса.
Альтернативой может быть отдельная таблица активных пользователей с обычным уникальным ограничением. Это делает правило явным, но усложняет транзакции, синхронизацию с архивом и восстановление записей. Ещё один вариант — уникальность составного значения с техническим маркером активности, однако он зависит от точной семантики NULL, индексов и ограничений конкретного хранилища, поэтому менее прозрачен.
Проверка только на уровне приложения не подходит как единственная защита. Кэш также не может гарантировать уникальность: его данные могут устареть, а несколько экземпляров сервиса могут принимать решения независимо.
Сервис управления учётными записями хранит удалённых пользователей для аудита. Сначала команда выполняла поиск активного пользователя с нужным логином, затем вставляла новую запись. При параллельной регистрации одного логина оба запроса иногда проходили проверку.
Рассматривались три варианта. Синхронизация через распределённую блокировку добавляла операционную сложность и требовала надёжно обрабатывать сбои блокировщика. Отдельная таблица активных пользователей обеспечивала ясную модель, но усложняла транзакции между активными и архивными данными. Условное уникальное ограничение сохранило текущую модель и передало контроль за инвариантом базе данных.
Был выбран третий вариант. После этого конкурентные операции стали сериализоваться самим хранилищем: одна из них успешно занимала логин, а другая получала конфликт ограничения и могла безопасно сообщить клиенту, что значение уже занято.
1. Что произойдёт при одновременной деактивации старого пользователя и создании нового с тем же логином?
Обе операции должны иметь явно определённую транзакционную семантику. Если деактивация действительно фиксируется до проверки вставки, новый пользователь может занять логин. При конкурирующих транзакциях итог зависит от изоляции и реализации ограничений, поэтому приложение должно обрабатывать конфликт и повторять операцию только там, где это безопасно.
2. Почему нельзя считать удалённые записи просто строками с пустым логином?
Это смешивает отсутствие бизнес-значения с техническим обходом ограничения. Поведение уникальности для NULL или пустых строк различается по типам хранилищ, а пустой логин может оказаться допустимым реальным значением. Явное условие по статусу лучше выражает правило и снижает риск ошибок при восстановлении или миграции данных.
3. Как обеспечить уникальность, если логин не чувствителен к регистру?
Сначала нужно формализовать нормализацию: например, считать значения с разным регистром одним логином. Затем ограничение должно применяться к нормализованному представлению, а не к исходной строке. Важно учитывать правила Unicode и локали; простое приведение ASCII-строк к нижнему регистру не всегда корректно для всех поддерживаемых языков.