Pull to refresh

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

Level of difficultyEasy
Reading time4 min
Views35K

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

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

Only registered users can participate in poll. Log in, please.
Какой тип колонок Вы используете чаще всего?
18.36% enum101
49.09% integer270
32.55% string179
550 users voted. 106 users abstained.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 33: ↑14 and ↓190
Comments178

Articles