Часто разработчики интересуются почему не рекомендуется использовать тип поля 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`.
Это крайне удобно и максимально практично.