Разбираемся с миграциями

На прошлом уроке мы дошли до запуска проекта со стандартными таблицами и данными, теперь давайте разберёмся, откуда они взялись.

В директории core/db есть 2 поддиректории: migrations и seeds. В первой, понятно, миграции, а во второй скрипты для "засеивания" начальных данных. Для работы используется Phinx, конфигурация которого хранится в core/phinx.php - там нам ничего менять не нужно, все настройки берутся из настроек окружения .env.

Миграциями называют изменение структуры базы данных без потери её консистентности. Каждая миграция может быть отменена, при этом она возвращает структуру БД к предыдущему виду. На продакшене, конечно же, никто миграции не откатывает - это нужно только на время разработки.

Для запуска миграций в Vesp мы используем команду composer db:migrate, для отката composer db:rollback.

Теперь давайте посмотрим на сами файлы миграций.

Название файла состоит из отметки времени и произвольного имени, которое будет использоваться для работы. Временной отпечаток сортирует миграции в порядке их создания, чтобы они всегда выполнялись последовательно.

Миграция Files

Vesp уже содержит 2 миграции: таблицы файлов и пользователей.

<?php

use Illuminate\Database\Schema\Blueprint;
use Vesp\Services\Migration;

// Класс должен расширять Vesp\Services\Migration
class Files extends Migration
{
    // Функция внесения изменений
    public function up(): void
    {
        // Создаём таблицу для файлов
        $this->schema->create(
            'files',
            function (Blueprint $table) {
                $table->id(); // Первичный ключ Unsigned Big Integer с автоинкрементом
                // Дальше колонки Varchar  
                $table->string('file'); 
                $table->string('path');
                // Следующие 2 колонки могут быть null при создании записи
                // То есть, они не являются обязательными
                $table->string('title')->nullable();
                $table->string('type')->nullable();
                $table->smallInteger('width')->unsigned()->nullable();
                $table->smallInteger('height')->unsigned()->nullable();
                $table->integer('size')->unsigned()->nullable();
                // колонка с типом JSON появилась только в MySQL 5.7
                $table->json('metadata')->nullable(); 
                // Специальная команда Eloquent, которая добавляет 2 колонки:
                // created_at и updated_at для сохранения времени создания и 
                // последнего изменения записи
                $table->timestamps();
            }
        );
    }

    // Функция отката изменений
    public function down(): void
    {
        // Просто удаляем таблицу
        $this->schema->drop('files');
    }
}

Вам не нужно запоминать типы колонок, здесь и далее везде прекрасно работает автодополнение IDE.

Но крайне желательно знать типы данных MySQL, все они перечислены в документации.

Итак, при запуске composer db:migrate Phinx смотрит на имеющиес миграции и выполняет новые. Как он знает, какие миграции уже были выполнены? Очень просто - он сохраняет их имена в таблице app_migrations.

Миграция Users

Теперь давайте посмотрим на вторую миграцию, она создаёт сразу 3 таблицы, и связывает их друг с другом.

<?php

use Illuminate\Database\Schema\Blueprint;
use Vesp\Services\Migration;

class Users extends Migration
{

    public function up(): void
    {
        // Первым делом создаём таблицу для хранения групп пользователей
        $this->schema->create(
            'user_roles',
            function (Blueprint $table) {
                // Первичный ключ
                $table->id();
                // Название группы должно быть уникальным
                $table->string('title')->unique();
                // Права доступа группы могут быть null
                $table->json('scope')->nullable();
                // Стандартные колонки с датами
                $table->timestamps();
            }
        );

        // Теперь таблица пользователей
        $this->schema->create(
            'users',
            function (Blueprint $table) {
                $table->id();
                // Уникальное имя пользователя
                $table->string('username')->unique();
                // Обязательный пароль (мы его захэшируем, конечно)
                $table->string('password');
                // Следующие 2 колонки не обязательны
                $table->string('fullname')->nullable();
                $table->string('email')->nullable();
                // Связь с таблицей группы пользователей (расскажу ниже)
                $table->foreignId('role_id')
                    ->constrained('user_roles')->onDelete('cascade');
                // Статус записи индексируется отдельно
                $table->boolean('active')->default(true)->index();
                $table->timestamps();
            }
        );

        // Таблица "сессий" пользователя
        $this->schema->create(
            'user_tokens',
            function (Blueprint $table) {
                // Здесь первичным ключом является строчный токен
                $table->string('token')->primary();
                // Свзяь с таблицей пользователей
                $table->foreignId('user_id')
                    ->constrained('users')->onDelete('cascade');
                // Срок годности токена
                $table->timestamp('valid_till')->index();
                // Ip адрес пользователя
                $table->string('ip', 16)->nullable();
                // Статус записи
                $table->boolean('active')->default(true);
                $table->timestamps();

                // Комбинированный индекс для быстрого поиска 
                // активных токенов пользователя
                $table->index(['token', 'user_id', 'active']);
            }
        );
    }

    // Функция отката изменений
    public function down(): void
    {
        // Просто удаляем все 3 таблицы в обратном порядке
        $this->schema->drop('user_tokens');
        $this->schema->drop('users');
        $this->schema->drop('user_roles');
    }
}

В этой миграции мы впервые видим связь таблиц между собой. Разберём на примере.

В таблице user_roles объявлен первичный ключ через $table->id() - на уровне MySQL это колонка id:

  • тип колонки BIGINT
  • атрибутом UNSIGNED, то есть значение не может быть меньше 0 и это даёт возможность сохранять больше положительных значений - аж до 2*64 степени - 1
  • ключ PRIMARY, то есть первичный уникальный
  • и еще она AUTO_INCREMENT, то есть значения увеличиваются автоматически на 1 в каждой новой записи

Такой вид колонки id - самый распространённый для идентификации конкретной записи. В дальнейшем по ней можно искать записи, обновлять и удалять.

Логично, что если мы хотим связать какую-то другу таблицу с этой - мы будем привязывать её именно к id. Eloquent предлагает для этого очень удобный синтаксис:

$table->foreignId('role_id')
    ->constrained('user_roles')
    ->onUpdate('restrict')
    ->onDelete('cascade');

foreignId создаёт колонку role_id (имя произвольное, но Eloquent очень просит, чтобы заканчивалось на _id) ровно того же типа UNSIGNED BIGINT, а constrained указывает на связь с таблицей user_roles. Эта запись буквально говорит, что role_id обязательно содержит id из таблицы user_roles.

PhpMyAdmin даже делает значения таких колонок ссылками со всплывающими подсказками о родительском значении:

Теперь вы просто не сможете сохранить в БД пользователя с неправильной группой - MySQL выдаст ошибку.

Командами onUpdate и onDelete мы указываем, что делать с юзерами при изменении родительской записи из группы: restrict означает не делать ничего, а cascade - делать тоже самое. В нашем случае указано, что при удалении записи из user_roles, будут удалены все пользователи этой группы. Автоматически, на уровне БД.

Таким образом, связав эти 2 таблицы, мы точно знаем, что пользователей без группы быть не может - MySQL не позволит сохранить такую запись, а при удалении группы удалит всех её пользователей.

Более подробно про связи можно почитать документацию MySQL.

Сиды

Seed - это семя или семечко по-английски. В терминологии миграций это скрипты, которые вносят какие-то данные в таблицы, чтобы не делать этого вручную. Запускаются командой composer db:seed.

В нашем случае сиды создают начальные группы и пользователей. Для этого выполняются 2 скрипта: core/db/seeds/UserRoles.php и core/db/seeds/Users.php.

Вот создание групп пользователей:

<?php

use Phinx\Seed\AbstractSeed;
use App\Models\UserRole;

class UserRoles extends AbstractSeed
{
    public function run(): void
    {
        // Просто массив с группами
        $roles = [
            'Administrator' => [
                // И их правами
                'scope' => ['profile', 'users'],
            ],
            'User' => [
                'scope' => ['profile'],
            ],
        ];

        // Дальше проходим по массиву
        foreach ($roles as $title => $data) {
            // Если группы с таким нзавнием нет
            if (!$group = UserRole::query()->where('title', $title)->first()) {
                // То создаём
                $group = new UserRole(['title' => $title]);
            }
            // И обновляем ей свойства
            $group->fill($data);
            $group->save();
        }
    }
}

Сиды используют модели Eloquent, которые мы будем рассматривать на следующем уроке. Но, как вы понимаете, в этих скриптах можно делать что угодно с данными, не только добавлять, но и удалять или менять.

Создание стандартных пользователей отличается только наличием еще одного метода:

public function getDependencies(): array
{
    return ['UserRoles'];
}

Это означает, что пользователи должны создаваться после создания их групп. Иначе мы бы получили ошибку от MySQL, помните?

Ну вот, а так мы указываем, что скрипт Users требует обязательного выполнения UserRoles перед ним.

Заключение

Мы рассмотрели базовые миграции и сиды Vesp. Нужно учесть, что они не выполняются сразу после создания пакета, вам нужно запускать их вручную.

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

Более того, структура таблиц не может быть неизменнной по ходу разработки проекта, так что мы напишем и запустим еще несколько миграций.

На следующем уроке будет изучать модели Eloquent для работы с нашими таблицами.

← Предыдущая заметка
Создание нового проекта
Комментарии (9)
bezumkinВасилий Наумкин
09.06.2022 14:33

Eloquent вообще толковый, после него xPDO невозможно использовать.

bezumkinВасилий Наумкин
09.06.2022 16:38

Да, верно, в зависимостях у vesp-core прописан Eloquent 8.

bezumkinВасилий Наумкин
20.06.2022 12:30

Не надо, оно по умолчанию так - я просто чуть более подробно написал.

bezumkinВасилий Наумкин
01.07.2023 08:55

Да, верно.

Если пройти по цепочке методов Illuminate\Database\Schema::Blueprint, начиная с id(), то увидишь вот это:

    public function id($column = 'id')
    {
        return $this->bigIncrements($column);
    }

    public function bigIncrements($column)
    {
        return $this->unsignedBigInteger($column, true);
    }

    public function unsignedBigInteger($column, $autoIncrement = false)
    {
        return $this->bigInteger($column, $autoIncrement, true);
    }

    public function bigInteger($column, $autoIncrement = false, $unsigned = false)
    {
        return $this->addColumn('bigInteger', $column, compact('autoIncrement', 'unsigned'));
    }

Последний метод и добавляет колонку в схему с нужными параметрами.

bezumkin
Василий Наумкин
09.04.2024 01:45
Ошибка 500 Это не похоже на ошибку Nginx, это скорее всего ошибка PHP - надо смотреть его логи. Во...
futuris
Futuris
04.04.2024 05:56
Я просто немного запутался. Когда в абзаце &quot;Vesp/Core&quot; ты пишешь про &quot;новый trait Fil...
bezumkin
Василий Наумкин
20.03.2024 18:21
Volledig!
Андрей
14.03.2024 10:47
Василий! Как всегда очень круто! Моё почтение!
russelgal
russel gal
09.03.2024 17:17
А этот стоило написать хотя бы затем, чтобы получить комментарий от юзера, который ничего не писал ...
inetlover
Александр Наумов
27.01.2024 00:06
Василий, спасибо! Извини, тупанул.
bezumkin
Василий Наумкин
22.01.2024 04:43
Давай-давай!
bezumkin
Василий Наумкин
24.12.2023 11:26
Спасибо!
bezumkin
Василий Наумкин
27.11.2023 02:43
Ура!
bezumkin
Василий Наумкин
25.11.2023 08:30
Vesp тянет 2 зависимости: vesp-frontent для фронта и vesp-core для бэкенда. Их можно обновлять, но э...