Szybki import danych w systemach klasy enterprise: podejście YAML z Java i Vue
W nowoczesnych systemach klasy enterprise zadanie szybkiego i efektywnego importu dużych wolumenów danych historycznych lub początkowych często staje się poważnym wyzwaniem. Tradycyjne podejścia, wymagające pisania obszernego kodu lub skomplikowanej konfiguracji, mogą wydłużać proces do wielu dni. Ten artykuł bada innowacyjną metodę tworzenia lekkiego serwisu opartego na YAML, wykorzystującego stos technologiczny Java i Vue.js, który pozwala skrócić czas ładowania danych do zaledwie kilku minut, znacznie upraszczając interakcję specjalistów technicznych z platformą i jej zawartością.
Rozwój nowej platformy JMatrixPlatform, opartej na zasadzie "wszystko jest kodem", ujawnił krytyczny problem: brak wygodnego narzędzia do jednorazowego ładowania danych. W przeciwieństwie do przestarzałych systemów, gdzie import danych był zadaniem trywialnym, nowa architektura wymagała pisania kodu Java dla każdej operacji, co czyniło proces czasochłonnym i nieelastycznym. Analitycy tracili możliwość samodzielnego zarządzania danymi, będąc całkowicie zależnymi od deweloperów.
Rozważano kilka standardowych podejść, z których każde miało znaczące wady:
- Uzupełnienie REST API o serwisy wsadowe: Wymagałoby skomplikowanej konwersji danych z Excela do tablic JSON, włączając w to ekranowanie cudzysłowów w formułach Excela, co samo w sobie jest zadaniem nietrywialnym. Użycie zewnętrznych narzędzi, takich jak Postman, dodatkowo skomplikowałoby proces.
- Użycie
curl + jsondla każdego obiektu: Wymagałoby dodatkowego skryptowania do uzyskiwania tokenów i nie zapewniałoby odporności na błędy, przerywając import przy pierwszym nieprawidłowym rekordzie. - Bezpośredni import przez SQL: Był wykluczony, ponieważ cała logika pracy z danymi jest zaimplementowana na poziomie aplikacji. Bezpośrednia ingerencja w bazę danych z pominięciem logiki biznesowej platformy mogłaby prowadzić do niespójności danych i naruszenia integralności systemu.
- Pisanie natywnego importu z Excela dla każdej encji: Było to rozwiązanie nieopłacalne ekonomicznie i czasochłonne dla jednorazowych importów.
Potrzeba szybkiego i elastycznego narzędzia, umożliwiającego specjalistom technicznym samodzielne zarządzanie ładowaniem danych, stała się oczywista.
Rozwiązanie: Serwis oparty na YAML do ładowania wsadowego
Uświadamiając sobie ograniczenia istniejących podejść, autor doszedł do wniosku, że potrzebny jest minimalistyczny serwis z jednym punktem końcowym (endpoint), zdolny do przyjmowania ustrukturyzowanych danych w celu wykonywania różnych operacji (tworzenie, modyfikacja, usuwanie). Zamiast JSON, wybór padł na YAML ze względu na jego zwięzłość i wygodę w ręcznym generowaniu lub automatycznym tworzeniu za pomocą prostych formuł Excela lub LLM.
Przykład zapytania YAML demonstruje jego prostotę i czytelność:
#zapytanie
- 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
I odpowiadająca mu odpowiedź, zawierająca status wykonania oraz identyfikator obiektu lub komunikat o błędzie:
#odpowiedź
---
- createObject:
status: 200
message: null
oid: "f4ba679e-9253-4a83-a390-44daf7ac7756"
- createObject:
status: 500
message: "Typ administracyjny ru.commons.matrix.schema.type.ATPPerson1 nie znaleziony. Wprowadź poprawną nazwę lub skontaktuj się z administratorem."
- createObject:
status: 200
message: null
oid: "d9c98e74-bd17-4b3c-ae07-d9c307151c74"
Taki format pozwala łatwo generować polecenia, na przykład z Excela, używając prostych formuł:
="- createObject:
id: "&K2&"
type: "&I2&"
policy: "&J2&"
code: "&B2&"
title: '"&C2&"'"
To podejście nie jest pełnoprawnym DSL (Domain Specific Language), ale wykorzystuje istniejące DTO (Data Transfer Objects) REST API, opakowane w polecenia-akcje, co zapewnia elastyczność i możliwość ponownego użycia.
Implementacja techniczna w Java i Spring Framework
Podstawą serwisu jest kontroler JQLController, który przetwarza przychodzące zapytania YAML.
@RestController
@RequestMapping("jql")
@RequiredArgsConstructor
public class JQLController {
/**
* Wykonuje pakiet poleceń.
*
* Używa LinkedHashMap do zachowania kolejności poleceń wejściowych i wyjściowych
* - polecenia wejściowe są przetwarzane w kolejności, w jakiej zostały określone w YAML
* - odpowiedzi są zwracane w tej samej kolejności, co zapytania
* - jest to ważne w scenariuszach, gdzie kolejność wykonania ma znaczenie (create → connect)
* Jackson domyślnie używa LinkedHashMap, ale jawne określenie
* chroni przed przypadkową zmianą implementacji w przyszłości.
*/
@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);
//błąd w poleceniu nie powoduje awarii całego pakietu
//run to uwzględnia i tutaj try catch nie jest potrzebny
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;
}
/**
* Wykonuje polecenia i zwraca wynik w tym samym formacie.
*
* Wejście: { "createObject": { "type": "...", "policy": "..." } }
* Wyjście: { "createObject": { "status": 200, "id": "..." } }
*
* lub
*
* Wyjście: { "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;
}
}
Kontroler JQLController wykorzystuje adnotacje Spring do obsługi żądań POST z typem zawartości application/yaml. Ważną cechą jest użycie LinkedHashMap do zachowania kolejności poleceń, co jest kluczowe w scenariuszach, gdzie sekwencja operacji ma znaczenie (np. tworzenie obiektu przed jego powiązaniem). Metoda fromYaml odpowiada za deserializację obiektów YAML do odpowiednich DTO, używając JQLEnum do określenia typu polecenia i klasy DTO. Metoda run wykonuje polecenia w ramach transakcji, obsługując błędy i generując ustrukturyzowaną odpowiedź YAML.
Konfiguracja Spring do pracy z YAML
Ponieważ Spring Framework domyślnie nie obsługuje application/yaml jako typu zawartości, wymagana jest dodatkowa konfiguracja HttpMessageConverter.
@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 definiuje bean YamlHttpMessageConverter, który rozszerza AbstractJackson2HttpMessageConverter i rejestruje YAMLMapper do obsługi formatów YAML (application/yaml, application/x-yaml, text/yaml). Pozwala to Springowi automatycznie marszalować i demarszalować dane YAML w obiektach kontrolera.
Struktura DTO i Enum poleceń
Dla standaryzacji odpowiedzi opracowano bazowe DTO JQLResponseData:
@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;
}
}
Ta klasa zapewnia jednolity format dla statusu wykonania i komunikatów o błędach.
Początkowo dla MVP zestaw poleceń zaimplementowano poprzez enum JQLEnum, co uprościło rozwój, ale w bardziej dojrzałych systemach może zostać zastąpione bardziej elastycznym mechanizmem rejestracji klas poleceń.
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;
}
},
//itd.
abstract JQLResponseData execute(JContext ctx, IJQLDTO value);
public abstract Class<? extends IJQLDTO> getDTOClass();
}
Każdy element JQLEnum hermetyzuje logikę wykonania konkretnego polecenia (execute) i udostępnia metodę do pobierania odpowiedniej klasy DTO (getDTOClass). Pozwala to na scentralizowane zarządzanie dostępnymi operacjami i ich przetwarzaniem.
Interfejs użytkownika w Vue.js
Dla interakcji z serwisem YAML opracowano prosty interfejs webowy w Vue.js. Składa się on z dwóch obszarów: jednego do wprowadzania zapytań YAML, drugiego do wyświetlania wyników. Użycie biblioteki ace-builds zapewnia wygodną edycję kodu YAML z podświetlaniem składni.
<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
Interfejs zapewnia interaktywne środowisko do testowania i wykonywania poleceń YAML, umożliwiając deweloperom i analitykom szybkie sprawdzanie wyników operacji i korygowanie zapytań. Użycie ace-builds znacząco zwiększa komfort pracy ze strukturami YAML.
Zalety i dalszy rozwój
Wdrożenie serwisu YAML pozwoliło radykalnie skrócić czas potrzebny na ładowanie danych historycznych, z dni do minut. To rozwiązanie nie tylko zwiększyło szybkość realizacji personalizacji przedsprzedażowych, ale także znacząco poszerzyło możliwości analityków, udostępniając im narzędzie do samodzielnej pracy z danymi bez bezpośredniego udziału deweloperów.
Główne zalety tego podejścia:
- Szybkość i efektywność: Szybkie ładowanie danych dzięki uproszczonemu formatowi i przetwarzaniu wsadowemu.
- Elastyczność: Możliwość łatwego generowania zapytań z różnych źródeł (Excel, LLM) bez skomplikowanego kodowania.
- Samodzielność analityków: Zmniejszenie zależności od deweloperów w zakresie rutynowych operacji importu danych.
- Czystość architektury: Wykorzystanie istniejących DTO i zachowanie logiki biznesowej na poziomie aplikacji.
W ramach dalszego rozwoju można rozważyć:
- Refaktoryzację
JQLEnumna bardziej dynamiczny mechanizm rejestracji poleceń, wykorzystujący refleksję lub konfigurację, w celu uproszczenia dodawania nowych operacji. - Rozszerzenie interfejsu użytkownika o możliwości walidacji schematów YAML i bardziej złożone przetwarzanie odpowiedzi.
- Integrację z systemami kontroli wersji w celu śledzenia zmian w importowanych danych.
To podejście pokazuje, jak dzięki przemyślanej architekturze i wyborowi odpowiednich technologii można rozwiązać złożone zadania importu danych, znacząco zwiększając produktywność zespołu i elastyczność systemu.
Kluczowe wnioski
- Problem: Tradycyjne metody importu danych (REST API, SQL, natywne narzędzia) są nieefektywne do jednorazowego ładowania danych historycznych w systemach enterprise typu "code-first", wymagając dni pracy.
- Rozwiązanie: Stworzenie lekkiego serwisu opartego na YAML w Java (Spring Framework) i Vue.js do wsadowego przetwarzania poleceń.
- Wybór YAML: Preferencja YAML nad JSON wynika z jego zwięzłości, łatwości generowania (z Excela, LLM) i czytelności dla specjalistów technicznych.
- Architektura: Serwis wykorzystuje
JQLControllerdo przetwarzania zapytań YAML,YamlHttpMessageConverterdo integracji ze Springiem, standaryzowane DTO (JQLResponseData) orazenumdo wykonywania poleceń. - Rezultat: Skrócenie czasu importu danych z dni do minut, zwiększenie samodzielności analityków i elastyczności systemu przy zachowaniu integralności logiki biznesowej.
— Editorial Team
Brak komentarzy.