Saya belum menulis tentang Habrรฉ tentang bagaimana saya sampai pada ide membentuk komponen untuk proyek masa depan saya atau proyek saat ini alih-alih menulis kode secara langsung. Singkatnya, seperti ini ... Saya menulis banyak proyek berbeda, menemukan komponen semu dan setiap kali saya menemukan fakta bahwa dalam satu proyek sangat nyaman untuk menggunakannya, dan di proyek lain itu sangat tidak nyaman. Saya mencoba mentransfer komponen yang "nyaman" ke proyek dan itu menjadi lebih tidak nyaman ... Singkatnya, tangan saya tidak berada di tempat yang tepat, kepala saya terlalu ambisius ... Seiring waktu, saya sampai pada pemikiran lain: " Kita perlu membuat repositori di GitHub dengan komponen terpisah, yang tidak akan bergantung pada komponen lain "... Semuanya berjalan dengan baik, tetapi saya sampai ke komponen yang ingin bekerja dengan komponen lain ... Akibatnya, antarmuka dengan metode datang untuk menyelamatkan.Dan sekarang mari kita bicarakankomponen SQL dari migrasi seperti yang saya lihat.
Jadi, kebanyakan orang, serta kolega saya, yakin bahwa migrasi tidak hanya berfungsi untuk memperbarui basis data antar pengembang, tetapi juga untuk operasi dengan file, folder, dan sebagainya. Misalnya, buat direktori untuk semua pengembang atau sesuatu yang lain untuk sesuatu di sana ...
Mungkin saya bisa salah, tetapi secara pribadi saya yakin sendiri bahwa migrasi diperlukan secara eksklusif untuk operasi database SQL. Untuk mengupdate file, Anda bisa menggunakan file git atau central init yang sama, seperti di Yii2.
Ide
Komponen migrasi, karena khusus untuk operasi SQL, akan didasarkan pada 2 file SQL. Ya, di sini sekarang akan ada banyak kritik tentang ambang pintu masuk dan hal-hal lain, tetapi saya akan segera mengatakan bahwa selama kami bekerja di perusahaan, kami beralih dari SQLBuilder ke SQL murni, karena lebih cepat. Selain itu, sebagian besar IDE modern dapat menghasilkan DDL untuk operasi database. Dan bayangkan saja, Anda perlu membuat tabel, mengisinya dengan data, dan juga mengubah sesuatu di tabel lain. Di satu sisi, Anda mendapatkan kode panjang dengan pembangun, di sisi lain, Anda dapat menggunakan SQL murni dalam pembangun yang sama, atau mungkin situasi ini bercampur ... Singkatnya, kemudian saya menyadari dan memutuskan bahwa dalam komponen saya dan pendekatan pemrograman secara umum akan ada dualitas sesedikit mungkin. Karena itu, saya memutuskan untuk hanya menggunakan kode SQL.
: , UP DOWN, . . .
SqlMigration
, . . .
ConsoleSqlMigration
, SqlMigration
. parent::
().
DatabaseInterface
. :
schema -
table -
path -
() , (). .
SqlMigration
. , , - . :
public function up(int $count = 0): array;
public function down(int $count = 0): array;
public function history(int $limit = 0): array;
public function create(string $name): bool;
. , PHPDoc:
/**
*
*
* @param int $count (0 - )
*
* @return array . :
* 1. , ,
* 2. :
* [
* 'success' => [...],
* 'error' => [...]
* ]
* error .
*
* @throws SqlMigrationException
*/
public function up(int $count = 0): array;
/**
*
*
* @param int $count (0 - )
*
* @return array . :
* 1. , ,
* 2. :
* [
* 'success' => [...],
* 'error' => [...]
* ]
* error .
*
* @throws SqlMigrationException
*/
public function down(int $count = 0): array;
/**
*
*
* @param int $limit (null - )
*
* @return array
*/
public function history(int $limit = 0): array;
/**
*
*
* @param string $name
*
* @return bool true, .
*
* @throws RuntimeException|SqlMigrationException
*/
public function create(string $name): bool;
SqlMigration
. . , :
/**
*
*/
public const UP = 'up';
public const DOWN = 'down';
/**
* SqlMigration constructor.
*
* @param DatabaseInterface $database
* @param array $settings
*
* @throws SqlMigrationException
*/
public function __construct(DatabaseInterface $database, array $settings) {
$this->database = $database;
$this->settings = $settings;
foreach (['schema', 'table', 'path'] as $settingsKey) {
if (!array_key_exists($settingsKey, $settings)) {
throw new SqlMigrationException(" {$settingsKey} .");
}
}
}
, . bool
:
/**
*
*
* @return bool true, .
*
*
* @throws SqlMigrationException
*/
public function initSchemaAndTable(): bool {
$schemaSql = <<<SQL
CREATE SCHEMA IF NOT EXISTS {$this->settings['schema']};
SQL;
if (!$this->database->execute($schemaSql)) {
throw new SqlMigrationException(' ');
}
$tableSql = <<<SQL
CREATE TABLE IF NOT EXISTS {$this->settings['schema']}.{$this->settings['table']} (
"name" varchar(180) COLLATE "default" NOT NULL,
apply_time int4,
CONSTRAINT {$this->settings['table']}_pk PRIMARY KEY ("name")
) WITH (OIDS=FALSE)
SQL;
if (!$this->database->execute($tableSql)) {
throw new SqlMigrationException(' ');
}
return true;
}
. ( ):
/**
*
*
* @param string $name
*
* @throws SqlMigrationException
*/
protected function validateName(string $name): void {
if (!preg_match('/^[\w]+$/', $name)) {
throw new SqlMigrationException(' , .');
}
}
/**
* : m{ Ymd_His}_name
*
* @param string $name
*
* @return string
*/
protected function generateName(string $name): string {
return 'm' . gmdate('Ymd_His') . "_{$name}";
}
, . : m___ - , :
/**
* @inheritDoc
*
* @throws RuntimeException|SqlMigrationException
*/
public function create(string $name): bool {
$this->validateName($name);
$migrationMame = $this->generateName($name);
$path = "{$this->settings['path']}/{$migrationMame}";
if (!mkdir($path, 0775, true) && !is_dir($path)) {
throw new RuntimeException(" . {$path} ");
}
if (file_put_contents($path . '/up.sql', '') === false) {
throw new RuntimeException(" {$path}/up.sql");
}
if (!file_put_contents($path . '/down.sql', '') === false) {
throw new RuntimeException(" {$path}/down.sql");
}
return true;
}
, , . :
/**
*
*
* @param int $limit (null - )
*
* @return array
*/
protected function getHistoryList(int $limit = 0): array {
$limitSql = $limit === 0 ? '' : "LIMIT {$limit}";
$historySql = <<<SQL
SELECT "name", apply_time
FROM {$this->settings['schema']}.{$this->settings['table']}
ORDER BY apply_time DESC, "name" DESC {$limitSql}
SQL;
return $this->database->queryAll($historySql);
}
, :
/**
* @inheritDoc
*/
public function history(int $limit = 0): array {
$historyList = $this->getHistoryList($limit);
if (empty($historyList)) {
return [' '];
}
$messages = [];
foreach ($historyList as $historyRow) {
$messages[] = " {$historyRow['name']} " . date('Y-m-d H:i:s', $historyRow['apply_time']);
}
return $messages;
}
, , , . , .
/**
*
*
* @param string $name
*
* @return bool true, ( ).
* .
*
* @throws SqlMigrationException
*/
protected function addHistory(string $name): bool {
$sql = <<<SQL
INSERT INTO {$this->settings['schema']}.{$this->settings['table']} ("name", apply_time) VALUES(:name, :apply_time);
SQL;
if (!$this->database->execute($sql, ['name' => $name, 'apply_time' => time()])) {
throw new SqlMigrationException(" {$name}");
}
return true;
}
/**
*
*
* @param string $name
*
* @return bool true, ( ).
* .
*
* @throws SqlMigrationException
*/
protected function removeHistory(string $name): bool {
$sql = <<<SQL
DELETE FROM {$this->settings['schema']}.{$this->settings['table']} WHERE "name" = :name;
SQL;
if (!$this->database->execute($sql, ['name' => $name])) {
throw new SqlMigrationException(" {$name}");
}
return true;
}
, . , .
/**
*
*
* @return array
*/
protected function getNotAppliedList(): array {
$historyList = $this->getHistoryList();
$historyMap = [];
foreach ($historyList as $item) {
$historyMap[$item['name']] = true;
}
$notApplied = [];
$directoryList = glob("{$this->settings['path']}/m*_*_*");
foreach ($directoryList as $directory) {
if (!is_dir($directory)) {
continue;
}
$directoryParts = explode('/', $directory);
preg_match('/^(m(\d{8}_?\d{6})\D.*?)$/is', end($directoryParts), $matches);
$migrationName = $matches[1];
if (!isset($historyMap[$migrationName])) {
$migrationDateTime = DateTime::createFromFormat('Ymd_His', $matches[2])->format('Y-m-d H:i:s');
$notApplied[] = [
'path' => $directory,
'name' => $migrationName,
'date_time' => $migrationDateTime
];
}
}
ksort($notApplied);
return $notApplied;
}
: up down. , up down . , , . , ( ) (up/down - , ).
/**
*
*
* @param array $list
* @param int $count
* @param string $type (up/down)
*
* @return array
*
* @throws RuntimeException
*/
protected function execute(array $list, int $count, string $type): array {
$migrationInfo = [];
for ($index = 0; $index < $count; $index++) {
$migration = $list[$index];
$migration['path'] = array_key_exists('path', $migration) ? $migration['path'] :
"{$this->settings['path']}/{$migration['name']}";
$migrationContent = file_get_contents("{$migration['path']}/{$type}.sql");
if ($migrationContent === false) {
throw new RuntimeException(' / ');
}
try {
if (!empty($migrationContent)) {
$this->database->beginTransaction();
$this->database->execute($migrationContent);
$this->database->commit();
}
if ($type === self::UP) {
$this->addHistory($migration['name']);
} else {
$this->removeHistory($migration['name']);
}
$migrationInfo['success'][] = $migration;
} catch (SqlMigrationException | PDOException $exception) {
$migrationInfo['error'][] = array_merge($migration, ['errorMessage' => $exception->getMessage()]);
break;
}
}
return $migrationInfo;
}
:
$migration['path'] =
array_key_exists('path', $migration) ? $migration['path'] : "{$this->settings['path']}/{$migration['name']}";
( ):
$migrationContent = file_get_contents("{$migration['path']}/{$type}.sql");
. UP - , .
( , ).
, . () up down:
/**
* @inheritDoc
*/
public function up(int $count = 0): array {
$executeList = $this->getNotAppliedList();
if (empty($executeList)) {
return [];
}
$executeListCount = count($executeList);
$executeCount = $count === 0 ? $executeListCount : min($count, $executeListCount);
return $this->execute($executeList, $executeCount, self::UP);
}
/**
* @inheritDoc
*/
public function down(int $count = 0): array {
$executeList = $this->getHistoryList();
if (empty($executeList)) {
return [];
}
$executeListCount = count($executeList);
$executeCount = $count === 0 ? $executeListCount : min($count, $executeListCount);
return $this->execute($executeList, $executeCount, self::DOWN);
}
. , . , , . - API . , , , :
<?php
declare(strict_types = 1);
namespace mepihindeveloper\components;
use mepihindeveloper\components\exceptions\SqlMigrationException;
use mepihindeveloper\components\interfaces\DatabaseInterface;
use RuntimeException;
/**
* Class ConsoleSqlMigration
*
* SQL ()
*
* @package mepihindeveloper\components
*/
class ConsoleSqlMigration extends SqlMigration {
public function __construct(DatabaseInterface $database, array $settings) {
parent::__construct($database, $settings);
try {
$this->initSchemaAndTable();
Console::writeLine(' ', Console::FG_GREEN);
} catch (SqlMigrationException $exception) {
Console::writeLine($exception->getMessage(), Console::FG_RED);
exit;
}
}
public function up(int $count = 0): array {
$migrations = parent::up($count);
if (empty($migrations)) {
Console::writeLine(" ");
exit;
}
foreach ($migrations['success'] as $successMigration) {
Console::writeLine(" {$successMigration['name']} ", Console::FG_GREEN);
}
if (array_key_exists('error', $migrations)) {
foreach ($migrations['error'] as $errorMigration) {
Console::writeLine(" {$errorMigration['name']}", Console::FG_RED);
}
exit;
}
return $migrations;
}
public function down(int $count = 0): array {
$migrations = parent::down($count);
if (empty($migrations)) {
Console::writeLine(" ");
exit;
}
if (array_key_exists('error', $migrations)) {
foreach ($migrations['error'] as $errorMigration) {
Console::writeLine(" {$errorMigration['name']} : " .
PHP_EOL .
$errorMigration['errorMessage'],
Console::FG_RED);
}
exit;
}
foreach ($migrations['success'] as $successMigration) {
Console::writeLine(" {$successMigration['name']} ", Console::FG_GREEN);
}
return $migrations;
}
public function create(string $name): bool {
try {
parent::create($name);
Console::writeLine(" {$name} ");
} catch (RuntimeException | SqlMigrationException $exception) {
Console::writeLine($exception->getMessage(), Console::FG_RED);
return false;
}
return true;
}
public function history(int $limit = 0): array {
$historyList = parent::history($limit);
foreach ($historyList as $historyRow) {
Console::writeLine($historyRow);
}
return $historyList;
}
}