mirror of
https://gitee.com/zoujingli/ThinkAdmin.git
synced 2026-06-06 20:18:10 +08:00
feat(database): 适配多数据库维护操作
新增 DatabaseMaintenance 适配器,按数据库类型生成维护语句:MySQL/MariaDB 支持 REPAIR 与 OPTIMIZE,SQLite 使用 PRAGMA optimize 与 VACUUM,PostgreSQL 使用 VACUUM ANALYZE,SQL Server 使用索引重组与统计信息更新。 重构 DatabaseCommand,将 repair 与 optimize 统一收敛到维护适配器执行,针对不支持的数据库返回友好队列成功提示,避免 SQLite 等环境直接报错中断任务。 补充 DatabaseMaintenanceTest 覆盖各数据库语句生成、标识符转义和不支持提示,并将测试加入 phpunit.xml.dist 的套件列表。
This commit is contained in:
parent
11377b6761
commit
ea10f17079
@ -20,6 +20,7 @@
|
||||
<file>plugin/think-library/tests/RequestTokenServiceTest.php</file>
|
||||
<file>plugin/think-plugs-install/tests/InstallCommandTest.php</file>
|
||||
<file>plugin/think-plugs-system/tests/helper/PublishTest.php</file>
|
||||
<file>plugin/think-plugs-system/tests/helper/DatabaseMaintenanceTest.php</file>
|
||||
<file>plugin/think-plugs-system/tests/helper/IndexNameServiceTest.php</file>
|
||||
<file>plugin/think-plugs-system/tests/helper/PluginMenuServiceTest.php</file>
|
||||
<file>plugin/think-plugs-system/tests/RbacAccessTest.php</file>
|
||||
|
||||
@ -49,12 +49,9 @@ class DatabaseCommand extends Command
|
||||
*/
|
||||
protected function execute(Input $input, Output $output): int
|
||||
{
|
||||
if ($this->app->db->connect()->getConfig('type') === 'sqlite') {
|
||||
$this->setQueueError('Sqlite 数据库不支持 REPAIR 和 OPTIMIZE 操作!');
|
||||
}
|
||||
$action = $input->getArgument('action');
|
||||
if (method_exists($this, $method = "_{$action}")) {
|
||||
$this->{$method}();
|
||||
$action = strtolower(strval($input->getArgument('action')));
|
||||
if (in_array($action, [DatabaseMaintenance::ACTION_REPAIR, DatabaseMaintenance::ACTION_OPTIMIZE], true)) {
|
||||
$this->maintain($action);
|
||||
} else {
|
||||
$this->output->error('Wrong operation, currently allow repair|optimize');
|
||||
}
|
||||
@ -62,44 +59,62 @@ class DatabaseCommand extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复所有数据表.
|
||||
* 执行数据库维护。
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function _repair(): void
|
||||
private function maintain(string $action): void
|
||||
{
|
||||
$connection = $this->app->db->connect();
|
||||
if (!$connection instanceof PDOConnection) {
|
||||
throw new Exception('当前数据库连接不支持 REPAIR 操作');
|
||||
throw new Exception("当前数据库连接不支持 {$action} 操作");
|
||||
}
|
||||
$this->setQueueProgress('正在获取需要修复的数据表', '0');
|
||||
[$tables, $total, $count] = SystemService::getTables();
|
||||
$this->setQueueProgress("总共需要修复 {$total} 张数据表", '0');
|
||||
foreach ($tables as $table) {
|
||||
$this->setQueueMessage($total, ++$count, "正在修复数据表 {$table}");
|
||||
$connection->query("REPAIR TABLE `{$table}`");
|
||||
$this->setQueueMessage($total, $count, "完成修复数据表 {$table}", 1);
|
||||
|
||||
$adapter = new DatabaseMaintenance(strval($connection->getConfig('type') ?: ''));
|
||||
if (!$adapter->supports($action)) {
|
||||
$message = $adapter->unsupportedMessage($action);
|
||||
$this->setQueueProgress($message, '100');
|
||||
$this->setQueueSuccess($message);
|
||||
return;
|
||||
}
|
||||
$this->setQueueSuccess("已完成对 {$total} 张数据表修复操作");
|
||||
|
||||
$label = DatabaseMaintenance::actionLabel($action);
|
||||
$tables = [];
|
||||
if ($adapter->requiresTables($action)) {
|
||||
$this->setQueueProgress("正在获取需要{$label}的数据表", '0');
|
||||
[$tables] = SystemService::getTables();
|
||||
}
|
||||
$operations = $adapter->operations($action, $tables);
|
||||
$total = count($operations);
|
||||
$count = 0;
|
||||
|
||||
if ($total < 1) {
|
||||
$this->setQueueSuccess("没有需要{$label}的数据表,已跳过处理");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->setQueueProgress("总共需要执行 {$total} 项数据库{$label}操作", '0');
|
||||
foreach ($operations as $operation) {
|
||||
$target = $operation['target'];
|
||||
$this->setQueueMessage($total, ++$count, "正在{$label}{$target}");
|
||||
foreach ($operation['statements'] as $statement) {
|
||||
$this->runStatement($connection, $statement);
|
||||
}
|
||||
$this->setQueueMessage($total, $count, "完成{$label}{$target}", 1);
|
||||
}
|
||||
$this->setQueueSuccess("已完成 {$total} 项数据库{$label}操作");
|
||||
}
|
||||
|
||||
/**
|
||||
* 优化所有数据表.
|
||||
* @throws Exception
|
||||
* 执行维护语句。
|
||||
*
|
||||
* @param array{sql:string, mode:string} $statement
|
||||
*/
|
||||
protected function _optimize(): void
|
||||
private function runStatement(PDOConnection $connection, array $statement): void
|
||||
{
|
||||
$connection = $this->app->db->connect();
|
||||
if (!$connection instanceof PDOConnection) {
|
||||
throw new Exception('当前数据库连接不支持 OPTIMIZE 操作');
|
||||
}
|
||||
$this->setQueueProgress('正在获取需要优化的数据表', '0');
|
||||
[$tables, $total, $count] = SystemService::getTables();
|
||||
$this->setQueueProgress("总共需要优化 {$total} 张数据表", '0');
|
||||
foreach ($tables as $table) {
|
||||
$this->setQueueMessage($total, ++$count, "正在优化数据表 {$table}");
|
||||
$connection->query("OPTIMIZE TABLE `{$table}`");
|
||||
$this->setQueueMessage($total, $count, "完成优化数据表 {$table}", 1);
|
||||
}
|
||||
$this->setQueueSuccess("已完成对 {$total} 张数据表优化操作");
|
||||
if ($statement['mode'] === DatabaseMaintenance::MODE_QUERY) {
|
||||
$connection->query($statement['sql']);
|
||||
} else {
|
||||
$connection->execute($statement['sql']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* +----------------------------------------------------------------------
|
||||
* | ThinkAdmin Plugin
|
||||
* +----------------------------------------------------------------------
|
||||
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
|
||||
* +----------------------------------------------------------------------
|
||||
* | Official Website: https://thinkadmin.top
|
||||
* +----------------------------------------------------------------------
|
||||
* | Licensed: https://mit-license.org
|
||||
* | Disclaimer: https://thinkadmin.top/disclaimer
|
||||
* | Vip Rights: https://thinkadmin.top/vip-introduce
|
||||
* +----------------------------------------------------------------------
|
||||
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
|
||||
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
|
||||
* +----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
namespace plugin\system\helper\command\database;
|
||||
|
||||
/**
|
||||
* 数据库维护语句适配器。
|
||||
*/
|
||||
class DatabaseMaintenance
|
||||
{
|
||||
public const ACTION_OPTIMIZE = 'optimize';
|
||||
public const ACTION_REPAIR = 'repair';
|
||||
public const MODE_EXECUTE = 'execute';
|
||||
public const MODE_QUERY = 'query';
|
||||
|
||||
private string $type;
|
||||
|
||||
public function __construct(string $type)
|
||||
{
|
||||
$this->type = $this->normalizeType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作名称。
|
||||
*/
|
||||
public static function actionLabel(string $action): string
|
||||
{
|
||||
return $action === self::ACTION_REPAIR ? '修复' : '优化';
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否支持指定维护操作。
|
||||
*/
|
||||
public function supports(string $action): bool
|
||||
{
|
||||
return match ($this->type) {
|
||||
'mysql', 'mariadb' => in_array($action, [self::ACTION_OPTIMIZE, self::ACTION_REPAIR], true),
|
||||
'sqlite', 'pgsql', 'sqlsrv' => $action === self::ACTION_OPTIMIZE,
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要遍历数据表。
|
||||
*/
|
||||
public function requiresTables(string $action): bool
|
||||
{
|
||||
return $this->supports($action) && in_array($this->type, ['mysql', 'mariadb', 'pgsql', 'sqlsrv'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成不支持时的提示。
|
||||
*/
|
||||
public function unsupportedMessage(string $action): string
|
||||
{
|
||||
return sprintf('%s 数据库暂不支持 %s 操作,已跳过处理。', strtoupper($this->type ?: 'unknown'), strtoupper($action));
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成维护操作。
|
||||
*
|
||||
* @param array<int, string> $tables
|
||||
* @return array<int, array{target:string, statements:array<int, array{sql:string, mode:string}>}>
|
||||
*/
|
||||
public function operations(string $action, array $tables = []): array
|
||||
{
|
||||
if (!$this->supports($action)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return match ($this->type) {
|
||||
'mysql', 'mariadb' => $this->mysqlOperations($action, $tables),
|
||||
'sqlite' => $this->sqliteOperations($action),
|
||||
'pgsql' => $this->pgsqlOperations($action, $tables),
|
||||
'sqlsrv' => $this->sqlsrvOperations($action, $tables),
|
||||
default => [],
|
||||
};
|
||||
}
|
||||
|
||||
private function normalizeType(string $type): string
|
||||
{
|
||||
$type = strtolower(trim($type));
|
||||
return match ($type) {
|
||||
'postgres', 'postgresql' => 'pgsql',
|
||||
'mssql', 'sqlserver' => 'sqlsrv',
|
||||
default => $type,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $tables
|
||||
* @return array<int, array{target:string, statements:array<int, array{sql:string, mode:string}>}>
|
||||
*/
|
||||
private function mysqlOperations(string $action, array $tables): array
|
||||
{
|
||||
$prefix = $action === self::ACTION_REPAIR ? 'REPAIR TABLE' : 'OPTIMIZE TABLE';
|
||||
$operations = [];
|
||||
foreach ($tables as $table) {
|
||||
$operations[] = [
|
||||
'target' => "数据表 {$table}",
|
||||
'statements' => [[
|
||||
'sql' => "{$prefix} {$this->quoteTable($table)}",
|
||||
'mode' => self::MODE_QUERY,
|
||||
]],
|
||||
];
|
||||
}
|
||||
return $operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{target:string, statements:array<int, array{sql:string, mode:string}>}>
|
||||
*/
|
||||
private function sqliteOperations(string $action): array
|
||||
{
|
||||
if ($action !== self::ACTION_OPTIMIZE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [[
|
||||
'target' => '数据库',
|
||||
'statements' => [
|
||||
['sql' => 'PRAGMA optimize', 'mode' => self::MODE_EXECUTE],
|
||||
['sql' => 'VACUUM', 'mode' => self::MODE_EXECUTE],
|
||||
],
|
||||
]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $tables
|
||||
* @return array<int, array{target:string, statements:array<int, array{sql:string, mode:string}>}>
|
||||
*/
|
||||
private function pgsqlOperations(string $action, array $tables): array
|
||||
{
|
||||
if ($action !== self::ACTION_OPTIMIZE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$operations = [];
|
||||
foreach ($tables as $table) {
|
||||
$operations[] = [
|
||||
'target' => "数据表 {$table}",
|
||||
'statements' => [[
|
||||
'sql' => "VACUUM (ANALYZE) {$this->quoteTable($table)}",
|
||||
'mode' => self::MODE_EXECUTE,
|
||||
]],
|
||||
];
|
||||
}
|
||||
return $operations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $tables
|
||||
* @return array<int, array{target:string, statements:array<int, array{sql:string, mode:string}>}>
|
||||
*/
|
||||
private function sqlsrvOperations(string $action, array $tables): array
|
||||
{
|
||||
if ($action !== self::ACTION_OPTIMIZE) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$operations = [];
|
||||
foreach ($tables as $table) {
|
||||
$quoted = $this->quoteTable($table);
|
||||
$operations[] = [
|
||||
'target' => "数据表 {$table}",
|
||||
'statements' => [
|
||||
['sql' => "ALTER INDEX ALL ON {$quoted} REORGANIZE", 'mode' => self::MODE_EXECUTE],
|
||||
['sql' => "UPDATE STATISTICS {$quoted}", 'mode' => self::MODE_EXECUTE],
|
||||
],
|
||||
];
|
||||
}
|
||||
return $operations;
|
||||
}
|
||||
|
||||
private function quoteTable(string $table): string
|
||||
{
|
||||
$parts = array_map('trim', explode('.', $table));
|
||||
$parts = array_filter($parts, static fn (string $part): bool => $part !== '');
|
||||
|
||||
return implode('.', array_map(fn (string $part): string => $this->quoteIdentifier($part), $parts));
|
||||
}
|
||||
|
||||
private function quoteIdentifier(string $identifier): string
|
||||
{
|
||||
return match ($this->type) {
|
||||
'pgsql' => '"' . str_replace('"', '""', $identifier) . '"',
|
||||
'sqlsrv' => '[' . str_replace(']', ']]', $identifier) . ']',
|
||||
default => '`' . str_replace('`', '``', $identifier) . '`',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* +----------------------------------------------------------------------
|
||||
* | ThinkAdmin Plugin
|
||||
* +----------------------------------------------------------------------
|
||||
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
|
||||
* +----------------------------------------------------------------------
|
||||
* | Official Website: https://thinkadmin.top
|
||||
* +----------------------------------------------------------------------
|
||||
* | Licensed: https://mit-license.org
|
||||
* | Disclaimer: https://thinkadmin.top/disclaimer
|
||||
* | Vip Rights: https://thinkadmin.top/vip-introduce
|
||||
* +----------------------------------------------------------------------
|
||||
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
|
||||
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
|
||||
* +----------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
namespace plugin\system\tests\helper;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use plugin\system\helper\command\database\DatabaseMaintenance;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
class DatabaseMaintenanceTest extends TestCase
|
||||
{
|
||||
public function testMysqlBuildsOptimizeAndRepairStatements(): void
|
||||
{
|
||||
$adapter = new DatabaseMaintenance('mysql');
|
||||
|
||||
$this->assertTrue($adapter->supports(DatabaseMaintenance::ACTION_OPTIMIZE));
|
||||
$this->assertTrue($adapter->supports(DatabaseMaintenance::ACTION_REPAIR));
|
||||
$this->assertTrue($adapter->requiresTables(DatabaseMaintenance::ACTION_OPTIMIZE));
|
||||
|
||||
$optimize = $adapter->operations(DatabaseMaintenance::ACTION_OPTIMIZE, [
|
||||
'system_queue',
|
||||
'admin.system_user',
|
||||
'weird`name',
|
||||
]);
|
||||
$repair = $adapter->operations(DatabaseMaintenance::ACTION_REPAIR, ['system_queue']);
|
||||
|
||||
$this->assertSame('OPTIMIZE TABLE `system_queue`', $optimize[0]['statements'][0]['sql']);
|
||||
$this->assertSame('OPTIMIZE TABLE `admin`.`system_user`', $optimize[1]['statements'][0]['sql']);
|
||||
$this->assertSame('OPTIMIZE TABLE `weird``name`', $optimize[2]['statements'][0]['sql']);
|
||||
$this->assertSame(DatabaseMaintenance::MODE_QUERY, $optimize[0]['statements'][0]['mode']);
|
||||
$this->assertSame('REPAIR TABLE `system_queue`', $repair[0]['statements'][0]['sql']);
|
||||
}
|
||||
|
||||
public function testSqliteBuildsDatabaseScopedOptimizeAndSkipsRepair(): void
|
||||
{
|
||||
$adapter = new DatabaseMaintenance('sqlite');
|
||||
|
||||
$this->assertTrue($adapter->supports(DatabaseMaintenance::ACTION_OPTIMIZE));
|
||||
$this->assertFalse($adapter->supports(DatabaseMaintenance::ACTION_REPAIR));
|
||||
$this->assertFalse($adapter->requiresTables(DatabaseMaintenance::ACTION_OPTIMIZE));
|
||||
|
||||
$operations = $adapter->operations(DatabaseMaintenance::ACTION_OPTIMIZE, ['system_queue']);
|
||||
|
||||
$this->assertSame('数据库', $operations[0]['target']);
|
||||
$this->assertSame('PRAGMA optimize', $operations[0]['statements'][0]['sql']);
|
||||
$this->assertSame('VACUUM', $operations[0]['statements'][1]['sql']);
|
||||
$this->assertSame(DatabaseMaintenance::MODE_EXECUTE, $operations[0]['statements'][0]['mode']);
|
||||
$this->assertSame([], $adapter->operations(DatabaseMaintenance::ACTION_REPAIR, ['system_queue']));
|
||||
$this->assertSame('SQLITE 数据库暂不支持 REPAIR 操作,已跳过处理。', $adapter->unsupportedMessage(DatabaseMaintenance::ACTION_REPAIR));
|
||||
}
|
||||
|
||||
public function testPgsqlBuildsVacuumAnalyzeWithDoubleQuotedTables(): void
|
||||
{
|
||||
$adapter = new DatabaseMaintenance('postgresql');
|
||||
|
||||
$this->assertTrue($adapter->supports(DatabaseMaintenance::ACTION_OPTIMIZE));
|
||||
$this->assertFalse($adapter->supports(DatabaseMaintenance::ACTION_REPAIR));
|
||||
$this->assertTrue($adapter->requiresTables(DatabaseMaintenance::ACTION_OPTIMIZE));
|
||||
|
||||
$operations = $adapter->operations(DatabaseMaintenance::ACTION_OPTIMIZE, [
|
||||
'public.system_queue',
|
||||
'weird"name',
|
||||
]);
|
||||
|
||||
$this->assertSame('VACUUM (ANALYZE) "public"."system_queue"', $operations[0]['statements'][0]['sql']);
|
||||
$this->assertSame('VACUUM (ANALYZE) "weird""name"', $operations[1]['statements'][0]['sql']);
|
||||
$this->assertSame(DatabaseMaintenance::MODE_EXECUTE, $operations[0]['statements'][0]['mode']);
|
||||
}
|
||||
|
||||
public function testSqlsrvBuildsIndexAndStatisticsStatementsWithBracketQuotedTables(): void
|
||||
{
|
||||
$adapter = new DatabaseMaintenance('mssql');
|
||||
|
||||
$this->assertTrue($adapter->supports(DatabaseMaintenance::ACTION_OPTIMIZE));
|
||||
$this->assertFalse($adapter->supports(DatabaseMaintenance::ACTION_REPAIR));
|
||||
$this->assertTrue($adapter->requiresTables(DatabaseMaintenance::ACTION_OPTIMIZE));
|
||||
|
||||
$operations = $adapter->operations(DatabaseMaintenance::ACTION_OPTIMIZE, [
|
||||
'dbo.system_queue',
|
||||
'weird]name',
|
||||
]);
|
||||
|
||||
$this->assertSame('ALTER INDEX ALL ON [dbo].[system_queue] REORGANIZE', $operations[0]['statements'][0]['sql']);
|
||||
$this->assertSame('UPDATE STATISTICS [dbo].[system_queue]', $operations[0]['statements'][1]['sql']);
|
||||
$this->assertSame('ALTER INDEX ALL ON [weird]]name] REORGANIZE', $operations[1]['statements'][0]['sql']);
|
||||
$this->assertSame(DatabaseMaintenance::MODE_EXECUTE, $operations[0]['statements'][0]['mode']);
|
||||
}
|
||||
|
||||
public function testUnsupportedDatabaseReturnsNoOperations(): void
|
||||
{
|
||||
$adapter = new DatabaseMaintenance('oracle');
|
||||
|
||||
$this->assertFalse($adapter->supports(DatabaseMaintenance::ACTION_OPTIMIZE));
|
||||
$this->assertFalse($adapter->supports(DatabaseMaintenance::ACTION_REPAIR));
|
||||
$this->assertSame([], $adapter->operations(DatabaseMaintenance::ACTION_OPTIMIZE, ['system_queue']));
|
||||
$this->assertSame('ORACLE 数据库暂不支持 OPTIMIZE 操作,已跳过处理。', $adapter->unsupportedMessage(DatabaseMaintenance::ACTION_OPTIMIZE));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user