Commit 62166c94 authored by Bertrand Gauthier's avatar Bertrand Gauthier
Browse files

Exécution de script SQL : amélioration, refactorisation. Mais plus possible de...

Exécution de script SQL : amélioration, refactorisation. Mais plus possible de parcourir un dossier.
parent 2348eb48
Pipeline #4811 failed with stage
in 9 seconds
......@@ -219,10 +219,11 @@ class Module implements
{
return [
// command
'run-sql --path= [--connection=]' => "Exécuter un script SQL ou tous les scripts SQL présents dans un répertoire",
'run-sql-script --path= [--logfile=] [--connection=]' => "Exécuter un script SQL",
// parameters
['--path', "Requis. Chemin vers le script SQL ou le répertoire contenant les scripts à exécuter"],
['--connection', "Facultatif. Identifiant de la connexion Doctrine. Par défaut 'orm_default'"],
['--path', "Requis. Chemin vers le script SQL à exécuter."],
['--logfile', "Facultatif. Chemin du fichier des logs d'exécution du script. Par défaut, il est généré."],
['--connection', "Facultatif. Identifiant de la connexion Doctrine. Par défaut : 'orm_default'."],
];
}
}
......@@ -222,10 +222,10 @@ return [
'run-sql' => [
'type' => Simple::class,
'options' => [
'route' => 'run-sql --path= [--connection=]',
'route' => 'run-sql-script --path= [--logfile=] [--connection=]',
'defaults' => [
'controller' => ConsoleController::class,
'action' => 'run-sql',
'action' => 'runSQLScript',
],
],
],
......
......@@ -3,6 +3,7 @@
namespace UnicaenApp\Controller;
use Doctrine\DBAL\Connection;
use Exception;
use UnicaenApp\Exception\RuntimeException;
use UnicaenApp\Service\SQL\RunSQLServiceAwareTrait;
use Zend\Log\Filter\Priority;
......@@ -21,16 +22,16 @@ class ConsoleController extends AbstractConsoleController
use RunSQLServiceAwareTrait;
/**
* @throws \Exception
* @throws Exception
*/
public function runSqlAction()
public function runSQLScriptAction()
{
$path = $this->params('path');
$connection = $this->params('connection', 'orm_default');
$logFilepath = $this->params('logfile');
$serviceName = "doctrine.connection.$connection";
$logger = $this->createLogger();
$this->runSQLService->setLogger($logger);
$logger->info("### Exécution de scripts SQL ###");
$logger->info(date_format(date_create(), 'd/m/Y H:i:s'));
......@@ -41,26 +42,12 @@ class ConsoleController extends AbstractConsoleController
/** @var Connection $conn */
$conn = $this->serviceLocator->get($serviceName);
$logger->info("Utilisation de la connexion '$serviceName'.");
$result = $this->runSQLService->runSQL($path, $conn);
$logger->info("Connexion : '$serviceName'");
if ($exception = $result->getException()) {
$logger->info("Une erreur a été rencontrée!");
$logger->info($exception->getMessage());
if ($previous = $exception->getPrevious()) {
$logger->info($previous->getMessage());
}
$logger->info($exception->getTraceAsString());
} else {
$logger->info("Exécution réalisée avec succès.");
}
$startDate = $result->getStartDate();
$endDate = $result->getEndDate();
$diffDate = date_diff($startDate, $endDate);
$this->runSQLService->setLogger($logger);
$result = $this->runSQLService->runSQLScript($path, $conn, $logFilepath);
$logger->info($diffDate->format('%i min %s sec.'));
$logger->info("Durée : " . $result->getDurationInSec() . " sec");
}
/**
......
......@@ -2,9 +2,10 @@
namespace UnicaenApp\Process;
use DateTime;
use DateInterval;
use Exception;
use UnicaenApp\Exception\LogicException;
use UnicaenApp\Exception\RuntimeException;
use Zend\Log\Formatter\Simple;
use Zend\Log\Logger;
use Zend\Log\LoggerInterface;
......@@ -18,6 +19,7 @@ use Zend\Log\Writer\WriterInterface;
* - une exception éventuelle en cas d'erreur.
* - une date de début d'exécution
* - une date de fin d'exécution
* - la calcul de la durée d'exécution
*
* @author Unicaen
*/
......@@ -44,14 +46,14 @@ abstract class ProcessResult
private $exception;
/**
* @var DateTime
* @var float
*/
private $startDate;
private $startMicrotime;
/**
* @var DateTime
* @var float
*/
private $endDate;
private $endMicrotime;
/**
* RunSqlResult constructor.
......@@ -66,7 +68,7 @@ abstract class ProcessResult
$this->logWriter = new Stream($this->logStream);
$this->logWriter->setFormatter($formatter);
$this->setStartDate(date_create('now'));
$this->setStartMicrotime();
}
public function __destruct()
......@@ -134,35 +136,50 @@ abstract class ProcessResult
}
/**
* @return DateTime
* @return float
*/
public function getStartDate(): DateTime
public function getStartMicrotime()
{
return $this->startDate;
return $this->startMicrotime;
}
/**
* @param DateTime $startDate
* @param float|null $startMicrotime
*/
public function setStartDate(DateTime $startDate = null)
public function setStartMicrotime($startMicrotime = null)
{
$this->startDate = $startDate ?: date_create();
$this->startMicrotime = $startMicrotime ?: microtime(true);
}
/**
* @return DateTime
* @return float
*/
public function getEndDate(): DateTime
public function getEndMicrotime()
{
return $this->endDate;
return $this->endMicrotime;
}
/**
* @param DateTime $endDate
* @param float|null $endMicrotime
*/
public function setEndDate(DateTime $endDate = null)
public function setEndMicrotime($endMicrotime = null)
{
$this->endDate = $endDate ?: date_create();
$this->endMicrotime = $endMicrotime ?: microtime(true);
}
/**
* @return float
*/
public function getDurationInSec()
{
$startMicrotime = $this->getStartMicrotime();
$endMicrotime = $this->getEndMicrotime();
if ($startMicrotime === null || $endMicrotime === null) {
throw new RuntimeException("Impossible de calculer la durée car l'instant de début ou de fin est null !");
}
return $endMicrotime - $startMicrotime;
}
/**
......
<?php
namespace UnicaenApp\Service\SQL;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DBALException;
use Exception;
use UnicaenApp\Exception\RuntimeException;
use Zend\Log\Logger;
use Zend\Log\LoggerAwareTrait;
class RunSQLProcess
{
use LoggerAwareTrait;
/**
* @var string
*/
private $scriptPath;
/**
* @var string
*/
private $logFilePath;
/**
* @var Connection
*/
private $connection;
/**
* @var RunSQLQueryStack
*/
private $queryStack;
/**
* @param string $scriptPath
* @return self
*/
public function setScriptPath($scriptPath)
{
$this->scriptPath = $scriptPath;
return $this;
}
/**
* @param Connection $connection
* @return RunSQLProcess
*/
public function setConnection(Connection $connection)
{
$this->connection = $connection;
return $this;
}
/**
* @param null|string $logFilePath
* @return self
*/
public function setLogFilePath($logFilePath = null)
{
$this->logFilePath = $logFilePath;
return $this;
}
/**
* Exécute dans la transaction courante toutes les instructions d'un script SQL.
*
* @return RunSQLResult
*/
public function executeScript()
{
$this->validateScriptPath();
$queries = $this->extractQueriesFromScript();
$this->logger->info("+ Exécution du script '$this->scriptPath'.");
$this->logger->info(sprintf("'--> Requêtes trouvées : %d", count($queries)));
$result = $this->executeQueries($queries);
$logFilePath = $this->logQueries();
$this->logger->info("'--> Logs : " . $logFilePath);
$this->logger->info($result->isSuccess() ? "Exécution terminée avec succès." : "Une erreur a été rencontrée!");
return $result;
}
/**
* Exécute dans la transaction courante toutes les instructions d'un script SQL.
*
* @param string $query
* @return RunSQLResult
*/
public function executeQuery($query)
{
$this->logger->info("+ Exécution d'une requête.");
$result = $this->executeQueries([$query]);
$logFilePath = $this->logQueries();
$this->logger->info("'--> Logs : " . $logFilePath);
$this->logger->info($result->isSuccess() ? "Exécution terminée avec succès." : "Une erreur a été rencontrée!");
return $result;
}
private function validateScriptPath()
{
if (is_dir($this->scriptPath)) {
throw new RuntimeException("Le fichier '$this->scriptPath' spécifié est un répertoire");
}
if (!is_readable($this->scriptPath)) {
throw new RuntimeException("Le fichier '$this->scriptPath' n'est pas accessible");
}
}
/**
* Extrait les requêtes contenues dans le script.
*
* @return string[]
*/
protected function extractQueriesFromScript()
{
$queries = array_filter(array_map('trim', explode('/', file_get_contents($this->scriptPath))));
if (count($queries) === 0) {
throw new RuntimeException("Aucune requête trouvée dans le script '$this->scriptPath'");
}
return $queries;
}
/**
* Exécute dans la transaction courante les requêtes spécifiées.
*
* @param string[] $queries
* @return RunSQLResult
*/
private function executeQueries($queries)
{
$result = new RunSQLResult();
$result->attachLogger($this->logger);
$result->setIsSuccess(true);
$this->queryStack = new RunSQLQueryStack();
try {
foreach ($queries as $q) {
$this->queryStack->startQuery($q);
$this->connection->executeQuery($q);
$this->queryStack->stopQuery();
}
} catch (DBALException $e) {
$result->setIsSuccess(false);
$result->setException($e);
$this->queryStack->stopQueryWithException($e);
}
$result->setEndMicrotime();
return $result;
}
/**
* @return string
*/
private function logQueries()
{
$logFilePath = $this->generateLogFilePath();
$logger = new Logger();
$logger->addWriter('stream', null, ['stream' => $logFilePath]);
foreach ($this->queryStack->getQueries() as $query) {
$logger->info("Requête : " . $query['sql']);
if (isset($query['exception'])) {
/** @var Exception $exception */
$exception = $query['exception'];
$logger->info($exception->getMessage());
$logger->info($exception->getTraceAsString());
}
$logger->info("Temps d'exécution : " . $query['executionMS'] . " sec");
$logger->info("---------------------------------------------------------------------------------------");
}
return $logFilePath;
}
/**
* @return string
*/
private function generateLogFilePath()
{
if ($this->logFilePath !== null) {
return $this->logFilePath;
}
if ($this->scriptPath) {
$this->logFilePath = sys_get_temp_dir() . '/' . basename($this->scriptPath) . '.log';
} else {
$this->logFilePath = sys_get_temp_dir() . '/' . uniqid('unicaen-app-run-sql-') . '.log';
}
return $this->logFilePath;
}
}
\ No newline at end of file
<?php
namespace UnicaenApp\Service\SQL;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Logging\DebugStack;
class RunSQLQueryStack extends DebugStack
{
public function stopQueryWithException(DBALException $exception)
{
$this->stopQuery();
if ($this->enabled) {
$this->queries[$this->currentQuery]['exception'] = $exception;
}
}
/**
* @return array
*/
public function getQueries(): array
{
return $this->queries;
}
}
\ No newline at end of file
......@@ -11,4 +11,5 @@ use UnicaenApp\Process\ProcessResult;
*/
class RunSQLResult extends ProcessResult
{
}
\ No newline at end of file
......@@ -3,10 +3,7 @@
namespace UnicaenApp\Service\SQL;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DBALException;
use UnicaenApp\Exception\RuntimeException;
use Zend\Log\LoggerAwareTrait;
use Zend\Stdlib\Glob;
/**
* Service permettant d'exécuter des scripts SQL dans une base de données.
......@@ -18,127 +15,67 @@ class RunSQLService
use LoggerAwareTrait;
/**
* Exécute le ou les scripts spécifiés par leurs chemins ou par le chemin de leur répertoire.
* Exécute le script spécifié par son chemin.
*
* NB: chaque script est exécuté dans une nouvelle transaction.
* NB: dans un script, les requêtes sont exécutées une par une.
* NB: Les requêtes doivent pouvoir être extraites afin de les exécuter une par une. Pour cela, vous devez donc
* utiliser dans vos scripts SQL le caractère de fin '/' et mettre un ';' seulement où c'est indispensable.
*
* Le formattage des requêtes SQL dans chaque script n'est pas anodin !
* Exemple de script SQL respectant le formattage requis :
* Exemple de script Oracle acceptable :
* <code>
* insert into API_LOG (ID, REQ_URI, REQ_START_DATE, REQ_END_DATE, REQ_STATUS, REQ_RESPONSE, REQ_TABLE)
* select API_LOG_ID_SEQ.nextval, 'xxxxx', sysdate, sysdate, 'test', 'hello!!', 'TEST' from dual
* /
*
```sql
---------------------------------
insert into API_LOG(ID, REQ_URI, REQ_START_DATE, REQ_END_DATE, REQ_STATUS, REQ_RESPONSE, REQ_ETABLISSEMENT, REQ_TABLE)
select API_LOG_ID_SEQ.nextval, 'xxxxx', sysdate, sysdate, 'test', 'hello!!', 'UCN', 'TEST' from dual
/
declare
d date;
begin
select sysdate into d from dual;
insert into API_LOG(ID, REQ_URI, REQ_START_DATE, REQ_END_DATE, REQ_STATUS, REQ_RESPONSE, REQ_ETABLISSEMENT, REQ_TABLE)
select API_LOG_ID_SEQ.nextval, 'yyyyy', d, d, 'test', 'hello!!', 'UCN', 'TEST' from dual;
end;
/
create index TEST_index on FICHIER (HISTO_MODIFICATION ASC)
/
drop index TEST_index
/
---------------------------------
```
* declare
* d date;
* begin
* select sysdate into d from dual;
* insert into API_LOG(ID, REQ_URI, REQ_START_DATE, REQ_END_DATE, REQ_STATUS, REQ_RESPONSE, REQ_TABLE)
* select API_LOG_ID_SEQ.nextval, 'yyyyy', d, d, 'test', 'hello!!', 'TEST' from dual;
* end;
* /
*
* create index TEST_index on FICHIER (HISTO_MODIFICATION ASC)
* /
* drop index TEST_index
* /
* </code>
*
* @param string $path Chemin absolu d'un script SQL
* @param Connection $conn Connexion Doctrine à la base de données
* @param string $logFilepath Chemin du fichier de log à produire
*
* @param string $path Chemin absolu d'un script SQL ou d'un répertoire contenant des scripts *.[sS][qQ][lL]
* @param Connection $conn Connexion Doctrine à la base de données
* @return RunSQLResult
*/
public function runSQL($path, Connection $conn)
public function runSQLScript($path, Connection $conn, $logFilepath = null)
{
$result = new RunSQLResult();
$result->attachLogger($this->logger);
$result->setIsSuccess(true);
try {
if (is_dir($path)) {
$this->executeDir($path, $conn);
} else {
$this->executeScript($path, $conn);
}
} catch (DBALException $e) {
$result->setIsSuccess(false);
$result->setException($e);
}
$result->setEndDate(date_create('now'));
return $result;
$process = new RunSQLProcess();
$process
->setScriptPath($path)
->setConnection($conn)
->setLogFilePath($logFilepath)
->setLogger($this->logger);
return $process->executeScript();
}
/**
* Exécute dans une nouvelle transaction chaque script SQL présent dans le répertoire spécifié.
* Exécute une requête.
*
* @param string $dirpath
* @param Connection $conn
* @return array
* @throws DBALException
*/
private function executeDir($dirpath, Connection $conn)
{
$this->logger->info("Lecture du répertoire '$dirpath'...");
// normalize paths
$paths = Glob::glob($dirpath . '/*.[sS][qQ][lL]');
if (empty($paths)) {
throw new RuntimeException("Le répertoire '$dirpath' ne contient aucun script *.[sS][qQ][lL]");
}
foreach ($paths as $p) {
if (is_dir($p)) {
//throw new RuntimeException("Le répertoire '$dirpath' ne doit pas contenir de sous-répertoire");
continue;
}
$this->executeScript($p, $conn);
}
return $paths;
}
/**
* Exécute dans une nouvelle transaction toutes les instructions d'un script SQL.
* @param string $query Requête à exécuter
* @param Connection $conn Connexion à la base de données
* @param string $logFilepath Chemin du fichier de log à produire
*
* @param string $scriptPath
* @param Connection $conn
* @return array
* @throws DBALException
* @return RunSQLResult
*/
private function executeScript($scriptPath, Connection $conn)
public function runSQLQuery($query, Connection $conn, $logFilepath = null)
{
if (is_dir($scriptPath)) {
throw new RuntimeException("Le fichier '$scriptPath' spécifié est un répertoire");
}
if (!is_readable($scriptPath)) {
throw new RuntimeException("Le fichier '$scriptPath' n'est pas accessible");
}
$scriptContent = file_get_contents($scriptPath);
$queries = array_filter(array_map('trim', explode('/', $scriptContent)));
$this->logger->info("+ Exécution du script '$scriptPath'.");