Почему тип поля enum на уровне базы — зло

от автора

Часто разработчики интересуются почему не рекомендуется использовать тип поля enum в базе данных, и в этой статье мы рассмотрим все плюсы и минусы данного типа.

Тип колонки enum используется для хранения данных, которые могут принимать определённые значения из заранее определённого набора. Он обеспечивает ограничение значений, которые может принимать колонка, и позволяет более строго контролировать данные. Это может быть полезно для хранения статусов, категорий, типов или любых других значений, которые могут быть заданы только из ограниченного набора вариантов.

Но это в теории. А что на практике? Давайте рассмотрим.

Допустим у нас есть таблица со списком платежей, содержащая колонку status со следующими значениями:

CREATE TABLE `payments`(     `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,   `status` ENUM('new', 'progress', 'done', 'fauled') NOT NULL,    KEY(`id`) ) CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Через какое-то время была замечена грамматическая ошибка в слове «failed» и принято решение её исправить.

Нюанс изменения enum поля в том, что при его редактировании сбрасываются значения колонки в null для всех строк таблицы, а то и вовсе получим ошибку Data truncated for column 'status' at row 3. То есть, чтобы корректно изменить enum поле, нужно куда-то сохранить данные. План действий будет таков:

  1. Создать новую enum колонку с правильным набором данных;

  2. Скопировать значение из старой колонки в новую и сразу применить исправление значения;

  3. Удалить старую enum колонку;

  4. Переименовать новую enum колонку.

При использовании фреймворка Laravel это будет выглядеть следующим образом:

use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema;  return new class extends Migration {     public function up(): void     {         // Создаём новую колонку         Schema::table('payments', function (Blueprint $table) {             $table->enum('tmp_status', ['new', 'progress', 'done', 'failed']);         });          // Копируем значения из enum колонки в новую с корректировкой значения         DB::statement('UPDATE payments SET tmp_status = (IF status = \'fauled\' THEN \'failed\' ELSE status END IF)');          // Удаляем старую колонку         Schema::table('payments', function (Blueprint $table) {             $table->dropColumn('status');         });          // Переименовываем колонку         Schema::table('payments', function (Blueprint $table) {             $table->renameColumn('tmp_status', 'status');         });     } };

На языке SQL эти действия будут выглядеть следующим образом:

ALTER TABLE `payments`   ADD COLUMN `tmp_status` ENUM('new', 'progress', 'done', 'failed') NOT NULL AFTER `status`;  UPDATE `payments` SET `tmp_status` = (     IF `status` = 'fauled'     THEN 'failed'     ELSE `status` END IF );  ALTER TABLE `payments`   DROP COLUMN `status`;  ALTER TABLE `payments`   CHANGE `tmp_status` `status` ENUM('new', 'progress', 'done', 'failed') CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci NULL;

При использовании фреймворка Laravel нельзя просто взять и применить метод «change» на поле типа enum.Это вызовет ошибку:

Unknown column type «enum» requested. Any Doctrine type that you use has to be registered with \Doctrine\DBAL\Types\Type::addType()…

Для решения этой проблемы приходится применять следующий костыль перед применением изменений:

protected function fixEnum(): void {     $platform = Schema::getConnection()->getDoctrineSchemaManager()->getDatabasePlatform();     $platform->registerDoctrineTypeMapping('enum', 'string'); }

И, кроме этого, всё равно нужно будет на стороне приложения обрабатывать значение либо как константу класса, либо как enum класс. Например:

class Payment extends Model {     protected $casts = [         'status' => StatusEnum::class,     ]; }  enum StatusEnum: string {     case New      = 'new';     case Progress = 'progress';     case Done     = 'done';     case Failed   = 'failed'; }

А теперь представьте, что как только эта задача выполнена, пришла новая — добавить новый статус — Refund

Альтернатива этой боли — отказ от полей БД типа enum в пользу integer. В этом случае как при изменении названия, так и при добавлении нового всё что нужно будет сделать — это изменить содержимое самого enum класса. Например:

// До enum StatusEnum: int {     case New      = 0;     case Progress = 1;     case Done     = 2;     case Fauled   = 3; }  // После enum StatusEnum: int {     case New      = 0;     case Progress = 1;     case Done     = 2;     case Failed   = 3;     case Refund   = 4; }

И всё. Никакой боли и задача закрывается буквально за несколько секунд.

Именно поэтому многие разработчики отказываются от использования колонок типа enum в базе данных сохраняя не только время, потраченное на задачу, но и нервы и, в качестве приятного бонуса, шанс потерять данные сводится к нулю.

Также можно ответить на вопрос «почему integer, а не string»: дело в том, что поля типа string подвержены грамматическим ошибкам и всё что они дают — это удобство чтения таких данных в одной конкретной таблице без использования SQL запросов с применением JOIN функций. Но если в слове будет допущена ошибка или слово нужно будет заменить на другое — придётся отправлять запрос в базу, чего не нужно делать при использовании целочисленных значений.

Таким образом, тип полей enum в базе является иррациональным и бессмысленным действием за исключением крайне редких и специфичных кейсов.

Заключение

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

В том же Laravel правило валидации реализуется очень просто:

public function rules(): array {     return [         'status' => [Rule::enum(StatusEnum::class)],     ]; }

И на выходе, в случае обнаружения проблем, получим корректный код четырёхсотой серии с расшифровкой вместо `500 Whoops! Something wrong`.

Это крайне удобно и максимально практично.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Какой тип колонок Вы используете чаще всего?

18.22% enum98
48.7% integer262
33.09% string178

Проголосовали 538 пользователей. Воздержались 105 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/757212/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *