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

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

В директории 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
Василий Наумкин
15.09.2023 08:27
Никакой разницы, только в количестве строк. Не знаю, почему я так написал - не задумывался -)
bezumkin
Василий Наумкин
14.09.2023 23:59
Очень рад, что тебе понравилось!
NightRider
12.09.2023 12:37
Понял. Спасибо за подробный ответ!
Сергей Лелеко
10.09.2023 06:23
Понял! да , сталкивался с этой проблемкой
Дмитрий П.
07.09.2023 11:19
ну да, что-то на этот момент я не обратил внимание) видимо дублирование в девтулсах происходит из-за...
NightRider
29.08.2023 10:30
Просто так совпало что у меня пока что есть свободное время на изучение. Остальные скорее всего побо...
bezumkin
Василий Наумкин
28.08.2023 01:02
Абсоюлютно верно, у меня даже IDE подсвечивает ошибку - не знаю, как пропустил. Поменял на $me-&gt;g...
bezumkin
Василий Наумкин
25.08.2023 10:24
Очень рад!
inetlover
Александр Наумов
24.08.2023 10:49
Спасибо!!
bezumkin
Василий Наумкин
21.08.2023 12:37
Сорян, что-то я уже второй раз затупил с этим дампом. Держи правильный ms2.sql.zip (2.79 Mb), в заме...