Laravel: миграция и посев для производственных данных
Моему приложению нужен предварительно зарегистрированный набор данных для работы. Поэтому мне нужно вставить их в базу данных при настройке приложения.
Laravel предлагает два механизма:
- Миграции базы данных: "Они позволяют команде изменять схему базы данных и оставаться в курсе текущего состояния схемы".
- Заполнение базы данных: "Laravel также включает в себя простой способ заполнить вашу базу данных тестовыми данными, используя начальные классы".
Когда я читаю это описание, ни одно из этих решений не кажется адаптированным.
Подобный вопрос был задан на stackru и ответил. В ответе предлагается использовать сеялку базы данных для заполнения базы данных путем определения текущей среды:
<?php
class DatabaseSeeder extends Seeder {
public function run()
{
Eloquent::unguard();
if (App::environment() === 'production')
{
$this->call('ProductionSeeder');
}
else
{
$this->call('StagingSeeder');
}
}
}
Конечно, это решение работает. Но я не уверен, что это правильный способ сделать это, потому что, вставляя данные с помощью сеялок, вы теряете все преимущества механизма миграции (обновление базы данных, откат...)
Я хочу знать, что является лучшей практикой в этом случае.
5 ответов
Развитие Laravel - это свобода. Итак, если вам нужно заполнить свою производственную базу данных и подумать, что DatabaseSeeder - лучшее место для этого, почему бы и нет?
Хорошо, сеялка должна использоваться в основном с тестовыми данными, но вы увидите, что некоторые люди используют ее как есть.
Я вижу этот важный вид семян как часть моей миграции, так как это не может быть за пределами моих таблиц базы данных и artisan migrate
запускается каждый раз, когда я развертываю новую версию своего приложения, поэтому я просто делаю
php artisan migrate:make seed_models_table
И создай в нём мои семена:
public function up()
{
$models = array(
array('name' => '...'),
);
DB::table('models')->insert($models);
}
Я часто задавался вопросом, каков правильный ответ на это. Лично я бы воздержался от использования заполнения для заполнения требуемых строк в базе данных, так как вам придется загружать условную логику, чтобы гарантировать, что вы не попытаетесь заполнить то, что уже есть. (Удаление и воссоздание данных очень нецелесообразно, так как вы можете столкнуться с несовпадением ключей, и если вы используете каскадное удаление, вы можете случайно стереть загрузку вашей базы данных, моя ошибка!;-)
Я поместил "заполнение" строк в сценарий миграции, так как есть вероятность, что данные должны быть там как часть процесса развертывания.
Стоит отметить, что вы должны использовать класс DB вместо моделей Eloquent для заполнения этих данных, поскольку структура вашего класса со временем может измениться, что затем не позволит вам заново создать базу данных с нуля (без переписывания истории и изменения файлов миграции, что Я уверен, что это плохо.)
Я хотел бы пойти с чем-то вроде этого:
public function up()
{
DB::beginTransaction();
Schema::create(
'town',
function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
}
);
DB::table('town')
->insert(
array(
array('London'),
array('Paris'),
array('New York')
)
);
Schema::create(
'location',
function (Blueprint $table) {
$table->increments('id');
$table->integer('town_id')->unsigned()->index();
$table->float('lat');
$table->float('long');
$table->timestamps();
$table->foreign('town_id')->references('id')->on('town')->onDelete('cascade');
}
);
DB::commit();
}
Это позволяет мне легко "заполнять" таблицу города, когда я ее впервые создаю, и не мешать никаким дополнениям, вносимым в нее во время выполнения.
Это то, что я использую в производстве.
Так как я запускаю миграцию на каждом развертывании
artisan migrate
Я создаю сеялку (просто для того, чтобы не допустить посева данных из-под миграции для последующего легкого доступа и знаю, какие данные я посеял, просто просматривая файлы в каталоге сеялки), используя
php artisan make:seeder YourTableSeeder
а затем позвоните в миграцию.
class YourTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$seeder = new YourTableSeeder();
$seeder->run();
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
}
}
Я не добавляю этот начальный вызов в seed /DatabaseSeeder.php, чтобы не запускать его дважды при новой установке.
Командное решение Artisan
Создать новую команду ремесленника
php artisan make:command UpsertConfigurationTables
Вставьте это во вновь созданный файл:
UpsertConfigurationTables.php
<?php namespace App\Console\Commands; use Exception; use Illuminate\Console\Command; class UpsertConfigurationTables extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'upsert:configuration'; /** * The console command description. * * @var string */ protected $description = 'Upserts the configuration tables.'; /** * The models we want to upsert configuration data for * * @var array */ private $_models = [ 'App\ExampleModel' ]; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return mixed */ public function handle() { foreach ($this->_models as $model) { // check that class exists if (!class_exists($model)) { throw new Exception('Configuration seed failed. Model does not exist.'); } // check that seed data exists if (!defined($model . '::CONFIGURATION_DATA')) { throw new Exception('Configuration seed failed. Data does not exist.'); } /** * seed each record */ foreach ($model::CONFIGURATION_DATA as $row) { $record = $this->_getRecord($model, $row['id']); foreach ($row as $key => $value) { $this->_upsertRecord($record, $row); } } } } /** * _fetchRecord - fetches a record if it exists, otherwise instantiates a new model * * @param string $model - the model * @param integer $id - the model ID * * @return object - model instantiation */ private function _getRecord ($model, $id) { if ($this->_isSoftDeletable($model)) { $record = $model::withTrashed()->find($id); } else { $record = $model::find($id); } return $record ? $record : new $model; } /** * _upsertRecord - upsert a database record * * @param object $record - the record * @param array $row - the row of update data * * @return object */ private function _upsertRecord ($record, $row) { foreach ($row as $key => $value) { if ($key === 'deleted_at' && $this->_isSoftDeletable($record)) { if ($record->trashed() && !$value) { $record->restore(); } else if (!$record->trashed() && $value) { $record->delete(); } } else { $record->$key = $value; } } return $record->save(); } /** * _isSoftDeletable - Determines if a model is soft-deletable * * @param string $model - the model in question * * @return boolean */ private function _isSoftDeletable ($model) { $uses = array_merge(class_uses($model), class_uses(get_parent_class($model))); return in_array('Illuminate\Database\Eloquent\SoftDeletes', $uses); } }
Заселить
$_models
с моделями Eloquent, которые вы хотите посеять.Определите ряды семян в модели:
const CONFIGURATION_DATA
<?php namespace App; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; class ExampleModel extends Model { use SoftDeletes; const CONFIG_VALUE_ONE = 1; const CONFIG_VALUE_TWO = 2; const CONFIGURATION_DATA = [ [ 'id' => self::CONFIG_VALUE_ONE, 'col1' => 'val1', 'col2' => 'val2', 'deleted_at' => false ], [ 'id' => self::CONFIG_VALUE_TWO, 'col1' => 'val1', 'col2' => 'val2', 'deleted_at' => true ], ]; }
Добавьте команду в свой сценарий развертывания Laravel Forge (или любой другой сценарий развертывания CI):
php artisan upsert:configuration
Другие примечательные вещи:
- Функциональность Upsert: если вы когда-нибудь захотите изменить какую-либо из засеянных строк, просто обновите их в своей модели, и при следующем развертывании будут обновлены значения вашей базы данных. Он никогда не создаст повторяющиеся строки.
- Модели с мягким удалением: обратите внимание, что вы определяете удаления, задавая
deleted_at
кtrue
илиfalse
. Команда Artisan обработает вызов правильного метода для удаления или восстановления вашей записи.
Проблемы с другими упомянутыми решениями:
- Сеялка: Запуск сеялки в производстве - это злоупотребление сеялок. Меня беспокоит, что инженер в будущем изменит сеялки, думая, что это безвредно, поскольку в документации указано, что они предназначены для заполнения тестовых данных.
- Миграции: добавление данных в миграцию странно и является злоупотреблением целью миграции. Он также не позволяет вам обновлять эти значения после выполнения миграции.
Я тоже столкнулся с этим. В итоге я добавил в свою миграцию ремесленную команду для запуска сеялки.
use Illuminate\Support\Facades\Artisan;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
});
Artisan::call('db:seed --class=DataSeeder --force');
}