Volver al inicio

Servicio YAML en Java/Vue: Importación rápida de datos

Aprende cómo crear un servicio YAML simple en Java y Vue.js para la carga eficiente y rápida de datos históricos en sistemas empresariales, evitando APIs complejas y SQL.

Importación rápida de datos en sistemas empresariales: Enfoque YAML con Java y Vue.js
Advertisement 728x90

Acelerando la Importación de Datos Empresariales: Un Servicio Basado en YAML con Java y Vue

En los sistemas empresariales modernos, la tarea de importar grandes volúmenes de datos históricos o iniciales de forma rápida y eficiente a menudo presenta un desafío significativo. Los enfoques tradicionales, que exigen código engorroso o configuraciones complejas, pueden prolongar el proceso durante días. Este artículo explora un método innovador para crear un servicio ligero y orientado a YAML utilizando un stack de Java y Vue.js, diseñado para reducir los tiempos de carga de datos a meros minutos, simplificando así significativamente la interacción de los especialistas técnicos con la plataforma y su contenido.

El desarrollo de la nueva JMatrixPlatform, construida bajo el principio de "todo como código", puso de manifiesto un problema crítico: la falta de una herramienta conveniente para cargas de datos únicas. A diferencia de los sistemas antiguos donde la importación de datos era una tarea trivial, la nueva arquitectura requería escribir código Java para cada operación, lo que hacía el proceso laborioso e inflexible. Los analistas perdieron la capacidad de gestionar datos de forma independiente, volviéndose completamente dependientes de los desarrolladores.

Se consideraron varios enfoques estándar, cada uno con inconvenientes significativos:

Google AdInline article slot
  • Aumentar la API REST con servicios por lotes: Esto requeriría una conversión compleja de datos de Excel a arrays JSON, incluyendo el escape de comillas dentro de fórmulas de Excel, lo cual es una tarea no trivial en sí misma. El uso de herramientas externas como Postman complicaría aún más el proceso.
  • Usar curl + json para cada objeto: Esto requeriría scripting adicional para la recuperación de tokens y no ofrecería resiliencia a errores, deteniendo la importación en el primer registro incorrecto.
  • Importación directa vía SQL: Esto se descartó porque toda la lógica de interacción de datos se implementa a nivel de aplicación. La intervención directa en la base de datos, eludiendo la lógica de negocio de la plataforma, podría llevar a inconsistencia de datos y comprometer la integridad del sistema.
  • Desarrollar una importación nativa de Excel para cada entidad: Esto se consideró económicamente inviable y que consume mucho tiempo para importaciones únicas.

La necesidad de una herramienta rápida y flexible, que empodere a los especialistas técnicos para gestionar las cargas de datos de forma independiente, se hizo evidente.

Solución: Un Servicio Orientado a YAML para Cargas por Lotes

Reconociendo las limitaciones de los enfoques existentes, el autor concluyó que se necesitaba un servicio minimalista, con un único punto de entrada (endpoint) capaz de aceptar datos estructurados para realizar diversas operaciones (crear, modificar, eliminar). En lugar de JSON, se eligió YAML debido a su concisión y facilidad de generación manual o creación automatizada utilizando fórmulas simples de Excel o LLMs.

Un ejemplo de solicitud YAML demuestra su simplicidad y legibilidad:

Google AdInline article slot
#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

Y la respuesta correspondiente, incluyendo el estado de ejecución y el ID del objeto o un mensaje de error:

#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"

Este formato permite una fácil generación de comandos, por ejemplo, desde Excel, utilizando fórmulas simples:

="- createObject:
    id: "&K2&"
    type: "&I2&"
    policy: "&J2&"
    code: "&B2&"
    title: '"&C2&"'"

Este enfoque no es un DSL (Lenguaje Específico de Dominio) completo, sino que aprovecha los DTOs (Objetos de Transferencia de Datos) existentes de la API REST envueltos en comandos de acción, asegurando flexibilidad y reutilización.

Google AdInline article slot

Implementación Técnica con Java y Spring Framework

El núcleo del servicio es el JQLController, que procesa las solicitudes YAML entrantes.

@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;
  }
}

El JQLController utiliza anotaciones de Spring para manejar solicitudes POST con un tipo de contenido application/yaml. Una característica clave es el uso de LinkedHashMap para preservar el orden de los comandos, lo cual es crucial para escenarios donde la secuencia de operaciones importa (por ejemplo, crear un objeto antes de vincularlo). El método fromYaml es responsable de deserializar objetos YAML en los DTOs correspondientes, utilizando JQLEnum para determinar el tipo de comando y la clase DTO. El método run ejecuta comandos dentro de una transacción, manejando errores y formando una respuesta YAML estructurada.

