Verification: a143cc29221c9be0

Php artisan db seed не работает

Disclaimer

Код представленный ниже это обусифицированный боевой код, я его не отлаживал, может потребоваться доработка напильником. Я делюсь с вами только идеями.

Описание проблемы

Есть у нас ветка драфт, что бы пушить в драфт не надо делать МР, эта ветка нужна для того что бы программист мог по быстрому показать свои наработки бизнес аналитику, что бы фронт-энд мог интегрироваться с изменениями на бэк-энде. Драфт регулярно ресетиться.

То есть ты что то сделал с функционалом, решил показать другим разработчикам - выкатываешь всё на драфт, на БД драфта раскатывается твоя миграция, ты показал, понял что был не прав и переделываешь миграцию, в это время кто то ресетит драфт вместе с твоей миграцией, и что мы имеем ? мы имеем миграцию которая были применена и которую ни кто не откатил, как думаете получиться ещё раз её применить ?

Другая проблема в том что миграции локально применяются по мере разработки кода, а накатываются на БД, по мере тестирования кода. Фича которую ты разработал может на пару недель застрять в тестировании, а фича которую ты делал на этой неделе уже будет раскатана сегодня, и когда завтра из тестирования выйдет первая фича и будет накатываться на прод, могут возникнуть проблемы. Это не смертельно, но очень неприятно.

То есть порядок применения миграций иногда бывает хаотичным.

Правило первое: "накатывать можно бесконечно"

Правило первое, пишем миграцию так что бы её можно было бесконечное количество раз накатывать на саму себя. То есть накатываем миграцию, удаляем запись этой миграции в таблице migrations, и если мы ещё раз накатим эту же миграцию, то не должно быть эксепшена, миграция не должна вызвать сбой.

Миграцию можно как бесконечное количество раз накатить на саму себя, так и откатить с самой себя.

Для работы миграциями по штучно я использую такое команды:

# накатить конкретную миграцию
php artisan migrate --path="services/best-team-servise/database/migrations/2021_02_04_240000_alter_data_model_table_add_unique_index.php" --pretend
# ключ --pretend позволит нам посмотреть SQL до того как будет применена миграция, иногда полезно

# откатить ровно одну миграцию
php artisan migrate:rollback --step=1
# можно откатить и десять, при случае раберётесь

После того как миграция применена, можно сгенерировать описание модели

php artisan ide-helper:models "Project\Models\DataModel"

и сделать посев данных:

php artisan db:seed --class=DataModelSeeder

Как сделать так что бы миграцию можно было бесконечно применять ? в методах up() и down() миграции, мы должны делать проверку того, что действие со схемой БД можно выполнить.

Если мы добавляем колонку, то проверяем что колонка не существует, если мы дропаем индекс, то проверяем что индекс существует.

Получаем Builder :

        $conn = (new DataModel())->connection;
        $builder = Schema::connection($conn);

проверяем что миграция не была применена (проверяем что миграция может быть выполнена):

        $isExists = $builder->hasColumn(
            'data_model',
            'deleted_at'
        );

Если миграция может быть выполнена, то выполняем:

        if (!$isExists) {
            $builder->table(
                'data_model',
                function (Blueprint $table) {
                    $table->softDeletesTz();
                }
            );
        }

Аналогично с таблицами - проверяем что таблица не существует, с индексами - проверяем что индекс не существует, с индексами посложней, но можно, поможет такой код:

        $alias = (new DataModel())->connection;
        $builder = Schema
            ::connection($alias)
            ->getConnection()
            ->getDoctrineSchemaManager();

$existingIndexes = $builder->listTableIndexes('data_model');

С индексами в Laravel есть заморочка, если мы создали индекс как:

Blueprint::unique('index_name');

То и удалять надо как:

Blueprint::dropUnique('index_name');

С таблицами и колонками у Laravel ок, с индексами похуже, с триггерами совсем плохо, приходиться писать на чистом SQL, наверное я чего то не знаю о Laravel ? Адепты, подскажите !

Накатываем чистым SQL, пишем так:

DROP TRIGGER IF EXISTS trigger_name
    ON public.data_model;
CREATE TRIGGER trigger_name
    BEFORE INSERT
    ON public.data_model
    FOR EACH ROW
    EXECUTE PROCEDURE public.function_name();

Когда откатываем, пишем так же:

DROP TRIGGER IF EXISTS trigger_name
    ON public.data_model;

Правило второе: "шаблонные имена"

Миграций в проекте сотни, за полгода было написано 1000+ миграций. Конечно вся 1000 не лежит в одной директории, они раскиданы по директориям модулей.

Но тем нем нее, когда открываешь директорию в которой 50+ миграций, тебе сложно ориентироваться в них без "хороших" имён.

