Accélérer l'Importation de Données d'Entreprise : Un Service Basé sur YAML avec Java et Vue
Dans les systèmes d'entreprise modernes, l'importation rapide et efficace de grands volumes de données historiques ou initiales représente souvent un défi majeur. Les approches traditionnelles, qui exigent un code lourd ou des configurations complexes, peuvent prolonger le processus pendant des jours. Cet article explore une méthode innovante pour créer un service léger, orienté YAML, en utilisant une pile Java et Vue.js, conçu pour réduire les temps de chargement des données à quelques minutes seulement, simplifiant ainsi considérablement l'interaction des spécialistes techniques avec la plateforme et son contenu.
Le développement de la nouvelle JMatrixPlatform, bâtie sur le principe du "tout en tant que code", a mis en lumière un problème crucial : l'absence d'un outil pratique pour les téléchargements de données ponctuels. Contrairement aux systèmes plus anciens où l'importation de données était une tâche triviale, la nouvelle architecture exigeait l'écriture de code Java pour chaque opération, rendant le processus laborieux et inflexible. Les analystes ont perdu la capacité de gérer les données de manière autonome, devenant entièrement dépendants des développeurs.
Plusieurs approches standards ont été envisagées, chacune présentant des inconvénients majeurs :
- Augmenter l'API REST avec des services de traitement par lots : Cela nécessiterait une conversion complexe des données Excel en tableaux JSON, y compris l'échappement des guillemets dans les formules Excel, ce qui est une tâche non triviale en soi. L'utilisation d'outils externes comme Postman compliquerait encore le processus.
- Utiliser
curl + jsonpour chaque objet : Cela exigerait un script supplémentaire pour la récupération des jetons et n'offrirait pas de résilience aux erreurs, arrêtant l'importation dès le premier enregistrement incorrect. - Importation directe via SQL : Cette option a été écartée car toute la logique d'interaction avec les données est implémentée au niveau de l'application. Une intervention directe dans la base de données, contournant la logique métier de la plateforme, pourrait entraîner des incohérences de données et compromettre l'intégrité du système.
- Développer une importation Excel native pour chaque entité : Cela a été jugé économiquement irréalisable et trop chronophage pour des importations ponctuelles.
Le besoin d'un outil rapide et flexible, permettant aux spécialistes techniques de gérer les téléchargements de données de manière autonome, est devenu évident.
Solution : Un Service Orienté YAML pour les Téléchargements par Lots
Reconnaissant les limites des approches existantes, l'auteur a conclu qu'un service minimaliste était nécessaire, doté d'un point d'entrée unique (endpoint) capable d'accepter des données structurées pour effectuer diverses opérations (créer, modifier, supprimer). Au lieu du JSON, YAML a été choisi en raison de sa concision et de sa facilité de génération manuelle ou de création automatisée à l'aide de simples formules Excel ou de LLM.
Un exemple de requête YAML démontre sa simplicité et sa lisibilité :
#request
- createObject:
type: ru.commons.matrix.schema.type.ATPPerson
policy: ru.commons.matrix.schema.policy.ALCPerson
- createObject:
type: ru.commons.matrix.schema.type.ATPPerson1
policy: ru.commons.matrix.schema.policy.ALCPerson
- createObject:
type: ru.commons.matrix.schema.type.ATPPerson
policy: ru.commons.matrix.schema.policy.ALCPerson
Et la réponse correspondante, incluant le statut d'exécution et l'ID de l'objet ou un message d'erreur :
#response
---
- createObject:
status: 200
message: null
oid: "f4ba679e-9253-4a83-a390-44daf7ac7756"
- createObject:
status: 500
message: "Admin type ru.commons.matrix.schema.type.ATPPerson1 not found. Enter a correct name or contact the administrator."
- createObject:
status: 200
message: null
oid: "d9c98e74-bd17-4b3c-ae07-d9c307151c74"
Ce format permet une génération facile des commandes, par exemple, depuis Excel, à l'aide de formules simples :
="- createObject:
id: "&K2&"
type: "&I2&"
policy: "&J2&"
code: "&B2&"
title: '"&C2&"'"
Cette approche n'est pas un DSL (Domain Specific Language) à part entière, mais elle exploite les DTO (Data Transfer Objects) existants de l'API REST, enveloppés dans des commandes d'action, garantissant flexibilité et réutilisabilité.
Implémentation Technique avec Java et le Framework Spring
Le cœur du service est le JQLController, qui traite les requêtes YAML entrantes.
@RestController
@RequestMapping("jql")
@RequiredArgsConstructor
public class JQLController {
/**
* Exécute un lot de commandes.
*
* LinkedHashMap est utilisé pour préserver l'ordre des commandes d'entrée et de sortie
* - les commandes d'entrée sont traitées dans l'ordre où elles apparaissent dans YAML
* - les réponses sont renvoyées dans le même ordre que les requêtes
* - ceci est crucial pour les scénarios où l'ordre d'exécution est important (créer → connecter)
* Jackson utilise LinkedHashMap par défaut, mais une spécification explicite
* protège contre les changements accidentels d'implémentation à l'avenir.
*/
@PostMapping(consumes = "application/yaml", produces = "application/yaml")
public ResponseEntity<List<LinkedHashMap<String, JQLResponseData>>> promote(@JPathContextVariable JContext ctx,
@RequestBody List<LinkedHashMap<String, Object>> commands) {
List<LinkedHashMap<String, JQLResponseData>> results = new ArrayList<>(commands.size());
for (Map<String, Object> command : commands) {
Map<JQLEnum, IJQLDTO> parsed = fromYaml(command);
//une erreur dans une commande n'entraîne pas l'échec de l'ensemble du lot
//`run` en tient compte, donc un bloc try-catch n'est pas nécessaire ici
results.add(run(ctx, parsed));
}
return ResponseEntity.ok(results);
}
private static Map<JQLEnum, IJQLDTO> fromYaml(Map<String, Object> commands) {
Map<JQLEnum, IJQLDTO> command = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : commands.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
JQLEnum jqlEnum = JQLEnum.valueOf(key);
Class<? extends IJQLDTO> dtoClass = jqlEnum.getDTOClass();
IJQLDTO dto = JObjectJSON.MAPPER.convertValue(value, dtoClass);
command.put(jqlEnum, dto);
}
return command;
}
/**
* Exécute les commandes et renvoie le résultat dans le même format.
*
* Entrée : { "createObject": { "type": "...", "policy": "..." } }
* Sortie : { "createObject": { "status": 200, "id": "..." } }
*
* ou
*
* Sortie : { "createObject": { "status": 500, "message": "..." } }
*/
private static LinkedHashMap<String, JQLResponseData> run(JContext ctx, Map<JQLEnum, IJQLDTO> commands) {
LinkedHashMap<String, JQLResponseData> response = new LinkedHashMap<>();
if (commands.isEmpty()) {
return response;
}
try {
ctx.getTxUpdate().executeWithoutResult(tx -> {
for (Map.Entry<JQLEnum, IJQLDTO> entry : commands.entrySet()) {
response.put(entry.getKey().name(), entry.getKey().execute(ctx, entry.getValue()));
}
});
} catch (JMatrixLocalizedError ex) {
commands.keySet().forEach(el -> {
response.put(el.name(), new JQLResponseData(500, ex.getLocalizedMessage(ctx.getLocale())));
});
} catch (Exception ex) {
commands.keySet().forEach(el -> {
response.put(el.name(), new JQLResponseData(500, ex.getMessage()));
});
}
return response;
}
}
Le JQLController utilise les annotations Spring pour gérer les requêtes POST avec un type de contenu application/yaml. Une caractéristique clé est l'utilisation de LinkedHashMap pour préserver l'ordre des commandes, ce qui est essentiel pour les scénarios où la séquence des opérations est importante (par exemple, créer un objet avant de le lier). La méthode fromYaml est responsable de la désérialisation des objets YAML en DTO correspondants, en utilisant JQLEnum pour déterminer le type de commande et la classe DTO. La méthode run exécute les commandes au sein d'une transaction, gérant les erreurs et formant une réponse YAML structurée.
Configuration Spring pour le Support YAML
Étant donné que le framework Spring ne prend pas en charge nativement application/yaml comme type de contenu, une configuration HttpMessageConverter supplémentaire est nécessaire.
@Configuration
public class YamlConfig {
@Bean
public YamlHttpMessageConverter yamlHttpMessageConverter() {
YAMLFactory factory = new YAMLFactory();
//.disable(YAMLGenerator.Feature.SPLIT_LINES)
//.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
YAMLMapper mapper = new YAMLMapper(factory);
return new YamlHttpMessageConverter(mapper);
}
public static class YamlHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
public YamlHttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper,
MediaType.parseMediaType("application/yaml"),
MediaType.parseMediaType("application/x-yaml"),
MediaType.parseMediaType("text/yaml"));
}
}
}
YamlConfig définit un bean YamlHttpMessageConverter, qui étend AbstractJackson2HttpMessageConverter et enregistre un YAMLMapper pour gérer les formats YAML (application/yaml, application/x-yaml, text/yaml). Cela permet à Spring de marshaler et unmarshaler automatiquement les données YAML au sein des objets contrôleur.
Structure des DTO et Énumération des Commandes
Pour standardiser les réponses, un DTO de base JQLResponseData a été développé :
@Getter
public class JQLResponseData {
private int status = 200;
private String message = null;
public JQLResponseData() {
}
public JQLResponseData(int status, String message) {
this.status = status;
this.message = message;
}
}
Cette classe fournit un format cohérent pour le statut d'exécution et les messages d'erreur.
Initialement, pour le MVP, l'ensemble des commandes a été implémenté via l'énumération JQLEnum, ce qui a simplifié le développement. Cependant, dans des systèmes plus matures, cela pourrait être remplacé par un mécanisme plus flexible pour l'enregistrement des classes de commande.
public enum JQLEnum {
createObject {
@Override
JQLResponseData execute(JContext ctx, IJQLDTO value) {
JDTODomainObject dto = (JDTODomainObject) value;
JDomainObject object;
if (dto.getId() == null) {
object = new JDomainObject();
} else {
object = new JDomainObject(dto.getId());
}
dto.unmap(object);
object.create(ctx, JModel.getRequiredAdminByName(dto.getType()), JModel.getRequiredAdminByName(dto.getPolicy()));
return new CreateDomainRS(object.getId());
}
@Override
public Class<? extends IJQLDTO> getDTOClass() {
return JDTODomainObject.class;
}
},
deleteObject {
@Override
JQLResponseData execute(JContext ctx, IJQLDTO value) {
DeleteDomainRQ dto = (DeleteDomainRQ) value;
new JDomainObject(dto.getId()).delete(ctx);
return new JQLResponseData();
}
@Override
public Class<? extends IJQLDTO> getDTOClass() {
return DeleteDomainRQ.class;
}
},
//etc.
abstract JQLResponseData execute(JContext ctx, IJQLDTO value);
public abstract Class<? extends IJQLDTO> getDTOClass();
}
Chaque élément de JQLEnum encapsule la logique d'exécution d'une commande spécifique (execute) et fournit une méthode pour récupérer la classe DTO correspondante (getDTOClass). Cela permet une gestion centralisée des opérations disponibles et de leur traitement.
Interface Utilisateur avec Vue.js
Pour interagir avec le service YAML, une interface web simple a été développée en utilisant Vue.js. Elle comporte deux zones : une pour saisir les requêtes YAML et une autre pour afficher les résultats. La bibliothèque ace-builds offre une édition pratique du code YAML avec coloration syntaxique.
<template>
<div class="jql-base-div">
<div ref="refRequests" class="jql-requests-div" @keyup.alt.enter="handleAltEnter"></div>
<div ref="refResults" class="jql-response-div"></div>
</div>
</template>
<script setup>
import { useJServices } from '@/composables/useJServices';
const { serviceFetch } = useJServices()
import ace from 'ace-builds';
import 'ace-builds/src-noconflict/mode-yaml';
import 'ace-builds/src-noconflict/theme-chrome';
import { onMounted, ref } from 'vue';
ace.config.set('basePath', '/ace')
ace.config.set('workerPath', '/ace')
ace.config.set('themePath', '/ace')
const props = defineProps({
routeParams: Object,
routeQuery: Object,
requestBody: Object,
metaComponent: Object
})
const refRequests = ref(null)
const refResults = ref(null)
let aceEditorRequests = null
let aceEditorResponse = null
onMounted(() => {
document.title = 'JMatrix: JQL'
aceEditorRequests = ace.edit(refRequests.value)
aceEditorRequests.setTheme("ace/theme/chrome")
aceEditorRequests.session.setMode("ace/mode/yaml")
aceEditorRequests.setOptions({
fontSize: "13px",
showPrintMargin: false,
r
L'interface fournit un environnement interactif pour tester et exécuter des commandes YAML, permettant aux développeurs et aux analystes de vérifier rapidement les résultats des opérations et d'ajuster les requêtes. L'utilisation de ace-builds améliore considérablement la commodité de travailler avec les structures YAML.
Avantages et Développements Futurs
L'implémentation du service YAML a considérablement réduit le temps nécessaire au chargement des données historiques, le faisant passer de plusieurs jours à quelques minutes. Cette solution a non seulement amélioré l'efficacité des personnalisations avant-vente, mais a également considérablement élargi les capacités des analystes en leur fournissant un outil pour travailler de manière autonome sur les données, sans l'implication directe des développeurs.
Les avantages clés de cette approche :
- Rapidité et Efficacité : Chargement rapide des données grâce à un format simplifié et au traitement par lots.
- Flexibilité : Génération facile des requêtes à partir de diverses sources (Excel, LLM) sans codage complexe.
- Autonomie des Analystes : Dépendance réduite vis-à-vis des développeurs pour les opérations d'importation de données de routine.
- Propreté Architecturale : Utilisation des DTO existants et préservation de la logique métier au niveau de l'application.
Les considérations pour les développements futurs incluent :
- Refactoriser
JQLEnumvers un mécanisme d'enregistrement de commandes plus dynamique, utilisant la réflexion ou la configuration, pour simplifier l'ajout de nouvelles opérations. - Étendre l'interface utilisateur avec des capacités de validation de schéma YAML et un traitement de réponse plus sophistiqué.
- Intégration avec des systèmes de contrôle de version pour suivre les modifications des données importées.
Cette approche démontre comment une architecture réfléchie et la sélection de technologies appropriées peuvent résoudre des défis complexes d'importation de données, augmentant significativement la productivité des équipes et la flexibilité du système.
Points Clés à Retenir
- Problème : Les méthodes d'importation de données traditionnelles (API REST, SQL, utilitaires natifs) sont inefficaces pour le chargement ponctuel de données historiques dans les systèmes d'entreprise "code-first", nécessitant souvent des jours de travail.
- Solution : Création d'un service léger, orienté YAML, utilisant Java (Framework Spring) et Vue.js pour le traitement des commandes par lots.
- Choix de YAML : YAML a été préféré au JSON en raison de sa concision, de sa facilité de génération (depuis Excel, LLM) et de sa lisibilité pour les spécialistes techniques.
- Architecture : Le service utilise
JQLControllerpour traiter les requêtes YAML,YamlHttpMessageConverterpour l'intégration Spring, des DTO standardisés (JQLResponseData) et une énumération pour l'exécution des commandes. - Résultat : Temps d'importation des données réduit de plusieurs jours à quelques minutes, autonomie accrue des analystes et flexibilité améliorée du système tout en maintenant l'intégrité de la logique métier.
— Editorial Team
Aucun commentaire pour le moment.