Configuración de Spring para Soporte YAML

Dado que Spring Framework no soporta nativamente application/yaml como tipo de contenido, se requiere una configuración adicional de 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 define un bean YamlHttpMessageConverter, que extiende AbstractJackson2HttpMessageConverter y registra un YAMLMapper para manejar formatos YAML (application/yaml, application/x-yaml, text/yaml). Esto permite a Spring serializar y deserializar automáticamente datos YAML dentro de los objetos del controlador.

Estructura DTO y Enum de Comandos

Para estandarizar las respuestas, se desarrolló un DTO base 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;
  }

}

Esta clase proporciona un formato consistente para el estado de ejecución y los mensajes de error.

Inicialmente, para el MVP (Producto Mínimo Viable), el conjunto de comandos se implementó a través de enum JQLEnum, lo que simplificó el desarrollo. Sin embargo, en sistemas más maduros, esto podría reemplazarse por un mecanismo más flexible para registrar clases de comandos.

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();
}

Cada elemento JQLEnum encapsula la lógica para ejecutar un comando específico (execute) y proporciona un método para recuperar la clase DTO correspondiente (getDTOClass). Esto permite una gestión centralizada de las operaciones disponibles y su procesamiento.

Interfaz de Usuario con Vue.js

Para interactuar con el servicio YAML, se desarrolló una interfaz web simple utilizando Vue.js. Cuenta con dos áreas: una para introducir solicitudes YAML y otra para mostrar los resultados. La biblioteca ace-builds proporciona una edición conveniente de código YAML con resaltado de sintaxis.

<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

La interfaz proporciona un entorno interactivo para probar y ejecutar comandos YAML, permitiendo a desarrolladores y analistas verificar rápidamente los resultados de las operaciones y ajustar las solicitudes. El uso de ace-builds mejora significativamente la comodidad de trabajar con estructuras YAML.

Ventajas y Desarrollo Futuro

La implementación del servicio YAML ha reducido drásticamente el tiempo requerido para la carga de datos históricos, de días a minutos. Esta solución no solo ha mejorado la eficiencia de las personalizaciones de preventa, sino que también ha ampliado significativamente las capacidades de los analistas al proporcionarles una herramienta para el trabajo de datos independiente sin la intervención directa de los desarrolladores.

Ventajas clave de este enfoque:

  • Velocidad y Eficiencia: Carga rápida de datos gracias a un formato simplificado y procesamiento por lotes.
  • Flexibilidad: Fácil generación de solicitudes desde diversas fuentes (Excel, LLM) sin necesidad de codificación compleja.
  • Autonomía del Analista: Menor dependencia de los desarrolladores para operaciones rutinarias de importación de datos.
  • Limpieza Arquitectónica: Utilización de DTOs existentes y preservación de la lógica de negocio a nivel de aplicación.

Consideraciones para el desarrollo futuro incluyen:

  • Refactorizar JQLEnum a un mecanismo de registro de comandos más dinámico, utilizando reflexión o configuración, para simplificar la adición de nuevas operaciones.
  • Expandir la UI con capacidades de validación de esquemas YAML y un procesamiento de respuestas más sofisticado.
  • Integración con sistemas de control de versiones para rastrear cambios en los datos importados.

Este enfoque demuestra cómo una arquitectura bien pensada y la selección de tecnologías apropiadas pueden resolver desafíos complejos de importación de datos, impulsando significativamente la productividad del equipo y la flexibilidad del sistema.

Puntos Clave

  • Problema: Los métodos tradicionales de importación de datos (API REST, SQL, utilidades nativas) son ineficientes para la carga única de datos históricos en sistemas empresariales "code-first", a menudo requiriendo días de trabajo.
  • Solución: Creación de un servicio ligero, orientado a YAML, utilizando Java (Spring Framework) y Vue.js para el procesamiento de comandos por lotes.
  • Elección de YAML: Se prefirió YAML sobre JSON por su concisión, facilidad de generación (desde Excel, LLM) y legibilidad para especialistas técnicos.
  • Arquitectura: El servicio utiliza JQLController para procesar solicitudes YAML, YamlHttpMessageConverter para la integración con Spring, DTOs estandarizados (JQLResponseData) y un enum para la ejecución de comandos.
  • Resultado: Reducción del tiempo de importación de datos de días a minutos, mayor autonomía del analista y mayor flexibilidad del sistema, manteniendo la integridad de la lógica de negocio.

— Editorial Team

Advertisement 728x90

Leer después