Часто разработчики интересуются почему не рекомендуется использовать тип поля 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 поле, нужно куда-то сохранить данные. План действий будет таков:
Создать новую
enumколонку с правильным набором данных;Скопировать значение из старой колонки в новую и сразу применить исправление значения;
Удалить старую
enumколонку;Переименовать новую
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`.
Это крайне удобно и максимально практично.