Effizienter Datenimport im Unternehmen: Ein YAML-basierter Service mit Java und Vue
In modernen Unternehmenssystemen stellt die Aufgabe, große Mengen historischer oder initialer Daten schnell und effizient zu importieren, oft eine erhebliche Herausforderung dar. Traditionelle Ansätze, die umständlichen Code oder komplexe Konfigurationen erfordern, können den Prozess um Tage verlängern. Dieser Artikel untersucht eine innovative Methode zur Erstellung eines schlanken, YAML-orientierten Dienstes unter Verwendung eines Java- und Vue.js-Stacks. Ziel ist es, die Datenladezeiten auf wenige Minuten zu reduzieren und so die Interaktion von technischen Spezialisten mit der Plattform und ihren Inhalten erheblich zu vereinfachen.
Die Entwicklung der neuen JMatrixPlatform, die auf dem Prinzip "everything as code" basiert, deckte ein kritisches Problem auf: das Fehlen eines bequemen Tools für einmalige Daten-Uploads. Im Gegensatz zu älteren Systemen, bei denen der Datenimport eine triviale Aufgabe war, erforderte die neue Architektur für jede Operation das Schreiben von Java-Code, was den Prozess mühsam und unflexibel machte. Analysten verloren die Fähigkeit, Daten unabhängig zu verwalten, und wurden vollständig von den Entwicklern abhängig.
Mehrere Standardansätze wurden in Betracht gezogen, jeder mit erheblichen Nachteilen:
- Erweiterung der REST-API um Batch-Dienste: Dies hätte eine komplexe Umwandlung von Excel-Daten in JSON-Arrays erforderlich gemacht, einschließlich des Escapings von Anführungszeichen innerhalb von Excel-Formeln, was an sich schon eine nicht-triviale Aufgabe ist. Die Verwendung externer Tools wie Postman hätte den Prozess zusätzlich verkompliziert.
- Verwendung von
curl + jsonfür jedes Objekt: Dies hätte zusätzliche Skripte für die Token-Abfrage erfordert und keine Fehlertoleranz geboten, wodurch der Import beim ersten fehlerhaften Datensatz gestoppt worden wäre. - Direkter Import über SQL: Dieser Ansatz wurde ausgeschlossen, da die gesamte Dateninteraktionslogik auf Anwendungsebene implementiert ist. Ein direkter Datenbankeingriff, der die Geschäftslogik der Plattform umgeht, könnte zu Dateninkonsistenzen führen und die Systemintegrität gefährden.
- Entwicklung eines nativen Excel-Imports für jede Entität: Dies wurde für einmalige Importe als wirtschaftlich unrentabel und zeitaufwendig erachtet.
Der Bedarf an einem schnellen und flexiblen Tool, das technischen Spezialisten die unabhängige Verwaltung von Daten-Uploads ermöglicht, wurde offensichtlich.
Lösung: Ein YAML-orientierter Dienst für Batch-Uploads
Angesichts der Einschränkungen bestehender Ansätze kam der Autor zu dem Schluss, dass ein minimalistischer Dienst benötigt wurde, der einen einzigen Einstiegspunkt (Endpoint) bietet, der strukturierte Daten akzeptieren kann, um verschiedene Operationen (Erstellen, Ändern, Löschen) durchzuführen. Anstelle von JSON wurde YAML aufgrund seiner Prägnanz und der einfachen manuellen Generierung oder automatisierten Erstellung mithilfe einfacher Excel-Formeln oder LLMs gewählt.
Ein YAML-Anfragebeispiel demonstriert seine Einfachheit und Lesbarkeit:
#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
Und die entsprechende Antwort, einschließlich Ausführungsstatus und Objekt-ID oder einer Fehlermeldung:
#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"
Dieses Format ermöglicht eine einfache Befehlsgenerierung, beispielsweise aus Excel, mithilfe einfacher Formeln:
="- createObject:
id: "&K2&"
type: "&I2&"
policy: "&J2&"
code: "&B2&"
title: '"&C2&"'"
Dieser Ansatz ist keine vollwertige DSL (Domain Specific Language), sondern nutzt bestehende REST API DTOs (Data Transfer Objects), die in Aktionsbefehle verpackt sind, um Flexibilität und Wiederverwendbarkeit zu gewährleisten.
Technische Implementierung mit Java und dem Spring Framework
Der Kern des Dienstes ist der JQLController, der eingehende YAML-Anfragen verarbeitet.
@RestController
@RequestMapping("jql")
@RequiredArgsConstructor
public class JQLController {
/**
* Executes a batch of commands.
*
* LinkedHashMap is used to preserve the order of input and output commands
* - input commands are processed in the order they appear in YAML
* - responses are returned in the same order as requests
* - this is crucial for scenarios where execution order matters (create → connect)
* Jackson uses LinkedHashMap by default, but explicit specification
* protects against accidental implementation changes in the future.
*/
@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);
//an error in a command does not cause the entire batch to fail
//run accounts for this, so a try-catch here is not needed
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;
}
/**
* Executes commands and returns the result in the same format.
*
* Input: { "createObject": { "type": "...", "policy": "..." } }
* Output: { "createObject": { "status": 200, "id": "..." } }
*
* or
*
* Output: { "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;
}
}
Der JQLController verwendet Spring-Annotationen, um POST-Anfragen mit dem Content-Type application/yaml zu verarbeiten. Ein Hauptmerkmal ist die Verwendung von LinkedHashMap zur Beibehaltung der Befehlsreihenfolge, was für Szenarien entscheidend ist, in denen die Reihenfolge der Operationen wichtig ist (z. B. das Erstellen eines Objekts vor dessen Verknüpfung). Die Methode fromYaml ist für die Deserialisierung von YAML-Objekten in entsprechende DTOs verantwortlich, wobei JQLEnum zur Bestimmung des Befehlstyps und der DTO-Klasse verwendet wird. Die Methode run führt Befehle innerhalb einer Transaktion aus, verarbeitet Fehler und erstellt eine strukturierte YAML-Antwort.
Spring-Konfiguration für YAML-Unterstützung
Da das Spring Framework application/yaml nicht nativ als Content-Type unterstützt, ist eine zusätzliche HttpMessageConverter-Konfiguration erforderlich.
@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 definiert einen YamlHttpMessageConverter-Bean, der AbstractJackson2HttpMessageConverter erweitert und einen YAMLMapper registriert, um YAML-Formate (application/yaml, application/x-yaml, text/yaml) zu verarbeiten. Dies ermöglicht Spring, YAML-Daten innerhalb von Controller-Objekten automatisch zu marshallen und unmarshallen.
DTO-Struktur und Befehls-Enum
Um Antworten zu standardisieren, wurde ein Basis-DTO JQLResponseData entwickelt:
@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;
}
}
Diese Klasse bietet ein konsistentes Format für den Ausführungsstatus und Fehlermeldungen.
Anfänglich, für das MVP (Minimum Viable Product), wurde der Befehlssatz über das enum JQLEnum implementiert, was die Entwicklung vereinfachte. In ausgereifteren Systemen könnte dies jedoch durch einen flexibleren Mechanismus zur Registrierung von Befehlsklassen ersetzt werden.
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();
}
Jedes JQLEnum-Element kapselt die Logik zur Ausführung eines spezifischen Befehls (execute) und bietet eine Methode zum Abrufen der entsprechenden DTO-Klasse (getDTOClass). Dies ermöglicht eine zentralisierte Verwaltung der verfügbaren Operationen und deren Verarbeitung.
Benutzeroberfläche mit Vue.js
Zur Interaktion mit dem YAML-Dienst wurde eine einfache Weboberfläche mit Vue.js entwickelt. Sie verfügt über zwei Bereiche: einen zur Eingabe von YAML-Anfragen und einen weiteren zur Anzeige der Ergebnisse. Die Bibliothek ace-builds bietet eine bequeme YAML-Codebearbeitung mit Syntaxhervorhebung.
<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
Die Oberfläche bietet eine interaktive Umgebung zum Testen und Ausführen von YAML-Befehlen, wodurch Entwickler und Analysten die Ergebnisse von Operationen schnell überprüfen und Anfragen anpassen können. Die Verwendung von ace-builds verbessert die Benutzerfreundlichkeit beim Arbeiten mit YAML-Strukturen erheblich.
Vorteile und zukünftige Entwicklung
Die Implementierung des YAML-Dienstes hat die für das Laden historischer Daten erforderliche Zeit drastisch reduziert, von Tagen auf Minuten. Diese Lösung hat nicht nur die Effizienz der Pre-Sales-Anpassungen verbessert, sondern auch die Fähigkeiten der Analysten erheblich erweitert, indem sie ihnen ein Tool für die unabhängige Datenarbeit ohne direkte Beteiligung von Entwicklern zur Verfügung stellt.
Hauptvorteile dieses Ansatzes:
- Geschwindigkeit und Effizienz: Schnelles Laden von Daten dank eines vereinfachten Formats und der Stapelverarbeitung.
- Flexibilität: Einfache Generierung von Anfragen aus verschiedenen Quellen (Excel, LLM) ohne komplexe Codierung.
- Autonomie der Analysten: Reduzierte Abhängigkeit von Entwicklern für routinemäßige Datenimportoperationen.
- Architektonische Sauberkeit: Nutzung bestehender DTOs und Beibehaltung der Geschäftslogik auf Anwendungsebene.
Überlegungen zur zukünftigen Entwicklung umfassen:
- Refactoring von
JQLEnumzu einem dynamischeren Mechanismus zur Befehlsregistrierung, unter Verwendung von Reflection oder Konfiguration, um das Hinzufügen neuer Operationen zu vereinfachen. - Erweiterung der Benutzeroberfläche um YAML-Schema-Validierungsfunktionen und eine ausgefeiltere Antwortverarbeitung.
- Integration mit Versionskontrollsystemen zur Nachverfolgung von Änderungen in importierten Daten.
Dieser Ansatz zeigt, wie durchdachte Architektur und die Auswahl geeigneter Technologien komplexe Datenimport-Herausforderungen lösen und die Teamproduktivität sowie die Systemflexibilität erheblich steigern können.
Wichtigste Erkenntnisse
- Problem: Traditionelle Datenimportmethoden (REST-API, SQL, native Dienstprogramme) sind für einmalige historische Datenladungen in "Code-First"-Unternehmenssystemen ineffizient und erfordern oft tagelange Arbeit.
- Lösung: Erstellung eines schlanken, YAML-orientierten Dienstes unter Verwendung von Java (Spring Framework) und Vue.js für die Batch-Befehlsverarbeitung.
- Wahl von YAML: YAML wurde gegenüber JSON aufgrund seiner Prägnanz, einfachen Generierung (aus Excel, LLM) und Lesbarkeit für technische Spezialisten bevorzugt.
- Architektur: Der Dienst verwendet
JQLControllerzur Verarbeitung von YAML-Anfragen,YamlHttpMessageConverterfür die Spring-Integration, standardisierte DTOs (JQLResponseData) und einenumfür die Befehlsausführung. - Ergebnis: Reduzierung der Datenimportzeit von Tagen auf Minuten, erhöhte Autonomie der Analysten und verbesserte Systemflexibilität bei gleichzeitiger Wahrung der Geschäftslogik-Integrität.
— Editorial Team
Noch keine Kommentare.