Введение
В работе с базами данных ключевым аспектом является не только отслеживание изменений в их структуре, но и подробная документация таблиц и их полей. Это особенно важно для проектов с быстро изменяющейся архитектурой, где ясность и точность данных играют решающую роль. Например, в крупных веб-приложениях часто требуется фиксировать новые таблицы, обновления полей или удаление устаревших элементов.
Для решения данной задачи разработаны два PHP-скрипта, предназначенные для работы с PostgreSQL. Эти скрипты выполняют две основные функции:
1. Сравнение старой и новой структуры базы данных с выявлением добавленных, удалённых и изменённых таблиц.
2. Создание Markdown-документации, которая содержит подробное описание назначения таблиц и характеристик их полей, что делает изменения в структуре базы данных прозрачными для разработчиков.
Markdown – это простой и удобный язык разметки, который идеально подходит для создания документации. Его основное преимущество – это лёгкость в использовании: с его помощью можно быстро формировать таблицы, списки и другие элементы, структурирующие данные. В контексте работы с базами данных Markdown позволяет создавать понятные файлы, которые легко преобразуются в HTML или другие форматы, что делает их универсальными для технической документации.
В качестве СУБД используется PostgreSQL – одна из самых мощных и популярных реляционных баз данных. Она обеспечивает поддержку сложных запросов, транзакций и различных типов данных, что делает её отличным выбором для создания гибкой и динамичной архитектуры проектов.
Рассматриваемые скрипты обрабатывают SQL-структуру базы данных и преобразуют её в формат Markdown. Они предназначены для автоматизации процесса документирования, добавляя информацию о значении и функционале каждой таблицы и её полей. При этом предусмотрена возможность ручного доработки описания, что обеспечивает высокую точность и полноту итоговой документации.
Описание первого скрипта: анализ структуры базы данных
Функциональность
Скрипт предназначен для извлечения информации о структуре базы данных PostgreSQL и автоматического преобразования её в документацию в формате Markdown. Это решение позволяет систематизировать данные о таблицах и их полях, добавляя описания для каждого элемента. Такой подход облегчает анализ базы данных и упрощает коммуникацию между разработчиками.
Этапы работы скрипта
1. Подключение к базе данных
На первом этапе скрипт устанавливает соединение с базой данных PostgreSQL, используя библиотеку PDO (PHP Data Objects). PDO обеспечивает унифицированный интерфейс для работы с базами данных и позволяет задавать параметры подключения, такие как хост, порт, имя базы данных, логин и пароль пользователя:
$dsn = "pgsql:host=localhost;port=5432;dbname=name;"; $user = "user"; $password = "password"; $pdo = new PDO($dsn, $user, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
Если подключение не удаётся, скрипт выводит соответствующее сообщение об ошибке.
Для повышения безопасности можно вынести параметры подключения в отдельный файл конфигурации.
2. Запрос информации о структуре базы данных
После успешного подключения скрипт выполняет SQL-запрос к системной таблице базы данных information_schema.columns. Эта таблица содержит метаинформацию о структуре базы данных, включая:
-
Название таблиц.
-
Название столбцов.
-
Типы данных (например,
text, integer, boolean). -
Возможность содержать NULL.
-
Значения по умолчанию.
-
Максимальную длину для строковых типов данных или точность для числовых типов.
SELECT table_name, column_name, data_type, is_nullable, character_maximum_length, numeric_precision, numeric_scale, column_default FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position;
Этот запрос фильтрует столбцы из схемы public и упорядочивает их по имени таблицы и порядковому номеру столбца. При необходимости запрос можно адаптировать для других схем.
3. Обработка полученных данных
После выполнения SQL-запроса скрипт обрабатывает результаты:
-
Преобразование значений. Системные значения преобразуются в более удобочитаемый формат. Например, поле
is_nullableсо значением YES заменяется на «Да», а NO – на «Нет». -
Добавление описаний. Скрипт поддерживает добавление пользовательских описаний для полей. Например, для столбца
idможно задать описание «Идентификатор», а для name – «Наименование». Эти описания могут быть заданы в виде массива или загружены из внешнего файла.
$row['is_nullable'] = ($row['is_nullable'] === 'YES') ? "Да" : "Нет"; $maxLen = !empty($row['character_maximum_length']) ? $row['character_maximum_length'] : $row['numeric_precision']; $row['max_len'] = $maxLen ?? '-';
4. Группировка данных по таблицам
Извлечённые данные группируются по таблицам, чтобы каждая таблица представлялась отдельным разделом в итоговом Markdown-документе. Это позволяет структурировать вывод и делает его более понятным.
$grouped = []; foreach ($results as $row) { $grouped[$row['table_name']][] = $row; }
5. Формирование Markdown-документа
На этом этапе скрипт преобразует сгруппированные данные в Markdown. Для каждой таблицы создаётся заголовок, за которым следует таблица с характеристиками столбцов: название, тип, возможность быть пустым, значения по умолчанию, длина и описание.
Пример таблицы в Markdown:
| Название поля | Тип поля | По умолчанию | Может NULL | Макс. длина | Описание | |---------------|----------|--------------|------------|-------------|----------| | id | integer | NULL | Нет | - | Идентификатор | | name | text | NULL | Да | 255 | Наименование |
Пример генерации таблицы в PHP:
$mdOutput[] = "| Название поля | Тип поля | По умолчанию | Может NULL | Макс. длина | Описание |\n"; $mdOutput[] = "|---------------|----------|--------------|------------|-------------|----------|\n"; foreach ($rows as $row) { $mdOutput[] = "| " . ($row['column_name'] ?: '-') . " | " . ($row['data_type'] ?: '-') . " | " . ($row['column_default'] ?: '-') . " | " . ($row['is_nullable'] ?: '-') . " | " . ($row['max_len'] ?: '-') . " | " . ($description ?: '-') . " |\n"; }
6. Сохранение результата в файл
После генерации содержимого Markdown-документа скрипт сохраняет его в файл с именем database_structure_new.md. Этот файл является готовой технической документацией и может быть использован для анализа базы данных или её сопровождения.
file_put_contents('database_structure_new.md', implode("", $mdOutput));
Итог
// Настройка подключения к базе данных PostgreSQL $dsn = "pgsql:host=localhost;port=5432;dbname=name;"; // Укажите имя вашей базы данных $user = "user"; // Укажите ваше имя пользователя $password = "password"; // Укажите ваш пароль try { // Создаем объект подключения к базе данных $pdo = new PDO($dsn, $user, $password, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); // SQL-запрос для извлечения структуры всех таблиц $query = " SELECT table_name, column_name, data_type, is_nullable, character_maximum_length, numeric_precision, numeric_scale, column_default FROM information_schema.columns WHERE table_schema = 'public' ORDER BY table_name, ordinal_position"; // Выполнение запроса и получение результатов $stmt = $pdo->query($query); $results = $stmt->fetchAll(PDO::FETCH_ASSOC); // Пост-обработка данных и замена значений для удобства $mdOutput = []; // Массив для формирования вывода в Markdown $descriptions = [ 'id' => 'Идентификатор', 'name' => 'Наименование', 'ordernum' => 'Порядковый номер', // Добавьте описания для других ключевых полей ]; // Преобразование значений в более читаемый формат foreach ($results as &$row) { $row['is_nullable'] = ($row['is_nullable'] === 'YES') ? "Да" : "Нет"; $maxLen = !empty($row['character_maximum_length']) ? $row['character_maximum_length'] : $row['numeric_precision']; $row['max_len'] = $maxLen ?? '-'; } // Группировка результатов по именам таблиц $grouped = []; foreach ($results as $row) { $grouped[$row['table_name']][] = $row; } // Формирование выводных данных в формате Markdown foreach ($grouped as $tableName => $rows) { $mdOutput[] = "## Таблица " . $tableName . "\n"; // Название таблицы $mdOutput[] = "**Описание**\n\n"; // Заголовок "Описание" $mdOutput[] = "| Название поля | Тип поля | По умолчанию | Может NULL | Макс. длина | Описание |\n"; // Заголовок таблицы $mdOutput[] = "|---------------|----------|--------------|------------|-------------|----------|\n"; // Разделитель $idRows = []; $otherRows = []; // Разделяем строки: сначала идем по строкам с id foreach ($rows as $row) { if (($row['column_name'] === 'id') || ($row['column_name'] === 'Id')) { array_unshift($idRows, $row); } elseif ((str_ends_with($row['column_name'], 'id')) || (str_ends_with($row['column_name'], 'Id')) || ($row['data_type'] === 'uuid')) { $idRows[] = $row; } else { $otherRows[] = $row; } } // Объединяем строки: сначала с id, затем остальные $sortedRows = array_merge($idRows, $otherRows); foreach ($sortedRows as $row) { $description = (str_ends_with($row['column_name'], 'id')) ? 'Идентификатор таблицы ' . substr($row['column_name'], 0, -2) : ($descriptions[$row['column_name']] ?? ''); // Формирование строки с данными для таблицы $mdOutput[] = "| " . ($row['column_name'] ?: '-') . " | " . ($row['data_type'] ?: '-') . " | " . ($row['column_default'] ?: '-') . " | " . ($row['is_nullable'] ?: '-') . " | " . ($row['max_len'] ?: '-') . " | " . $description . " |\n"; } $mdOutput[] = "\n"; // Переход на новую строку } // Сохранение результата в файл Markdown $mdFileName = 'database_structure_new.md'; file_put_contents($mdFileName, implode("", $mdOutput)); echo "Данные успешно сохранены в файл " . $mdFileName . "\n"; } catch (PDOException $e) { // Обработка ошибок подключения или выполнения запроса echo "Ошибка подключения или выполнения: " . $e->getMessage(); }
Описание второго скрипта: анализ изменений в структуре базы данных
Этот скрипт предназначен для анализа изменений в структуре базы данных, представленной в формате Markdown, с выделением добавленных, удалённых и изменённых таблиц. Он выполняет несколько этапов:
-
Анализ структуры таблиц из новых и старых файлов
-
Определение изменений: добавленных, удалённых и обновлённых строк
-
Генерация файлов с изменениями
Этапы работы скрипта:
1. Функции для работы с таблицами
addRowsInTable — функция принимает массив данных о поле таблицы и возвращает строку в формате Markdown.addNameAndHeaderTable — функция добавляет заголовок таблицы и её описание.sortedRows — сортирует массив строк таблицы так, чтобы поля id или заканчивающиеся на id шли в начале.
2. Парсинг данных из Markdown
markDownToArray — функция преобразует содержимое Markdown-файла в ассоциативный массив.
3. Основной блок скрипта
-
Чтение новых и старых файлов
• Считывание нового файла:
$newMdContent = file_get_contents(‘database_structure_new.md’);
list($newTables,$newDescriptions) = markDownToArray($newMdContent);
• Считывание старых файлов и их обработка.
-
Сравнение данных
• Вычисление удалённых, добавленных и изменённых таблиц.
• Пример: сравнение массивов $oldTables и $newTables.
-
Сохранение результатов
• Генерация Markdown-файлов для каждой категории изменений (новые, удалённые, изменённые таблицы).
Итог
/** * Добавляем строки в таблицу * * @param array $row: строка * @return string: строки таблицы */ function addRowsInTable(array $row): string { return "| " . ($row['Название поля'] ?? '-') . " | " . ($row['Тип поля'] ?? '-') . " | " . ($row['По умолчанию'] ?? '-') . " | " . ($row['Может NULL'] ?? '-') . " | " . ($row['Макс. длина'] ?? '-') . " | " . ($row['Описание'] ?? '-') . " |\n"; } /** * Добавляем название и описание таблицы * * @param $tableName: название таблицы * @param string $descriptions: описание * @return string: первые 5 строк таблицы */ function addNameAndHeaderTable(string $tableName, string $descriptions = "**Описание**"): string { return "## Таблица " . $tableName . "\n" . $descriptions . "\n\n" . "| Название поля | Тип поля | По умолчанию | Может NULL | Макс. длина | Описание |\n" . "|---------------|----------|--------------|------------|-------------|----------|\n"; } /** * Сортировка строк таблицы по id * * @param $rows: строка * @return array: отсортированная таблица */ function sortedRows(array $rows): array { $idRows = []; $otherRows = []; // Разделение строк по наличию id на конце foreach ($rows as $row) { if (($row['Название поля'] === 'id') || ($row['Название поля'] === 'Id')) { array_unshift($idRows, $row); } elseif ((str_ends_with($row['Название поля'], 'id')) || (str_ends_with($row['Название поля'], 'Id')) || ($row['Тип поля'] === 'uuid')) { $idRows[] = $row; } else { $otherRows[] = $row; } } // Объединяем строки, начиная с id строк return array_merge($idRows, $otherRows); } /** * Преобразование MarkDown в ассоциативный массив * * @param string $mdContent: таблицы файла * @return array[]: ассоциативные массивы с таблицами и их данными, описанием */ function markDownToArray(string $mdContent): array { $tables = []; $descriptions = []; // Используем регулярные выражения для поиска всех таблиц $pattern = '/(## Таблица .+?)(?=\n## Таблица |\z)/s'; preg_match_all($pattern, $mdContent, $matches); // Обработка каждой найденной таблицы foreach ($matches[0] as $table) { $lines = array_map('trim', explode("\n", trim($table))); // Если таблица короче 4 строк - пропускаем if (count($lines) < 4) { continue; } // Заголовок таблицы и описание $tableName = str_replace(["## Таблица ", " +"], '', $lines[0]); $description = trim($lines[1]); // Описание таблицы // Извлечение заголовков $headers = array_filter(array_map('trim', explode('|', $lines[3]))); // Проверка корректности заголовков if (empty($headers)) { echo "Ошибка: не удалось извлечь заголовки для таблицы " . $tableName . "\n"; continue; } // Извлечение строк данных $rows = []; for ($i = 5; $i < count($lines); $i++) { if (!empty(trim($lines[$i]))) { $row = array_map('trim', explode('|', $lines[$i])); // удаляем пробелы if (count($row) > 1) { // Создаем ассоциативный массив, используя заголовки как ключи $rows[] = array_combine($headers, array_slice($row, 1, -1)); } } } // Добавление таблиц и их описаний $tables[$tableName] = $rows; $descriptions[$tableName] = $description; } return [$tables, $descriptions]; } // Извлекаем данные из последнего обновления БД $newMdContent = file_get_contents('database_structure_new.md'); list($newTables, $newDescriptions) = markDownToArray($newMdContent); // Путь к папке со старыми файлами $oldFilesDirectory = './files'; $allOldTables = []; $allOldData = []; $oldDescriptions = []; // Обработка каждого старого файла отдельно try { $files = @scandir($oldFilesDirectory); if ($files === false) { throw new Exception("Произошла ошибка: не удалось найти директорию."); } // Удаление спец. пунктов '.' и '..' для проверки на пустоту $filteredFiles = array_diff($files, array('.', '..')); if (count($filteredFiles) === 0) { throw new Exception("Произошла ошибка: директория пуста."); } foreach ($files as $fileName) { if (str_ends_with($fileName, '.md')) { $filePath = $oldFilesDirectory . '/' . $fileName; $oldMdContent = file_get_contents($filePath); list($oldTables, $oldDescriptions) = markDownToArray($oldMdContent); $updatedTables = []; $updatedDescriptions = []; // Сохраняем названия всех старых таблиц foreach ($oldTables as $oldTableName => $oldRows) { $allOldTables[] = $oldTableName; $oldGroup = $oldRows; // Совмещение таблиц, если таблица есть в обоих наборах данных if (isset($newTables[$oldTableName])) { $newGroup = $newTables[$oldTableName]; // Выявляем только новые строки, отсутствующие в строй таблице $newRows = array_udiff($newGroup, $oldGroup, function ($new, $old) { return strcmp($new['Название поля'], $old['Название поля']); }); // Объединяем старую и новую таблицы $combinedTable = array_merge($oldGroup, $newRows); $updatedTables[$oldTableName] = $combinedTable; } else { $updatedTables[$oldTableName] = $oldGroup; } $updatedDescriptions[$oldTableName] = $oldDescriptions[$oldTableName] ?? ''; // Здесь мы добавляем данные каждой таблицы из старых файлов в $allOldData $allOldData[] = [ 'name' => $oldTableName, 'data' => $oldRows, 'description' => $oldDescriptions[$oldTableName] ?? '' ]; } // Подготовка выходного содержимого $finalMdOutput = []; foreach ($updatedTables as $tableName => $data) { $finalMdOutput[] = addNameAndHeaderTable($tableName, $updatedDescriptions[$tableName]); $sortedRows = sortedRows($data); foreach ($sortedRows as $row) { $finalMdOutput[] = addRowsInTable($row); } $finalMdOutput[] = "\n"; } $updatedFileName = $oldFilesDirectory . '/updated_' . $fileName; file_put_contents($updatedFileName, implode("", $finalMdOutput)); echo "Изменения сохранены в файл " . $updatedFileName . "\n"; } } // Также найдём и сохраним уникальные таблицы, которые были удалены и недостающие в новом (УДАЛЕННЫЕ ТАБЛИЦЫ) $removedTablesOutput = []; // Итерация по всем старым таблицам, чтобы найти те, которые отсутствуют в обновлении foreach ($allOldData as $oldTable) { $tableName = $oldTable['name']; // Проверяем, присутствует ли таблица в новых данных if (!isset($newTables[$tableName])) { // Таблица была удалена, добавляем её в выходной массив $removedTablesOutput[] = addNameAndHeaderTable($tableName, $oldTable['description']); $sortedRows = sortedRows($oldTable['data']); // Добавляем каждую строку данных таблицы foreach ($sortedRows as $row) { $removedTablesOutput[] = addRowsInTable($row); } $removedTablesOutput[] = "\n"; } } // Записываем удаленные таблицы в файл $removedMdFileName = 'removed_old_structure.md'; file_put_contents($removedMdFileName, implode("", $removedTablesOutput)); echo "Таблицы, которые были удалены, сохранены в файл " . $removedMdFileName . "\n"; // Теперь найдем таблицы, которые уникальны для нового и не присутствуют ни в одном из старых (НОВЫЕ ТАБЛИЦЫ) $newTablesOutput = []; // Итерация по всем новым таблицам, чтобы найти те, которые отсутствуют в старых данных foreach ($newTables as $tableName => $newRows) { // Проверяем, присутствует ли таблица в старых данных if (!in_array($tableName, $allOldTables)) { // Таблица была удалена, добавляем её в выходной массив $newTablesOutput[] = addNameAndHeaderTable($tableName); // Добавляем каждую строку данных таблицы foreach ($newRows as $row) { $newTablesOutput[] = addRowsInTable($row); } $newTablesOutput[] = "\n"; } } // Запись новых данных в файл $newMdFileName = 'add_new_structure.md'; file_put_contents($newMdFileName, implode("", $newTablesOutput)); echo "Новые таблицы сохранены в файл " . $newMdFileName . "\n"; } catch (Exception $exception) { echo $exception->getMessage();
Преимущества и особенности использования скрипта
-
Автоматизация: исключает ручное сравнение структур.
-
Гибкость: поддерживает работу с несколькими файлами.
-
Чёткая структура: отчёты в формате Markdown легко читать и анализировать.
Внедрение скриптов в проект
Скрипты для анализа и описания базы данных можно упростить в использовании, интегрировав их в проект. Это особенно удобно, если проект использует фреймворк, поддерживающий команды из консоли, например, Symfony. Если же такого фреймворка нет, можно создать свою CLI-команду. Ниже приведены оба варианта внедрения.
1. Интеграция в проект с использованием Symfony
1.1 Создание команды
Для создания команды используем встроенную возможность Symfony. Выполняем команду:
php bin/console make:command app:describe-database
1.2 Реализация команды
После выполнения команды откроется файл сгенерированного класса команды, например:
src/Command/DescribeDatabaseCommand.php.
Добавляем логику скрипта в метод execute():
namespace App\Command; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; #[AsCommand( name: 'app:describe-database', description: 'Генерирует описание структуры базы данных', )] class DescribeDatabaseCommand extends Command { protected function execute(InputInterface $input, OutputInterface $output): int { // Вставляем вашу логику для описания базы данных $output->writeln('Начало описания базы данных...'); // Здесь можно перенести основной код скрипта // Например: describeDatabase(); // Вызов основной функции, содержащей логику анализа $output->writeln('Описание базы данных завершено.'); return Command::SUCCESS; } } // Функция для описания базы данных function describeDatabase() { echo "Описание структуры базы данных...\n"; // Логика анализа структуры базы echo "Описание завершено.\n"; }
1.3 Запуск команды
Для выполнения команды из консоли достаточно запустить:
php bin/console app:describe-database
2. Создание независимой CLI-команды
Если проект не использует фреймворк с поддержкой консольных команд, можно реализовать собственный PHP-скрипт.
2.1 Создание скрипта
Создаём файл describe_db.php в корне проекта со следующим содержимым:
#!/usr/bin/env php <?php // Основная логика скрипта function describeDatabase() { echo "Начало описания базы данных...\n"; // Вставьте вашу логику скрипта echo "Описание базы данных завершено.\n"; } // Запуск функции описания describeDatabase();
2.2 Добавление разрешений на выполнение
Для запуска скрипта как исполняемого файла необходимо сделать его исполняемым:
chmod +x describe_db.php
2.3 Запуск скрипта
Теперь скрипт можно запускать из командной строки:
./describe_db.php
Важные замечания
Шебанг (shebang)
Первая строка #!/usr/bin/env php указывает, что для выполнения скрипта нужно использовать интерпретатор PHP. Это работает только в Unix-подобных системах (Linux, macOS). В Windows эта строка игнорируется, но не вызывает ошибок.
Платформенная совместимость
Если проект используется на Windows, запуск скрипта будет отличаться. Вместо выполнения через ./describe_db.php нужно использовать:
php describe_db.php
Преимущества подхода
o Вариант с Symfony автоматически интегрируется в существующую архитектуру проекта.
o Независимый CLI-скрипт подходит для проектов без фреймворка, а также для системных утилит.
Оба варианта позволяют быстро запускать скрипты анализа структуры базы данных и предоставляют удобство в использовании. Выбирайте подход в зависимости от архитектуры вашего проекта!
Заключение
Интеграция скриптов для анализа и описания базы данных в проект значительно повышает удобство их использования и автоматизирует процессы, что особенно важно при работе с большими и динамично изменяющимися базами данных. Использование таких скриптов позволяет ускорить процессы анализа структуры базы, генерации документации и обеспечения согласованности данных.
ссылка на оригинал статьи https://habr.com/ru/articles/883616/
Добавить комментарий