From 57cfe80611d1e5f935652eab0ba39d27141c08a6 Mon Sep 17 00:00:00 2001 From: Florentin L'Homme <florentin.lhomme@unicaen.fr> Date: Wed, 15 May 2019 18:13:47 +0200 Subject: [PATCH] =?UTF-8?q?D=C3=A9but=20refonte=20de=20la=20documentation?= =?UTF-8?q?=20(en=20(r)accord=20avec=20le=20nouveau=20fonctionnement=20d'U?= =?UTF-8?q?nicaenDbImport)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-old.md | 414 +++++++++++++++ README.md | 470 +++++++----------- documentation/README.md | 9 + .../fonctionnement_synchro_distant.drawio | 1 + .../fonctionnement_synchro_distant.png | Bin 0 -> 15418 bytes .../fonctionnement_synchro_locale.drawio | 1 + .../fonctionnement_synchro_locale.png | Bin 0 -> 15829 bytes 7 files changed, 612 insertions(+), 283 deletions(-) create mode 100644 README-old.md create mode 100644 documentation/README.md create mode 100644 documentation/fonctionnement_synchro_distant.drawio create mode 100644 documentation/fonctionnement_synchro_distant.png create mode 100644 documentation/fonctionnement_synchro_locale.drawio create mode 100644 documentation/fonctionnement_synchro_locale.png diff --git a/README-old.md b/README-old.md new file mode 100644 index 0000000..997175d --- /dev/null +++ b/README-old.md @@ -0,0 +1,414 @@ +UnicaenDbImport +=============== + + +Ce module a pour but de réaliser l'import de données d'une table (ou d'un select) d'une base de données source vers une +table d'une base de données destination. + +Concrètement : + + - Si la source contient un enregistrement qui n'existe pas dans la destination, il est ajouté dans cette dernière. + - Si la source contient un enregistrement qui existe aussi dans la destination avec les mêmes valeurs de colonnes, + rien n'est fait. + - Si la source contient un enregistrement qui existe aussi dans la destination mais avec des valeurs de colonnes + différentes, les colonnes de la destination sont mises à jour en conséquence. + - Si la source ne contient pas un enregistrement qui existe dans la destination, l'enregistrement destination est historisé + (i.e. marqué "supprimé"). + - Si la source contient un enregistrement qui existe aussi dans la destination mais marqué "supprimé", ce dernier est + dé-historisé. + +Les enregistrements source et les enregistrements destination doivent avoir un identifiant unique en commun permettant +de les rapprocher : on l'appellera "code source" (cf. paramètre de config `source_code_column`). +**Cet identifiant doit être de type chaîne de caractères.** + + + +La différence avec le module UnicaenImport ? +-------------------------------------------- + +Comme son nom ne l'indique pas, UnicaenImport fonctionne uniquement entre +2 bases de données **Oracle** ; et la synchronisation est faite entièrement par le SGBD (ou presque). + +UnicaenDbImport fonctionne en majeure partie en PHP tout en déléguant au maximum au SGBD ce qu'il sait bien faire +(`select FULL OUTER JOIN` pour le différentiel entre données source et destination, fonction pour l'alimentation du registre +d'import, etc.) Par conséquent, il est possible et souhaitable de l'enrichir pour implémenter une synchronisation vers +différentes plateformes de base de données destination. Les plateformes de base de données destination suivantes sont +implémentées : + - PostgreSQL + +*NB: En revanche toutes les plateformes de base de données source sont supportées car on se contente d'y faire un select +à l'aide de l'ORM Doctrine 2 pour alimenter une table intermédiaire.* + + +Installation +------------ + + composer require unicaen/unicaen-db-import + + +Configuration +------------- + +- Récupérer les fichiers config de distribution : +```bash +cp -n vendor/unicaen/db-import/config/unicaen-db-import.global.php.dist config/autoload/unicaen-db-import.global.php +cp -n vendor/unicaen/db-import/config/unicaen-db-import.local.php.dist config/autoload/unicaen-db-import.local.php +``` + +- Adapter leur contenu à vos besoins. + + +Utilisation +----------- + +Le module fournit une ligne de commande pour : + + - lancer un import par son nom, exemple : + + ```bash + php public/index.php run import --name "IMPORT_PFI_SIFAC" + ``` + + - lancer tous les imports : + + ```bash + php public/index.php run import --all + ``` + +*NB: Cette commande exécute la synchronisation une fois. Pour synchroniser en permanence la base de données destination, +il faut programmer le lancement périodique de cette commande à l'aide de CRON (cf. plus bas pour un exemple).* + + +Contraintes +----------- + +### Identifiant commun + +L'identifiant unique commun des enregistrements source et destination doit être de type **chaîne de caractères**. + +### Source de type `'select'` + +Dans le cas d'une *source* de type `'select'`, il est nécessaire de convertir chaque colonne en +**chaîne de caractères** dans le `select`. + +- Pour une colonne de type "date", `TO_CHAR(nom_colonne,'YYYY-MM-DD')` permet de convertir + en chaînes de caractères dans un format quasi universel ; +- Pour une colonne de type numérique, `nom_colonne||''` permet par exemple de convertir + entiers et décimaux en chaînes de caractères. + +Il est également nécessaire de spécifier dans la config de la *destination* la fonction de conversion à appliquer +à chaque colonne pour la convertir en chaîne de caractères (cf. paramètre de config destination `columns_to_char`). + +### Source de type `'table'` + +Dans le cas d'une *source* de type `'table'`, il est nécessaire de spécifier dans la config de la +*destination* la fonction de conversion à appliquer à chaque colonne pour la convertir en chaîne de caractères +(cf. paramètre de config destination `columns_to_char`). + +### Table destination + +La *table destination* doit obligatoirement posséder les colonnes d'historique `created_on`, `updated_on` +et `deleted_on`. Exemple avec PostgreSQL : +```sql +ALTER TABLE TABLE_DESTINATION ADD COLUMN created_on TIMESTAMP(0) WITH TIME ZONE DEFAULT LOCALTIMESTAMP(0) NOT NULL; +ALTER TABLE TABLE_DESTINATION ADD COLUMN updated_on TIMESTAMP(0) WITH TIME ZONE; +ALTER TABLE TABLE_DESTINATION ADD COLUMN deleted_on TIMESTAMP(0) WITH TIME ZONE; +``` + + +Exemple de mise en oeuvre +------------------------- + +Import des programmes de financement SIFAC depuis une base Oracle vers une table PostgreSQL. Les données sources sont +"extraites" grâce à un select. + +- Configuration globale du module : + +`unicaen-db-import.global.php` +```php +return [ + /** + * Configuration du module UnicaenDbImport. + */ + 'import' => [ + /** + * Liste des imports. + */ + 'imports' => [ + [ + /** + * Petit nom (unique) de l'import. + */ + 'name' => 'Import_PFI_SIFAC', + + /** + * Configuration de la source de données à importer : + * - 'name' : petit nom (unique) de la source + * - 'table' : nom de la table source contenant les données à importer + * - 'select' : select SQL de mise en forme des données source à importer (NB: antinomique avec 'table') + * - 'connection' : identifiant de la connexion Doctrine à la base de données source + * - 'source_code_column' : nom de la colonne dans la table/vue source contenant l'identifiant unique + * - 'columns' : liste ordonnée des noms des colonnes à prendre en compte dans la table/vue source + */ + 'source' => [ + 'name' => 'PFI SIFAC', + 'connection' => 'doctrine.connection.orm_oracle', + 'source_code_column' => 'CODE', + 'columns' => ['LIBELLE', 'FLECHE', 'DEBUT_VALIDITE', 'FIN_VALIDITE'], + 'select' => <<<'EOT' +select distinct + MEASURE CODE, + SHORT_DESC LIBELLE, + ZFLECHE FLECHE, + to_char(TO_DATE(VALID_FROM,'YYYYMMDD'),'YYYY-MM-DD') DEBUT_VALIDITE, + to_char(TO_DATE(VALID_TO,'YYYYMMDD'),'YYYY-MM-DD') FIN_VALIDITE + from ( + SELECT + SAPSR3.FMMEASURE.MEASURE, + SAPSR3.FMMEASURET.SHORT_DESC, + SAPSR3.FMMEASURE.ZFLECHE, + SAPSR3.FMMEASURE.VALID_FROM, + SAPSR3.FMMEASURE.VALID_TO + FROM + SAPSR3.FMMEASURE, + SAPSR3.FMMEASURET + WHERE + ( (SAPSR3.FMMEASURE.CLIENT in (select A.MANDT from SAPSR3.FM01H A where A.MANDT = '500') OR SAPSR3.FMMEASURE.CLIENT is null) + and (SAPSR3.FMMEASURE.FMAREA in (select A.FIKRS from SAPSR3.FM01H A where A.FIKRS = '1010') or SAPSR3.FMMEASURE.FMAREA is null) ) + AND ( SAPSR3.FMMEASURE.CLIENT=SAPSR3.FMMEASURET.CLIENT(+) ) + AND ( SAPSR3.FMMEASURE.FMAREA=SAPSR3.FMMEASURET.FMAREA(+) ) + AND ( SAPSR3.FMMEASURE.MEASURE=SAPSR3.FMMEASURET.MEASURE(+) ) +) +EOT + , + ], + + /** + * Forçage éventuel du nom de la table intermédiaire utilisée lorsque source et destination + * ne partagent pas la même connexion. NB: cette table intermédiaire est créée/peuplée/supprimée + * dans la base de données destination à chaque import. + * En l'absence de ce forçage, le nom de la table intermédiaire sera celui de la table destination + * préfixé par "src_". + */ + 'intermediate_table' => 'src_progfin', + + /** + * Suppression automatique de la table intermédiaire "src_" au début de l'import. + * Si la suppression automatique est désactivée, l'existence de la table intermédiaire au démarrage + * de l'import fera échouer l'import. + */ + 'intermediate_table_auto_drop' => false, + + /** + * Configuration de la destination des données importées : + * - 'name' : petit nom (unique) de la destination + * - 'table' : nom de la table destination vers laquelle les données sont importées + * - 'connection' : identifiant de la connexion Doctrine à la base de données destination + * - 'source_code_column' : nom de la colonne dans la table destination contenant l'identifiant unique + * - 'columns' : liste ordonnée des noms des colonnes importées dans la table destination + * - 'columns_to_char' : format sprintf nécessaire pour mettre des colonnes au format requis (string) + */ + 'destination' => [ + 'name' => 'PFI Zébu', + 'table' => 'progfin', + 'connection' => 'doctrine.connection.orm_default', + 'source_code_column' => 'code', + 'columns' => ['libelle', 'fleche', 'debut_validite', 'fin_validite'], + 'columns_to_char' => [ + 'debut_validite' => "TO_CHAR(%s,'YYYY-MM-DD')", // car colonne destination de type TIMESTAMP + 'fin_validite' => "TO_CHAR(%s,'YYYY-MM-DD')", // idem + ], + ], + ], + ], + ], +]; +``` + +*On doit renseigner le paramètre `intermediate_table` car les données source proviennent d'un select.* + +- Configuration locale du module : + +`unicaen-db-import.local.php` +```php +return [ + /** + * Configuration Doctrine minimum requise. + */ + 'doctrine' => [ + 'connection' => [ + 'orm_oracle' => [ + 'driverClass' => 'Doctrine\\DBAL\\Driver\\OCI8\\Driver', + 'params' => [ + 'host' => 'host.domain.fr', + 'port' => '1525', + 'user' => 'x', + 'password' => 'y', + 'dbname' => 'z', + 'charset' => 'AL32UTF8', + ], + 'eventmanager' => 'orm_oracle', + ], + 'orm_default' => [ + 'driverClass' => 'Doctrine\\DBAL\\Driver\\PDOPgSql\\Driver', + 'params' => [ + 'host' => 'host.domain.fr', + 'port' => '5432', + 'charset' => 'utf8', + 'dbname' => 'a', + 'user' => 'b', + 'password' => 'c', + + ], + ], + ], + 'eventmanager' => [ + 'orm_sifac' => [ + 'subscribers' => [ + \Doctrine\DBAL\Event\Listeners\OracleSessionInit::class, + ], + ], + ], + ], +]; +``` + +*NB: les paramètres de config de la connexion `orm_default` existent sans doute déjà dans la configuration de votre +application, il est bien-sûr inutile de les répéter dans la config du module unicaen/db-import.* + +- Configuration CRON pour lancer périodiquement la synchro : +```cron +# Du lundi au vendredi, entre 6h00 et 19h45, toutes les 15 minutes +*/15 6-19 * * 1-5 root /usr/bin/php /var/www/zebu-back/public/index.php run import --all 1> /tmp/zebu-cron.log 2>&1 +``` + +- Exemple de logs affichés par la commande : + + ``` +######################## IMPORTS ######################## + +### Import des PFI SIFAC ### +05/09/2017 16:22:08 +# delete : +UPDATE progfin SET deleted_on = LOCALTIMESTAMP(0) WHERE CODE = 'fddsfsd' ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = 'fddsfsd-99c4b652-9245-11e7-bddd-0242f725575b' ; +(1 instructions exécutées) +# insert : +INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('971UP009', 'Of Wisco', ' ', '2014-01-01', '2047-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '971UP009-99c4b652-9245-11e7-bddd-0242f725575b' ; +INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('977CA025', 'Screening de 5', ' ', '2016-07-21', '2018-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '977CA025-99c4b652-9245-11e7-bddd-0242f725575b' ; +INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('F950MEC1', 'MDE CAEN FONCTIONNEM', ' ', '2015-01-01', '2047-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = 'F950MEC1-99c4b652-9245-11e7-bddd-0242f725575b' ; +INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('F971CF03', 'UNIVERSITE DU HAVRE', ' ', '2015-01-01', '2047-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = 'F971CF03-99c4b652-9245-11e7-bddd-0242f725575b' ; +INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('014DU009', 'DU RDCM', ' ', '2016-01-01', '2047-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '014DU009-99c4b652-9245-11e7-bddd-0242f725575b' ; +INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('012RE001', 'COLLOQUE 50 ANS SC E', ' ', '2017-01-01', '2017-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '012RE001-99c4b652-9245-11e7-bddd-0242f725575b' ; +INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('012CH021', 'Typo-chronologie', ' ', '2017-01-01', '2017-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '012CH021-99c4b652-9245-11e7-bddd-0242f725575b' ; +INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('013PL002', 'CMABIO', ' ', '2016-01-01', '2047-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '013PL002-99c4b652-9245-11e7-bddd-0242f725575b' ; +INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('999CM003', '2016-AGRI-133', ' ', '2016-03-30', '2018-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '999CM003-99c4b652-9245-11e7-bddd-0242f725575b' ; +(9 instructions exécutées) +# undelete : +UPDATE progfin SET LIBELLE = 'TA 2017', FLECHE = ' ', DEBUT_VALIDITE = '2017-01-01', FIN_VALIDITE = '2047-12-31', updated_on = LOCALTIMESTAMP(0), deleted_on = null WHERE CODE = '913TA017' ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '913TA017-99c4b652-9245-11e7-bddd-0242f725575b' ; +UPDATE progfin SET LIBELLE = 'TA 2017', FLECHE = ' ', DEBUT_VALIDITE = '2017-01-01', FIN_VALIDITE = '2047-12-31', updated_on = LOCALTIMESTAMP(0), deleted_on = null WHERE CODE = '920TA017' ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '920TA017-99c4b652-9245-11e7-bddd-0242f725575b' ; +UPDATE progfin SET LIBELLE = 'PAIE DIU ADOLESCENTS', FLECHE = ' ', DEBUT_VALIDITE = '2017-01-01', FIN_VALIDITE = '2047-12-31', updated_on = LOCALTIMESTAMP(0), deleted_on = null WHERE CODE = 'P14DU910' ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = 'P14DU910-99c4b652-9245-11e7-bddd-0242f725575b' ; +(3 instructions exécutées) +# update : +UPDATE progfin SET LIBELLE = 'PLATIN''', updated_on = LOCALTIMESTAMP(0) WHERE CODE = '925PL005' ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '925PL005-PLATINvxcvxc-99c4b652-9245-11e7-bddd-0242f725575b' ; +(1 instructions exécutées) + +Import réalisé avec succès. +``` + + +Dans le moteur +-------------- + +### Table intermédiaire `SRC_*` + +Lorsque les données source sont issues d'un select, une table intermédiaire `SRC_*` est créée dans la base de données +destination à partir de ces données source. La synchronisation est ensuite réalisée entre cette table intermdiaire et +la table destination. À la fin du processus de synchronisation, cette table intermdiaire est supprimée. + +*NB: attention, si au lancement de la commande d'import la table intermédiaire `SRC_*` existe déjà dans la base de +données ete que le paramètre de config `intermediate_table_auto_drop` est à `false`, l'import échouera.* + +### Différentiel entre source et destination + +La requête utilisée pour comparer les données sources et destination pour une table destination `progfin` ressemble +à cela (syntaxe PostgreSQL) : + +```sql +SELECT create_import_metarequest_for_progfin( + src.code, src.libelle, TO_CHAR(src.debut_validite,'YYYY-MM-DD'), + dest.code, dest.libelle, TO_CHAR(dest.debut_validite,'YYYY-MM-DD'), dest.deleted_on, + 'eb1ab85c-916a-11e7-aba7-0242f725575b' +) AS operation +FROM src_progfin src +FULL OUTER JOIN progfin dest ON src.code = dest.code +; +``` + +### Fonction `create_import_metarequest_for_*` + +Le module crée automatiquement dans le SGBD pour chaque import configuré une fonction `create_import_metarequest_for_*` +chargée de : + - déterminer l'opération de mise à jour nécessaire à partir du résultat du différentiel entre données source et + destination ; + - inscrire dans un "registre d'import" (table `IMPORT_REG`) les instructions SQL nécessaires à la mise à jour de + la table destination. + +Vous trouverez dans [ce fichier](test/UnicaenDbImportUnitTest/CodeGenerator/PostgreSQL/Helper/expected_function_creation_sql.sql) +un exemple de fonction `create_import_metarequest_for_*` pour une table destination `ztemptable` dans une base PostgreSQL. + +*NB: Pour un enregistrement existant à la fois dans la source et la destination (opération "update"), la détection de +différence et donc sa mise à jour se fait colonne par colonne.* + +### Table `IMPORT_REG`, registre d'import + +Une table de travail `import_reg` est utilisée par le module pour y inscrire les opérations nécessaires à chaque +synchronisation. + +Ce registre comporte les colonnes suivantes : + - le type d'opération nécessaire : insert, update, delete, undelete + - le "source code" de l'enregistrement concerné + - le nom de la table concernée + - le nom de la colonne concernée (si c'est pertinent pour l'opération) + - la valeur de la colonne avant synchronisation (si c'est pertinent pour l'opération) + - la valeur de la colonne après synchronisation (si c'est pertinent pour l'opération) + - l'instruction SQL à exécuter pour mettre à jour la table destination + - la date d'exécution de l'instruction SQL de mise à jour de la table destination + - la date de création de la ligne de registre + - un hash permettant d'identifier l'ensemble de lignes auquel appartient cette ligne de registre + (utile pour mettre à jour la date d'exécution) + +Exemple de contenu de la table `import_reg` : + +| operation | source_code | table_name | field_name | from_value | to_value | sql | created_on | executed_on | import_hash | +|-----------|-------------|------------|------------|--------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------|----------------------------|--------------------------------------| +| insert | 013CC021 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$013CC021$$, $$Caract. verres trait$$, $$ $$, $$2017-05-01$$, $$2018-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| insert | 920CA050 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$920CA050$$, $$Tthèse CIFRE2015/119$$, $$ $$, $$2016-01-01$$, $$2018-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| insert | 909CC189 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$909CC189$$, $$THESE CIFRE$$, $$ $$, $$2016-02-01$$, $$2019-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| insert | 909EA002 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$909EA002$$, $$ED SIMEM FONCTIONNEM$$, $$ $$, $$2015-01-01$$, $$2047-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| insert | 913CA052 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$913CA052$$, $$ECO CORAIL$$, $$ $$, $$2014-09-26$$, $$2017-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| insert | 917CA955 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$917CA955$$, $$CAPACITE$$, $$ $$, $$2013-11-01$$, $$2047-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| insert | F971UP03 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$F971UP03$$, $$DELAWARE$$, $$ $$, $$2015-01-01$$, $$2047-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| insert | 925CD271 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$925CD271$$, $$CONV.2015PCM51$$, $$ $$, $$2016-01-01$$, $$2017-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| insert | 925CD276 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$925CD276$$, $$CONV.1060284$$, $$ $$, $$2015-12-10$$, $$2019-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| insert | 920CA047 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$920CA047$$, $$Projet THYMOTHE$$, $$ $$, $$2015-12-21$$, $$2018-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| undelete | 013GR014 | progfin | NULL | NULL | NULL | UPDATE progfin SET LIBELLE = $$SUBVENTION COTENTIN$$, FLECHE = $$ $$, DEBUT_VALIDITE = $$2017-01-01$$, FIN_VALIDITE = $$2047-12-31$$, updated_on = now(), deleted_on = null WHERE CODE = $$013GR014$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| undelete | 012CB018 | progfin | NULL | NULL | NULL | UPDATE progfin SET LIBELLE = $$Bilan sur les nouv.$$, FLECHE = $$ $$, DEBUT_VALIDITE = $$2016-12-15$$, FIN_VALIDITE = $$2018-12-31$$, updated_on = now(), deleted_on = null WHERE CODE = $$012CB018$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| undelete | 012RF001 | progfin | NULL | NULL | NULL | UPDATE progfin SET LIBELLE = $$COLLOQUE CONSTITUTIO$$, FLECHE = $$ $$, DEBUT_VALIDITE = $$2017-01-01$$, FIN_VALIDITE = $$2017-12-31$$, updated_on = now(), deleted_on = null WHERE CODE = $$012RF001$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | +| update | 971UP002 | progfin | LIBELLE | Etablissement api | Etablissement API | UPDATE progfin SET LIBELLE = $$Etablissement API$$, updated_on = now() WHERE CODE = $$971UP002$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$cd384e78-2588-4d99-ad9a-5364adac41fc$$ ; | 2017-09-04 14:17:41.501375 | NULL | cd384e78-2588-4d99-ad9a-5364adac41fc | +| update | 971UP004 | progfin | LIBELLE | ETAB GIORGIA north | ETAB GIORGIA NORTH | UPDATE progfin SET LIBELLE = $$ETAB GIORGIA NORTH$$, updated_on = now() WHERE CODE = $$971UP004$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$cd384e78-2588-4d99-ad9a-5364adac41fc$$ ; | 2017-09-04 14:17:41.501375 | NULL | cd384e78-2588-4d99-ad9a-5364adac41fc | +| update | 950ME003 | progfin | LIBELLE | REGIE DE REC | REGIE DE RECETTES-EN | UPDATE progfin SET LIBELLE = $$REGIE DE RECETTES-EN$$, updated_on = now() WHERE CODE = $$950ME003$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$cd384e78-2588-4d99-ad9a-5364adac41fc$$ ; | 2017-09-04 14:17:41.501375 | NULL | cd384e78-2588-4d99-ad9a-5364adac41fc | +| ``` | | | | | | | | | | + +Remarques : + + - Le "hash" (`eb1ab85c-916a-11e7-aba7-0242f725575b` par exemple) dans la colonne `import_hash` permet d'identifier + un ensemble d'instructions SQL à exécuter au sein d'une même transaction dans la base de données destination. + + - Chaque instruction SQL de mise à jour de la table destination + (ex: `UPDATE progfin SET LIBELLE = $$Etablissement API$$, updated_on = now() WHERE CODE = $$971UP002$$ ;`) + est suivie d'une autre instruction SQL permettant d'inscrire dans le registre d'import la date d'exécution + de la fameuse instruction de mise à jour de la table destination + (ex: `UPDATE import_reg SET executed_on = now() WHERE import_hash = $$cd384e78-2588-4d99-ad9a-5364adac41fc$$ ;`). + + - La colonne `executed_on` accueille la date d'exécution de l'instruction de mise à jour de la table destination ; + si cette date est NULL, l'instruction n'a pas été exécutée. diff --git a/README.md b/README.md index 00bfc09..78af97e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ UnicaenDbImport =============== - - Ce module a pour but de réaliser l'import de données d'une table (ou d'un select) d'une base de données source vers une table d'une base de données destination. @@ -22,28 +20,39 @@ Les enregistrements source et les enregistrements destination doivent avoir un i de les rapprocher : on l'appellera "code source" (cf. paramètre de config `source_code_column`). **Cet identifiant doit être de type chaîne de caractères.** +__**Table des matières**__ + + * [La différence avec le module UnicaenImport ?](#la-différence-avec-le-module-unicaenimport-) + * [Installation](#installation) + * [Configuration](#configuration) + * [Utilisation](#utilisation) + * [Contraintes](#contraintes) + * [Fonctionnement](#fonctionnement) + * [Exemples](#exemples) + + La différence avec le module UnicaenImport ? -------------------------------------------- + + Comme son nom ne l'indique pas, UnicaenImport fonctionne uniquement entre + 2 bases de données **Oracle** ; et la synchronisation est faite entièrement par le SGBD (ou presque). + + UnicaenDbImport fonctionne en majeure partie en PHP tout en déléguant au maximum au SGBD ce qu'il sait bien faire + (`select FULL OUTER JOIN` pour le différentiel entre données source et destination, fonction pour l'alimentation du registre + d'import, etc.) Par conséquent, il est possible et souhaitable de l'enrichir pour implémenter une synchronisation vers + différentes plateformes de base de données destination. Les plateformes de base de données destination suivantes sont + implémentées : + - PostgreSQL + + *NB: En revanche toutes les plateformes de base de données source sont supportées car on se contente d'y faire un select + à l'aide de l'ORM Doctrine 2 pour alimenter une table intermédiaire.* + -Comme son nom de l'indique pas, UnicaenImport fonctionne uniquement entre -2 bases de données Oracle ; et la synchronisation est faite entièrement par le SGBD (ou presque). - -UnicaenDbImport fonctionne en majeure partie en PHP tout en déléguant au maximum au SGBD ce qu'il sait bien faire -(`select FULL OUTER JOIN` pour le différentiel entre données source et destination, fonction pour l'alimentation du registre -d'import, etc.) Par conséquent, il est possible et souhaitable de l'enrichir pour implémenter une synchronisation vers -différentes plateformes de base de données destination. Les plateformes de base de données destination suivantes sont -implémentées : - - PostgreSQL - -*NB: En revanche toutes les plateformes de base de données source sont supportées car on se contente d'y faire un select -à l'aide de l'ORM Doctrine 2 pour alimenter une table intermédiaire.* - - Installation ------------ - - composer require unicaen/unicaen-db-import + + composer require unicaen/unicaen-db-import Configuration @@ -61,21 +70,37 @@ cp -n vendor/unicaen/db-import/config/unicaen-db-import.local.php.dist config/au Utilisation ----------- -Le module fournit une ligne de commande pour : +Le module possède deux mécanismes distincts : +1. import : réalise un import "brut" des données, autrement dit une copie, d'une source vers une destination; +2. synchro : réalise une synchronisation entre une source et une destination en gérant une historisation (created_on, updated_on, deleted_on). - - lancer un import par son nom, exemple : +Le module fournit donc une ligne de commande pour lancer : + + - un import par son nom : ```bash - php public/index.php run import --name "IMPORT_PFI_SIFAC" + php public/index.php run import --name "NOM_ETABLI_DANS_LA_CONFIG" ``` - - lancer tous les imports : + - tous les imports : ```bash php public/index.php run import --all ``` + + - une synchro par son nom : + + ```bash + php public/index.php run synchro --name "NOM_ETABLI_DANS_LA_CONFIG" + ``` + + - toutes les synchros : + + ```bash + public/index.php run synchro --all + ``` -*NB: Cette commande exécute la synchronisation une fois. Pour synchroniser en permanence la base de données destination, +*NB: L'exécution d'une de ces commandes n'est effectif qu'une fois. Pour importer/synchroniser en permanence (dans) la base de données destination, il faut programmer le lancement périodique de cette commande à l'aide de CRON (cf. plus bas pour un exemple).* @@ -84,7 +109,7 @@ Contraintes ### Identifiant commun -L'identifiant unique commun des enregistrements source et destination doit être de type **chaîne de caractères**. +L'identifiant unique commun des enregistrements (source_code_column) source et destination doit être de type **chaîne de caractères**. ### Source de type `'select'` @@ -99,6 +124,8 @@ Dans le cas d'une *source* de type `'select'`, il est nécessaire de convertir c Il est également nécessaire de spécifier dans la config de la *destination* la fonction de conversion à appliquer à chaque colonne pour la convertir en chaîne de caractères (cf. paramètre de config destination `columns_to_char`). +_Note : Peut être pas obligé de faire la convertion TO_CHAR dans le SELECT (mais quand même le faire dans le columns_to_char)._ + ### Source de type `'table'` Dans le cas d'une *source* de type `'table'`, il est nécessaire de spécifier dans la config de la @@ -116,299 +143,176 @@ ALTER TABLE TABLE_DESTINATION ADD COLUMN deleted_on TIMESTAMP(0) WITH TIME ZONE; ``` -Exemple de mise en oeuvre -------------------------- -Import des programmes de financement SIFAC depuis une base Oracle vers une table PostgreSQL. Les données sources sont -"extraites" grâce à un select. +Fonctionnement +-------------- + +### Import de données + +Afin de s'adapter au plus grand nombre de SGBD, UnicaenDbImport s'appuie sur l'_ORM Doctrine 2_. +Ainsi nous effectuons le déroulement suivant : + +<!--Voir le dossier documentation/ pour toutes modifications--> + + +*Schématisation du déroulement d'un import* + +1. Suppression des données existantes dans _Destination_ +2. Récupération des données de la _Source_ vers _Doctrine_ +3. Génération des requêtes SQL (insert, update, ...) depuis PHP +4. Exécution des requêtes / Récupération des données vers _Destination_ + + +### Synchronisation de données locales + +L'un des mécanismes d'UnicaenDbImport est d'appliquer un système de synchronisation avec gestion d'une historisation. +Avec une table _Source_ et une table _Destination_ toutes deux dans une base locale, le déroulement est le suivant : + +<!--Voir le dossier documentation/ pour toutes modifications--> + + +*Schématisation du déroulement d'une synchro (locale)* -- Configuration globale du module : +1. Création d'une vue différentielle +2. Préparation/détection des actions à appliquer (INSERT, UPDATE, ...) +3. Application des mises à jour (avec historisation : created_on, updated_on, ...) + + +### Synchronisation de données distantes + +Concernant le mécanisme de synchronisation, il est possible de spécifier une table _Source_ provenant d'une base distante. +Dans ce cas, les mécanismes d'[import](#import-de-données) et de [synchro (locale)](synchronisation-de-données-locales) seront réalisés successivement. + +<!--Voir le dossier documentation/ pour toutes modifications--> + + +*Schématisation du déroulement d'une synchro (distant)* + +1. Déclenchement du mécanisme d'import entre la _Source_ et une table _Temporaire_ +2. Déclenchement du mécanisme de synchro entre la table _Temporaire_ et la table _Destination +3. Suppression de la table _Temporaire_ + + +Exemple +------- + +Pour les exemples suivants, on suppose posséder : + * 1 Base de donnée **A** locale (celle de notre application) contenant les tables : + * **UTILISATEUR**(int ID, str CODE, str NOM, str PRENOM, date NAISSANCE) + * **COMPOSANTE**(int ID, str CODE, str NOM) + * **PAIN_AU_CHOCOLAT**(int ID, str NOM, str BOULANGERIE) + * 1 Base de donnée **B** distante (par exemple Apogée) contenant les tables : + * **UTILISATEUR**(int ID, str CODE, str NOM, str PRENOM, date NAISSANCE) + * **COMPOSANTE**(int ID, str CODE, str NOM, int DIRECTEUR_ID, str ADRESSE) + * **CHOCOLATINE**(int ID, str NOM, str BOULANGERIE, int POURCENT_GRAS, int NOTE) + + +En outre, on suppose avoir déclaré les configurations _Doctrine_ 'orm_A' et 'orm_B' respectivement pour les bases **A** et **B** ci-dessus (soit dans le fichier [unicaen-db-import.local.php](config/unicaen-db-import.local.php.dist) soit dans un autre fichier de config.). + +### Exemple 1 : Importation d'une table de données distantes + +Dans de nombreux cas, on peut souhaiter importer une table de données provenant d'une base distante. +Ce peut être le cas notamment lorsque l'on souhaite s'assurer de la constante disponibilité des données. `unicaen-db-import.global.php` ```php return [ - /** - * Configuration du module UnicaenDbImport. - */ 'import' => [ - /** - * Liste des imports. - */ 'imports' => [ [ - /** - * Petit nom (unique) de l'import. - */ - 'name' => 'Import_PFI_SIFAC', - - /** - * Configuration de la source de données à importer : - * - 'name' : petit nom (unique) de la source - * - 'table' : nom de la table source contenant les données à importer - * - 'select' : select SQL de mise en forme des données source à importer (NB: antinomique avec 'table') - * - 'connection' : identifiant de la connexion Doctrine à la base de données source - * - 'source_code_column' : nom de la colonne dans la table/vue source contenant l'identifiant unique - * - 'columns' : liste ordonnée des noms des colonnes à prendre en compte dans la table/vue source - */ + 'name' => "IMPORTATION DES DONNÉES B VERS A", + 'source' => [ - 'name' => 'PFI SIFAC', - 'connection' => 'doctrine.connection.orm_oracle', + 'name' => 'TABLE UTILISATEUR DE MA BASE B', + 'table' => 'UTILISATEUR', + 'connection' => 'doctrine.connection.orm_B', 'source_code_column' => 'CODE', - 'columns' => ['LIBELLE', 'FLECHE', 'DEBUT_VALIDITE', 'FIN_VALIDITE'], - 'select' => <<<'EOT' -select distinct - MEASURE CODE, - SHORT_DESC LIBELLE, - ZFLECHE FLECHE, - to_char(TO_DATE(VALID_FROM,'YYYYMMDD'),'YYYY-MM-DD') DEBUT_VALIDITE, - to_char(TO_DATE(VALID_TO,'YYYYMMDD'),'YYYY-MM-DD') FIN_VALIDITE - from ( - SELECT - SAPSR3.FMMEASURE.MEASURE, - SAPSR3.FMMEASURET.SHORT_DESC, - SAPSR3.FMMEASURE.ZFLECHE, - SAPSR3.FMMEASURE.VALID_FROM, - SAPSR3.FMMEASURE.VALID_TO - FROM - SAPSR3.FMMEASURE, - SAPSR3.FMMEASURET - WHERE - ( (SAPSR3.FMMEASURE.CLIENT in (select A.MANDT from SAPSR3.FM01H A where A.MANDT = '500') OR SAPSR3.FMMEASURE.CLIENT is null) - and (SAPSR3.FMMEASURE.FMAREA in (select A.FIKRS from SAPSR3.FM01H A where A.FIKRS = '1010') or SAPSR3.FMMEASURE.FMAREA is null) ) - AND ( SAPSR3.FMMEASURE.CLIENT=SAPSR3.FMMEASURET.CLIENT(+) ) - AND ( SAPSR3.FMMEASURE.FMAREA=SAPSR3.FMMEASURET.FMAREA(+) ) - AND ( SAPSR3.FMMEASURE.MEASURE=SAPSR3.FMMEASURET.MEASURE(+) ) -) -EOT - , + 'columns' => ['ID', 'CODE', 'NOM', 'PRENOM', 'NAISSANCE'], ], - - /** - * Forçage éventuel du nom de la table intermédiaire utilisée lorsque source et destination - * ne partagent pas la même connexion. NB: cette table intermédiaire est créée/peuplée/supprimée - * dans la base de données destination à chaque import. - * En l'absence de ce forçage, le nom de la table intermédiaire sera celui de la table destination - * préfixé par "src_". - */ - 'intermediate_table' => 'src_progfin', - - /** - * Suppression automatique de la table intermédiaire "src_" au début de l'import. - * Si la suppression automatique est désactivée, l'existence de la table intermédiaire au démarrage - * de l'import fera échouer l'import. - */ - 'intermediate_table_auto_drop' => false, - - /** - * Configuration de la destination des données importées : - * - 'name' : petit nom (unique) de la destination - * - 'table' : nom de la table destination vers laquelle les données sont importées - * - 'connection' : identifiant de la connexion Doctrine à la base de données destination - * - 'source_code_column' : nom de la colonne dans la table destination contenant l'identifiant unique - * - 'columns' : liste ordonnée des noms des colonnes importées dans la table destination - * - 'columns_to_char' : format sprintf nécessaire pour mettre des colonnes au format requis (string) - */ + 'destination' => [ - 'name' => 'PFI Zébu', - 'table' => 'progfin', - 'connection' => 'doctrine.connection.orm_default', - 'source_code_column' => 'code', - 'columns' => ['libelle', 'fleche', 'debut_validite', 'fin_validite'], + 'name' => 'TABLE UTILISATEUR DE MA BASE A', + 'table' => 'UTILISATEUR', + 'connection' => 'doctrine.connection.orm_A', + 'source_code_column' => 'CODE', + 'columns' => ['ID', 'CODE', 'NOM', 'PRENOM', 'NAISSANCE'], 'columns_to_char' => [ - 'debut_validite' => "TO_CHAR(%s,'YYYY-MM-DD')", // car colonne destination de type TIMESTAMP - 'fin_validite' => "TO_CHAR(%s,'YYYY-MM-DD')", // idem + 'ID' => "%s||''", + 'NAISSANCE' => "TO_CHAR(%s,'YYYY-MM-DD')", ], ], ], ], ], ]; -``` +``` + +`Dans le terminal du serveur` +```bash +php public/index.php run import --name "IMPORTATION DES DONNÉES B VERS A" +``` + + + + -*On doit renseigner le paramètre `intermediate_table` car les données source proviennent d'un select.* +### Exemple 2 : Importation d'un select de données distantes -- Configuration locale du module : +Les bases distantes requêtées sont généralement bien garnies. +Pourtant il est fréquent de ne vouloir importer qu'une partie de ces données. -`unicaen-db-import.local.php` +`unicaen-db-import.global.php` ```php return [ - /** - * Configuration Doctrine minimum requise. - */ - 'doctrine' => [ - 'connection' => [ - 'orm_oracle' => [ - 'driverClass' => 'Doctrine\\DBAL\\Driver\\OCI8\\Driver', - 'params' => [ - 'host' => 'host.domain.fr', - 'port' => '1525', - 'user' => 'x', - 'password' => 'y', - 'dbname' => 'z', - 'charset' => 'AL32UTF8', - ], - 'eventmanager' => 'orm_oracle', - ], - 'orm_default' => [ - 'driverClass' => 'Doctrine\\DBAL\\Driver\\PDOPgSql\\Driver', - 'params' => [ - 'host' => 'host.domain.fr', - 'port' => '5432', - 'charset' => 'utf8', - 'dbname' => 'a', - 'user' => 'b', - 'password' => 'c', - - ], - ], - ], - 'eventmanager' => [ - 'orm_sifac' => [ - 'subscribers' => [ - \Doctrine\DBAL\Event\Listeners\OracleSessionInit::class, + 'import' => [ + 'imports' => [ + [ + 'name' => "IMPORTATION PARTIELLE DES DONNÉES B VERS A", + + 'source' => [ + 'name' => 'TABLE CHOCOLATINE DE MA BASE B', + 'select' => <<<'EOT' +SELECT +ID, +NOM, +BOULANGERIE +FROM CHOCOLATINE +EOT + , + 'connection' => 'doctrine.connection.orm_B', + 'source_code_column' => 'ID', + 'columns' => ['ID', 'NOM', 'BOULANGERIE'], + ], + + 'destination' => [ + 'name' => 'TABLE PAIN_AU_CHOCOLAT DE MA BASE A', + 'table' => 'PAIN_AU_CHOCOLAT', + 'connection' => 'doctrine.connection.orm_A', + 'source_code_column' => 'ID', + 'columns' => ['ID', 'NOM', 'BOULANGERIE'], + 'columns_to_char' => [ + 'ID' => "%s||''", + ], + ], ], ], - ], - ], + ], ]; ``` - -*NB: les paramètres de config de la connexion `orm_default` existent sans doute déjà dans la configuration de votre -application, il est bien-sûr inutile de les répéter dans la config du module unicaen/db-import.* - -- Configuration CRON pour lancer périodiquement la synchro : -```cron -# Du lundi au vendredi, entre 6h00 et 19h45, toutes les 15 minutes -*/15 6-19 * * 1-5 root /usr/bin/php /var/www/zebu-back/public/index.php run import --all 1> /tmp/zebu-cron.log 2>&1 -``` -- Exemple de logs affichés par la commande : - - ``` -######################## IMPORTS ######################## - -### Import des PFI SIFAC ### -05/09/2017 16:22:08 -# delete : -UPDATE progfin SET deleted_on = LOCALTIMESTAMP(0) WHERE CODE = 'fddsfsd' ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = 'fddsfsd-99c4b652-9245-11e7-bddd-0242f725575b' ; -(1 instructions exécutées) -# insert : -INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('971UP009', 'Of Wisco', ' ', '2014-01-01', '2047-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '971UP009-99c4b652-9245-11e7-bddd-0242f725575b' ; -INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('977CA025', 'Screening de 5', ' ', '2016-07-21', '2018-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '977CA025-99c4b652-9245-11e7-bddd-0242f725575b' ; -INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('F950MEC1', 'MDE CAEN FONCTIONNEM', ' ', '2015-01-01', '2047-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = 'F950MEC1-99c4b652-9245-11e7-bddd-0242f725575b' ; -INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('F971CF03', 'UNIVERSITE DU HAVRE', ' ', '2015-01-01', '2047-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = 'F971CF03-99c4b652-9245-11e7-bddd-0242f725575b' ; -INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('014DU009', 'DU RDCM', ' ', '2016-01-01', '2047-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '014DU009-99c4b652-9245-11e7-bddd-0242f725575b' ; -INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('012RE001', 'COLLOQUE 50 ANS SC E', ' ', '2017-01-01', '2017-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '012RE001-99c4b652-9245-11e7-bddd-0242f725575b' ; -INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('012CH021', 'Typo-chronologie', ' ', '2017-01-01', '2017-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '012CH021-99c4b652-9245-11e7-bddd-0242f725575b' ; -INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('013PL002', 'CMABIO', ' ', '2016-01-01', '2047-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '013PL002-99c4b652-9245-11e7-bddd-0242f725575b' ; -INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE, created_on) VALUES ('999CM003', '2016-AGRI-133', ' ', '2016-03-30', '2018-12-31', LOCALTIMESTAMP(0)) ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '999CM003-99c4b652-9245-11e7-bddd-0242f725575b' ; -(9 instructions exécutées) -# undelete : -UPDATE progfin SET LIBELLE = 'TA 2017', FLECHE = ' ', DEBUT_VALIDITE = '2017-01-01', FIN_VALIDITE = '2047-12-31', updated_on = LOCALTIMESTAMP(0), deleted_on = null WHERE CODE = '913TA017' ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '913TA017-99c4b652-9245-11e7-bddd-0242f725575b' ; -UPDATE progfin SET LIBELLE = 'TA 2017', FLECHE = ' ', DEBUT_VALIDITE = '2017-01-01', FIN_VALIDITE = '2047-12-31', updated_on = LOCALTIMESTAMP(0), deleted_on = null WHERE CODE = '920TA017' ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '920TA017-99c4b652-9245-11e7-bddd-0242f725575b' ; -UPDATE progfin SET LIBELLE = 'PAIE DIU ADOLESCENTS', FLECHE = ' ', DEBUT_VALIDITE = '2017-01-01', FIN_VALIDITE = '2047-12-31', updated_on = LOCALTIMESTAMP(0), deleted_on = null WHERE CODE = 'P14DU910' ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = 'P14DU910-99c4b652-9245-11e7-bddd-0242f725575b' ; -(3 instructions exécutées) -# update : -UPDATE progfin SET LIBELLE = 'PLATIN''', updated_on = LOCALTIMESTAMP(0) WHERE CODE = '925PL005' ; UPDATE import_reg SET executed_on = LOCALTIMESTAMP(0) WHERE import_hash = '925PL005-PLATINvxcvxc-99c4b652-9245-11e7-bddd-0242f725575b' ; -(1 instructions exécutées) - -Import réalisé avec succès. +`Dans le terminal du serveur` +```bash +php public/index.php run import --name "IMPORTATION PARTIELLE DES DONNÉES B VERS A" ``` +### Exemple 3 : Synchronisation de données locales -Dans le moteur --------------- - -### Table intermédiaire `SRC_*` +Ex: SRC_XXX vers XXX -Lorsque les données source sont issues d'un select, une table intermédiaire `SRC_*` est créée dans la base de données -destination à partir de ces données source. La synchronisation est ensuite réalisée entre cette table intermdiaire et -la table destination. À la fin du processus de synchronisation, cette table intermdiaire est supprimée. +### Exemple 4 : Synchronisation de données distantes -*NB: attention, si au lancement de la commande d'import la table intermédiaire `SRC_*` existe déjà dans la base de -données ete que le paramètre de config `intermediate_table_auto_drop` est à `false`, l'import échouera.* +Ex: WELCOME_CATALOGUE vers CATALOGUE (-> Utilisation du mécanisme d'import, etc, donc potentiellement des paramètres 'intermediate_table' et 'intermediate_table_auto_drop', ...) -### Différentiel entre source et destination - -La requête utilisée pour comparer les données sources et destination pour une table destination `progfin` ressemble -à cela (syntaxe PostgreSQL) : +### Exemple 5 : Synchronisation de données distantes avec récupération des clés étrangères locales -```sql -SELECT create_import_metarequest_for_progfin( - src.code, src.libelle, TO_CHAR(src.debut_validite,'YYYY-MM-DD'), - dest.code, dest.libelle, TO_CHAR(dest.debut_validite,'YYYY-MM-DD'), dest.deleted_on, - 'eb1ab85c-916a-11e7-aba7-0242f725575b' -) AS operation -FROM src_progfin src -FULL OUTER JOIN progfin dest ON src.code = dest.code -; -``` - -### Fonction `create_import_metarequest_for_*` - -Le module crée automatiquement dans le SGBD pour chaque import configuré une fonction `create_import_metarequest_for_*` -chargée de : - - déterminer l'opération de mise à jour nécessaire à partir du résultat du différentiel entre données source et - destination ; - - inscrire dans un "registre d'import" (table `IMPORT_REG`) les instructions SQL nécessaires à la mise à jour de - la table destination. - -Vous trouverez dans [ce fichier](test/UnicaenDbImportUnitTest/CodeGenerator/PostgreSQL/Helper/expected_function_creation_sql.sql) -un exemple de fonction `create_import_metarequest_for_*` pour une table destination `ztemptable` dans une base PostgreSQL. - -*NB: Pour un enregistrement existant à la fois dans la source et la destination (opération "update"), la détection de -différence et donc sa mise à jour se fait colonne par colonne.* - -### Table `IMPORT_REG`, registre d'import - -Une table de travail `import_reg` est utilisée par le module pour y inscrire les opérations nécessaires à chaque -synchronisation. - -Ce registre comporte les colonnes suivantes : - - le type d'opération nécessaire : insert, update, delete, undelete - - le "source code" de l'enregistrement concerné - - le nom de la table concernée - - le nom de la colonne concernée (si c'est pertinent pour l'opération) - - la valeur de la colonne avant synchronisation (si c'est pertinent pour l'opération) - - la valeur de la colonne après synchronisation (si c'est pertinent pour l'opération) - - l'instruction SQL à exécuter pour mettre à jour la table destination - - la date d'exécution de l'instruction SQL de mise à jour de la table destination - - la date de création de la ligne de registre - - un hash permettant d'identifier l'ensemble de lignes auquel appartient cette ligne de registre - (utile pour mettre à jour la date d'exécution) - -Exemple de contenu de la table `import_reg` : - -| operation | source_code | table_name | field_name | from_value | to_value | sql | created_on | executed_on | import_hash | -|-----------|-------------|------------|------------|--------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------|----------------------------|--------------------------------------| -| insert | 013CC021 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$013CC021$$, $$Caract. verres trait$$, $$ $$, $$2017-05-01$$, $$2018-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| insert | 920CA050 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$920CA050$$, $$Tthèse CIFRE2015/119$$, $$ $$, $$2016-01-01$$, $$2018-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| insert | 909CC189 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$909CC189$$, $$THESE CIFRE$$, $$ $$, $$2016-02-01$$, $$2019-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| insert | 909EA002 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$909EA002$$, $$ED SIMEM FONCTIONNEM$$, $$ $$, $$2015-01-01$$, $$2047-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| insert | 913CA052 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$913CA052$$, $$ECO CORAIL$$, $$ $$, $$2014-09-26$$, $$2017-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| insert | 917CA955 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$917CA955$$, $$CAPACITE$$, $$ $$, $$2013-11-01$$, $$2047-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| insert | F971UP03 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$F971UP03$$, $$DELAWARE$$, $$ $$, $$2015-01-01$$, $$2047-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| insert | 925CD271 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$925CD271$$, $$CONV.2015PCM51$$, $$ $$, $$2016-01-01$$, $$2017-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| insert | 925CD276 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$925CD276$$, $$CONV.1060284$$, $$ $$, $$2015-12-10$$, $$2019-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| insert | 920CA047 | progfin | NULL | NULL | NULL | INSERT INTO progfin(CODE, LIBELLE, FLECHE, DEBUT_VALIDITE, FIN_VALIDITE) VALUES ($$920CA047$$, $$Projet THYMOTHE$$, $$ $$, $$2015-12-21$$, $$2018-12-31$$) ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| undelete | 013GR014 | progfin | NULL | NULL | NULL | UPDATE progfin SET LIBELLE = $$SUBVENTION COTENTIN$$, FLECHE = $$ $$, DEBUT_VALIDITE = $$2017-01-01$$, FIN_VALIDITE = $$2047-12-31$$, updated_on = now(), deleted_on = null WHERE CODE = $$013GR014$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| undelete | 012CB018 | progfin | NULL | NULL | NULL | UPDATE progfin SET LIBELLE = $$Bilan sur les nouv.$$, FLECHE = $$ $$, DEBUT_VALIDITE = $$2016-12-15$$, FIN_VALIDITE = $$2018-12-31$$, updated_on = now(), deleted_on = null WHERE CODE = $$012CB018$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| undelete | 012RF001 | progfin | NULL | NULL | NULL | UPDATE progfin SET LIBELLE = $$COLLOQUE CONSTITUTIO$$, FLECHE = $$ $$, DEBUT_VALIDITE = $$2017-01-01$$, FIN_VALIDITE = $$2017-12-31$$, updated_on = now(), deleted_on = null WHERE CODE = $$012RF001$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$eb1ab85c-916a-11e7-aba7-0242f725575b$$ ; | 2017-09-04 14:17:41.501375 | 2017-09-04 14:17:41.549583 | eb1ab85c-916a-11e7-aba7-0242f725575b | -| update | 971UP002 | progfin | LIBELLE | Etablissement api | Etablissement API | UPDATE progfin SET LIBELLE = $$Etablissement API$$, updated_on = now() WHERE CODE = $$971UP002$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$cd384e78-2588-4d99-ad9a-5364adac41fc$$ ; | 2017-09-04 14:17:41.501375 | NULL | cd384e78-2588-4d99-ad9a-5364adac41fc | -| update | 971UP004 | progfin | LIBELLE | ETAB GIORGIA north | ETAB GIORGIA NORTH | UPDATE progfin SET LIBELLE = $$ETAB GIORGIA NORTH$$, updated_on = now() WHERE CODE = $$971UP004$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$cd384e78-2588-4d99-ad9a-5364adac41fc$$ ; | 2017-09-04 14:17:41.501375 | NULL | cd384e78-2588-4d99-ad9a-5364adac41fc | -| update | 950ME003 | progfin | LIBELLE | REGIE DE REC | REGIE DE RECETTES-EN | UPDATE progfin SET LIBELLE = $$REGIE DE RECETTES-EN$$, updated_on = now() WHERE CODE = $$950ME003$$ ; UPDATE import_reg SET executed_on = now() WHERE import_hash = $$cd384e78-2588-4d99-ad9a-5364adac41fc$$ ; | 2017-09-04 14:17:41.501375 | NULL | cd384e78-2588-4d99-ad9a-5364adac41fc | -| ``` | | | | | | | | | | - -Remarques : - - - Le "hash" (`eb1ab85c-916a-11e7-aba7-0242f725575b` par exemple) dans la colonne `import_hash` permet d'identifier - un ensemble d'instructions SQL à exécuter au sein d'une même transaction dans la base de données destination. - - - Chaque instruction SQL de mise à jour de la table destination - (ex: `UPDATE progfin SET LIBELLE = $$Etablissement API$$, updated_on = now() WHERE CODE = $$971UP002$$ ;`) - est suivie d'une autre instruction SQL permettant d'inscrire dans le registre d'import la date d'exécution - de la fameuse instruction de mise à jour de la table destination - (ex: `UPDATE import_reg SET executed_on = now() WHERE import_hash = $$cd384e78-2588-4d99-ad9a-5364adac41fc$$ ;`). - - - La colonne `executed_on` accueille la date d'exécution de l'instruction de mise à jour de la table destination ; - si cette date est NULL, l'instruction n'a pas été exécutée. +Cas d'une table "Composante" où des composantes sont en clé étrangère avec une table "Formation" via son ID. -> Passage par une vue SRC (déclaration d'un import et d'une synchro)... diff --git a/documentation/README.md b/documentation/README.md new file mode 100644 index 0000000..8512c93 --- /dev/null +++ b/documentation/README.md @@ -0,0 +1,9 @@ +Documentation +============= + +Ce répertoire contient les diverses ressources (images...) nécessaire à la documentation générale de l'application. + + +*file* **.png** : image utilisée directement dans la documentation + +*file* **.drawio** : source d'une image permettant sa modification sur https://www.draw.io \ No newline at end of file diff --git a/documentation/fonctionnement_synchro_distant.drawio b/documentation/fonctionnement_synchro_distant.drawio new file mode 100644 index 0000000..949b69f --- /dev/null +++ b/documentation/fonctionnement_synchro_distant.drawio @@ -0,0 +1 @@ +<mxfile modified="2019-05-15T14:18:26.824Z" host="www.draw.io" agent="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0" etag="Jr5_HcIyoOzdWaDvR-2I" version="10.6.7" type="device"><diagram id="mWMuuFM-TLnJckGO8D4I" name="Page-1">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</diagram></mxfile> \ No newline at end of file diff --git a/documentation/fonctionnement_synchro_distant.png b/documentation/fonctionnement_synchro_distant.png new file mode 100644 index 0000000000000000000000000000000000000000..85c92db2600b2a68b1943b58929e6a7cc866f666 GIT binary patch literal 15418 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 GGywpTXt*u_ literal 0 HcmV?d00001 diff --git a/documentation/fonctionnement_synchro_locale.drawio b/documentation/fonctionnement_synchro_locale.drawio new file mode 100644 index 0000000..62e8699 --- /dev/null +++ b/documentation/fonctionnement_synchro_locale.drawio @@ -0,0 +1 @@ +<mxfile modified="2019-05-15T13:45:14.165Z" host="www.draw.io" agent="Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0" etag="7NW-oTR3yv0mZXlWeKPD" version="10.6.7" type="device"><diagram id="mWMuuFM-TLnJckGO8D4I" name="Page-1">3Vhbj5s4FP41PM4IcMjlMSVNq2pXu1Kk2faRgkPcOpg1zm1//R5jGwwmk0TNNFUz0sQ+vn/nnO+z46F4e/zAk3LzJ8sw9UI/O3po4YXhCI3gvzSclGHqI2XIOcmUKWgNK/If1kZfW3ckw1Wno2CMClJ2jSkrCpyKji3hnB263daMdlctkxw7hlWaUNf6D8nERp8i8lv7R0zyjVk58HXLNjGdtaHaJBk7WCb03kMxZ0yo0vYYYyqxM7ioccszrc3GOC7ENQNCNWCf0J0+24rteIr17sTJHBk2WspieqKkyDD30LvDhgi8KpNU2g/gaLBtxJZCLYDiV7aDjtkfXxtDkn7PubT+tRMwC9b2NaE0ZpTxeiUUx8tlHIPdPYs+3h5zgY+WSZ/tA2ZbLPgJuhxNYKkROs4CA/uh9dpYmzaWw8ywRMdJ3kzcQgkFjeYwsshB1gvHFBZ4l5E9FHNRH1GZ1gyOaOM9/nfHTMNTVSfAHDoEYXlsG80sC1wJ8mTmgo2p6bpLgHlg4bvvpUgEYcWNe3lYrC2XM/jcJ9bQA4MtGgi2Hqi4yOaS+iSuNKkqknYhxEciPlvlL1D2nyNdW8hD+qZyMpUC9vnZrlijZLUdVtfMuLNoV4p8bGoSCc+x7vXyInjxiUTkQMJpddp/Yqv1U6DFBGcd0nb9ZPkhGvCDsXFMIYb3Xaofco5e4W9G6oTRYTDpRkE067lXHVEPspm5N08jESacxr2JFDDORHWoNKe+KnomTvQsvBh585kA8ayz2c/Ieq1s4LMUtLcfXpAkohtQleDsOzbJVjCZg53806aEkryQUQnBUOe6TDkCYjvXDVuSZXKZQRLgKvMvBdb1aRzMph3cZ5GTxcFoIHzCO6Tx1HHEQB5TCtecc3BY+CdVqe4+a3KUAPW5bx3JP8dR0DKuP5rxLbv63AnmyeQSzOEboRy4l57b2dIwX3Ab8xmW9W9g2bNo9xjvBq60eRY9ki0R6rJcc8W/lS5R0KPLvqzejy5fRfb1+94LtF17H5OU+/z4G1POk4zgDg80N3TTtiBcKwUsBZfRixcthycG4vssdYBgPUcPu2oNe9+V0JgrvdT3YX+/cx9Vv5Jo/qBLxj2XTFyPTNHb8PmwR1wtDRz8fx0t/TH0/fEl7N9KSwexNxPf4znSiuMXq+XSc6TV4WbUxeeIo6YX3ievyoCtxI8V2N4zIuw/I64W2MkFpT4jsODl5GR1K2WH6vyGw6h7/w6R/+q+Rj3h7/WHgtrBXdW+ue+2ET4vSwoEbN5L8onkb0lVf9dKANvyvwHYv7EKzMKuK0YuEQXBz1QBQzyWn9BvqwKToKfBQ/jfSQig2v5IrbKq/aUfvf8f</diagram></mxfile> \ No newline at end of file diff --git a/documentation/fonctionnement_synchro_locale.png b/documentation/fonctionnement_synchro_locale.png new file mode 100644 index 0000000000000000000000000000000000000000..3252272fbade08c5f9df71f931a44a35d28ec567 GIT binary patch literal 15829 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 B)q?;4 literal 0 HcmV?d00001 -- GitLab