Соглашения об именах

Если создаём таблицу, то имя миграции начинается с create, если меняем таблицу то alter, если удаляем таблицу, то drop.

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

alter_data_model_add_property_column
alter_data_model_alter_property_column_to_text
alter_data_model_alter_property_column_set_default_value
alter_data_model_create_index_on_code_type_columns
alter_data_model_create_unique_index_on_code_column

Дропать таблицы, конечно, можно только на этапе MVP.

Команды для создания миграций:

# делаем миграцию для создания таблицы
php artisan make:migration create_profile_table --create=profile

# делаем миграцию для изменения таблицы
php artisan make:migration add_confirmed_to_profile --table=profile

Файл миграции будет помещён в директорию database/migrations собственно приложения, у нас каждый сервис это отдельный пакет и после создания файла миграции его надо переложить в директорию своего пакета.

Правило третье: таблицы и колонки дропать нельзя, все колонки nullable()

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

Откатывать миграции вместе с репозиторием, эта так себе занятие.

Поэтому все колонки должны быть только nullable(), проверку того что обязательные колонки имеют значение, надо делать на уровне кода бизнес логики или с помощью обсерверов.

Соответственно при посеве данных, что бы не подменять файлы данных для посева, не прописывать какие конкретно колонки мы пишем - мы должны делать запись только в те колонки которые существуют:

            $columns = Schema
            ::connection((new DataModel())->connection)
            ->getConnection()
            ->getDoctrineSchemaManager()
            ->listTableColumns($(new DataModel())->getTable());

            $data = [];
            foreach ($columns as $column) {
                $name = $column->getName();
/* @var array[] $record строка данных для посева*/
                $exists = key_exists($name, $record);
                if ($exists) {
                    $data[$name] = $record[$name];
                }
            }

            $isSuccess = DataModel
                ::withTrashed()
                ->updateOrCreate(
                    ['uniqe_index_column' => $data['uniqe_index_column'],],
                    $data
                )->exists;

Правило четвёртое: значения по умолчанию, там где null недопустим

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

Либо можно в коде использовать какое то значение по умолчанию, но я такой способ не люблю, потому что это хард код, и это убивает гибкость нашего приложения. Работа приложения должна настраиваться или через переменные окружения, или файлы конфигов, или записи БД.

Введение

Laravel предлагает возможность наполнения базы данных тестовыми данными с использованием классов-наполнителей. Все классы наполнителей хранятся в каталоге database/seeders. Класс DatabaseSeeder уже определен по умолчанию. В этом классе вы можете использовать метод call для запуска других наполнителей, что позволит вам контролировать порядок наполнения БД.

При наполнении базы данных автоматически отключается защита массового присвоения.

Написание наполнителей

Чтобы сгенерировать новый наполнитель, используйте команду make:seeder Artisan. Эта команда поместит новый класс наполнителя в каталог database/seeders вашего приложения:

php artisan make:seeder UserSeeder

Класс наполнителя по умолчанию содержит только один метод: run. Этот метод вызывается при выполнении команды db:seed Artisan. В методе run вы можете вставлять данные в свою базу данных, как хотите. Вы можете использовать построитель запросов для самостоятельной вставки данных или использовать фабрики моделей Eloquent.

В качестве примера давайте изменим класс DatabaseSeeder, созданный по умолчанию, и добавим выражение вставки фасада DB в методе run:

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class DatabaseSeeder extends Seeder
{
    
    public function run()
    {
        DB::table('users')->insert([
            'name' => Str::random(10),
            'email' => Str::random(10).'@gmail.com',
            'password' => Hash::make('password'),
        ]);
    }
}
В методе run вы можете объявить любые необходимые типы зависимостей. Они будут автоматически извлечены и внедрены через контейнер служб Laravel.

Использование фабрик моделей

Конечно, ручное указание атрибутов для каждой модели наполнителя обременительно. Вместо этого вы можете использовать фабрики моделей для удобного создания большого количества записей в БД. Сначала просмотрите документацию фабрики моделей, чтобы узнать, как определить свои фабрики.

Например, давайте создадим 50 пользователей, у каждого из которых будет по одному посту:

use App\Models\User;


public function run()
{
    User::factory()
            ->count(50)
            ->hasPosts(1)
            ->create();
}

Вызов дополнительных наполнителей

Внутри класса DatabaseSeeder вы можете использовать метод call для запуска других наполнителей. Использование метода call позволяет вам разбить ваши наполнители БД на несколько файлов, так что ни один класс наполнителя не станет слишком большим. Метод call принимает массив классов, которые должны быть выполнены:


public function run()
{
    $this->call([
        UserSeeder::class,
        PostSeeder::class,
        CommentSeeder::class,
    ]);
}