sábado, 26 de julio de 2025

Kubernetes en un vistazo

Continuamos con esta serie de entregas sobre Docker y Kubernetes

Según la documentación oficial Kubernetes (k8s) es una plataforma de código abierto para automatizar la implementación, el escalado y la administración de aplicaciones en contenedores.

Básicamente es una plataforma de código abierto que orquesta contenedores (como los de Docker) en grupos distribuidos de servidores.

¿Para qué sirve?

  • Escalar aplicaciones fácilmente (de 1 a 1,000 contenedores sin perder el control).
  • Distribuir carga entre servidores. 
  • Recuperarse automáticamente si algo falla.
  • Declarar el estado ideal (Kubernetes se encarga de mantenerlo).

Es como tener un director de orquesta que sabe cuándo y dónde deben sonar los instrumentos (contenedores) para que la sinfonía (tu app) no se detenga.

Comandos básicos

Observemos ahora algunos de los comandos más usados en Kubernetes.

Crear un Deployment que maneje un Pod:

$ kubectl create deployment hello-node --image=registry.k8s.io/echoserver:1.4

Ver el Deployment:

$ kubectl get deployments

Mostrar los contenedores que están corriendo en un clúster:

$ kubectl get pods

Mostrar los eventos del clúster:

$ kubectl get events

Mostrar la configuración:

$ kubectl config view

Crear o actualizar recursos desde un archivo YAML:

$ kubectl apply -f application.yaml

Mostrar información detallada del Pod:

$ kubectl describe pod mypod

Mostrar los logs del contenedor:

$ kubectl logs mypod

Mostrar todos los recursos:

$ kubectl get all

Eliminar recursos definidos en el archivo YAML:

$ kubectl delete -f application.yaml

Interacción con Docker

En una entrega anterior vimos el flujo de trabajo con Docker.

Ahora imaginemos que tienes una aplicación en un contenedor Docker. Para que Kubernetes la administre, se crea un archivo de configuración (deployment.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mi-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mi-app
  template:
    metadata:
      labels:
        app: mi-app
    spec:
      containers:
      - name: mi-app
        image: usuario/mi-imagen:latest
        ports:
        - containerPort: 80

Ahora desplegamos con Kubernetes:

$ kubectl apply -f deployment.yaml

Se podría usar Docker Swarm en lugar de Kubernetes por simplicidad y rapidez. Sin embargo, si tu proyecto es algo más complejo es mejor usar Kubernetes. Ya que es ideal para arquitecturas complejas, microservicios y entornos empresariales.

Este solo es un pequeño vistazo a lo que es Kubernetes. Continuaremos con este tema en próximas entregas.

Enlaces:

https://kubernetes.io/
https://codemonkeyjunior.blogspot.com/2025/07/docker-conociendo-el-flujo-de-trabajo.html
https://codemonkeyjunior.blogspot.com/2025/01/docker-en-palabras-sencillas.html
https://alquimistadecodigo.blogspot.com/2020/07/docker-imagenes-y-contenedores.html
https://hub.docker.com/
https://alquimistadecodigo.blogspot.com/2020/03/docker-instalacion.html
https://alquimistadecodigo.blogspot.com/2020/08/docker-instalando-una-imagen-de-nimrod.html

domingo, 20 de julio de 2025

Docker: conociendo el flujo de trabajo

 


En esta entrega veremos el flujo de trabajo en Docker.

Tomemos como referencia la siguiente imagen:

Todo inicia con nuestra aplicación (MyApp). Que es la que queremos subir a un contenedor Docker.

Para ello debemos crear un archivo llamado Dockerfile. El cual contendrá todas las dependencias, rutas y configuraciones necesarias para hacer funcionar nuestra aplicación.

Una vez teniendo este archivo, docker se encargará de crear la imagen, la cual es un paquete autocontenido con todo lo necesario para ejecutar la aplicación.

Posteriormente tendremos un contenedor, que es la instancia activa y aislada de la aplicación corriendo en el sistema.

Todo gracias al motor de Docker (Docker Engine), el componente que interpreta el Dockerfile, construye la imagen, lanza el contenedor y gestiona todo ese flujo. Es el corazón del ecosistema Docker.

El SO anfitrión donde reside Docker puede ser un SO Linux, Mac OS o Windows.

Básicamente este es flujo de trabajo de Docker. Omitiendo su interracción con Kubernetes.

Enlaces:

https://codemonkeyjunior.blogspot.com/2025/01/docker-en-palabras-sencillas.html
https://alquimistadecodigo.blogspot.com/2020/07/docker-imagenes-y-contenedores.html
https://hub.docker.com/

Las leyes (no escritas) de la programación (4ta parte)

En un mundo donde la I.A. ha dejado de ser un sueño o fantasía y es usada por la mayoría de las empresas de tecnología e IT el programador deberá saber adaptarse o morir.

Sin embargo, ¿La I.A. supone la muerte de la programación? No, ¡claro que no!

Quien quiera programar deberá saber programar. Así como quien quiera ser escritor deberá saber escribir.

Muchos creen que usar ChatGPT, Copilot, DeepSeek o cualquier otra inteligencia artificial automáticamente los convertirá en programadores. Se equivocan. Quien crea eso terminará dándose de topes en la pared cuando se enfrente a problemas de programación reales. No sabrá como resolverlos cuando la I.A. arroje resultados erróneos o inútiles.

He visto personas que creen que ChatGPT les resolverá todos sus bugs o creará de la nada sus proyectos. Y es por qué no conocen cómo funciona la I.A.

La I.A. no piensa, imita la forma de pensar. Carece de conciencia aunque nos quieran decir lo contrario. No es un Skynet, pero sus creadores lo harán parecer que sí.

Y tampoco es "gratis", ya que el producto final es quien la usa: nosotros.

Y nosotros somos quien la alimentamos, quien le damos retroalimentación, la "entrenamos" de acuerdo a lo que le pedimos, a lo que se le solicita.

ChatGPT, DeepSeek, Grok, etc. accede a sitios web, repositorios de código, base de datos propias para "crear" las respuestas a las solicitudes del usuario.

Lo cual es muy sospechoso. Es como si se hubiesen puesto de acuerdo para que funcionara así. Pues es capaz de recolectar datos de casi cualquier lado en la Internet.

Y también es sospechoso que antes del auge de las I.A. la malévola empresa Microsoft compró Github.😱

Pero, ¿Qué significa adaptarse? Significa que:

  • Recordar que los fundamentos nunca deben dejarse en saco roto. 
  • Mantener el código lo más simple y claro posible.
  • Actualizarse constantemente.  
  • No dejar de practicar y conocer nuevos temas. 
  • Usar la I.A. a favor y no en contra.
  • Saber qué solicitar y saber qué no es útil.

Esperemos que pasa en el futuro.

Enlaces:

https://codemonkeyjunior.blogspot.com/2025/01/las-leyes-no-escritas-de-la.html
https://codemonkeyjunior.blogspot.com/2025/06/vibe-coding-la-programacion-via-ia-el.html

sábado, 19 de julio de 2025

Programando en C++ no. 5 (manejo de excepciones)

El manejo de excepciones nos ayuda a que el flujo normal de nuestros programas no se vea interrumpido. C++ tiene mecanismos similares a lenguajes como Java y/o C# para realizar esta tarea.

Una excepción ocurre cuando hay un problema en alguna instrucción de nuestro programa, ya sea por que accedemos a una variable sin asignación de valor y que de por resultado un null. Un acceso a un método en un momento inoportuno. Una conversión numérica que no cumple las condiciones adecuadas. Acceder a un elemento que no existe en un arreglo o matriz.

Nosotros podríamos provocar una excepción con toda intencionalidad. Veamos un ejemplo:

excepcion.cpp

#include <iostream>

int main() {
  try {
    throw 0.0;
  }
  catch(double ex){
    std::cout << "Excepcion de tipo double: " << ex << std::endl;
  }
}

Compilamos y ejecutamos:

$ g++ excepcion.cpp -o excepcion.exe
$ excepcion.exe

¿Qué queremos decir en este programa?

El bloque ``try-catch`` se encargará de lanzar (throw) una excepción para luego cacharla (catch). En este caso provocamos la excepción que es un valor 0.0 (tipo double) y la excepción deberá ser del mismo tipo.

En otras palabras:

  • try: define el código a probar, es decir, es el cuerpo del código donde puede ocurrir una excepción.
  • throw: activa o lanza una excepción. 
  • catch: maneja o atrapa el error.

Veamos las siguientes excepciones hechas en Java:

try{
    int divide = 2/0;
}catch(ArithmeticException ex){
    ex.printStackTrace();
}


int[] arreglo = {1,2,3};
try{
    int numero = arreglo[3];
}
catch(ArrayIndexOutOfBoundsException ex){
    ex.printStackTrace();
}
catch(Exception ex){
    ex.printStackTrace();
}


try{
    int entero = Integer.parseInt("23f");
}catch(NumberFormatException ex){
    ex.printStackTrace();
}


String cadena = null;
try{
    boolean vacio = cadena.isEmpty();
}catch(NullPointerException ex){
    ex.printStackTrace();
}

Tipos de excepciones que vemos:

  • ArithmeticException: cuando tratamos de hacer una operación aritmética inválida. En este caso una división por cero. 
  • ArrayIndexOutOfBoundsException: al tratar de acceder a un índice inexistente de un arreglo. 
  • NumberFormatException: intentar convertir una cadena a número, pero la cadena no cumple el formato numérico. 
  • NullPointerException: acceder a un elemento null.

El código equivalente en C++ sería algo como esto:

excepciones.cpp

#include <iostream>
#include <vector>
#include <string>
#include <stdexcept>

int main() {
    // División por cero simulada
    try {
        int divisor = 0;
        if (divisor == 0) throw std::runtime_error("División por cero detectada.");
        int divide = 2 / divisor;
        std::cout << "Resultado: " << divide << std::endl;
    } catch (const std::exception& ex) {
        std::cerr << "[División] Excepción: " << ex.what() << std::endl;
    }

    // Acceso fuera de los límites del arreglo
    try {
        std::vector<int> arreglo = {1, 2, 3};
        int numero = arreglo.at(3); // índice fuera de rango
        std::cout << "Número: " << numero << std::endl;
    } catch (const std::out_of_range& ex) {
        std::cerr << "[Arreglo] Excepción: " << ex.what() << std::endl;
    } catch (...) {
        std::cerr << "[Arreglo] Otra excepción inesperada." << std::endl;
    }

    // Conversión inválida de cadena a entero
    try {
        std::string texto = "f23f";
        int numero = std::stoi(texto);
        std::cout << "Número convertido: " << numero << std::endl;
    } catch (const std::invalid_argument& ex) {
        std::cerr << "[Conversión] Excepción: " << ex.what() << std::endl;
    }

    // Puntero nulo accediendo a método
    try {
        std::string* cadena = nullptr;
        if (!cadena) throw std::runtime_error("Cadena nula.");
        bool vacio = cadena->empty();
        std::cout << "¿Está vacía?: " << std::boolalpha << vacio << std::endl;
    } catch (const std::exception& ex) {
        std::cerr << "[Cadena nula] Excepción: " << ex.what() << std::endl;
    }

    return 0;
}

Compilamos y ejecutamos:

$ g++ excepciones.cpp -o excepciones.exe
$ excepciones.exe

Salida:

[Divisi├│n] Excepci├│n: Divisi├│n por cero detectada.
[Arreglo] Excepci├│n: vector::_M_range_check: __n (which is 3) >= this->size() (which is 3)
[Conversi├│n] Excepci├│n: stoi
[Cadena nula] Excepci├│n: Cadena nula.

¡Hemos visto como crear excepciones en C++!

Continuaremos con este tema en próximas entregas sobre este lenguaje.

Enlaces:

https://codemonkeyjunior.blogspot.com/2024/06/hace-tiempo-vimos-como-crear-un.html
https://hektorprofe.github.io/cpp/13-manejo-excepciones/
https://isocpp.org/wiki/faq/exceptions
https://www.w3schools.com/cpp/cpp_exceptions.asp

domingo, 13 de julio de 2025

Programando en C# no. 8 (manejo de excepciones)

En el mundo de la programación hay que saber diferenciar entre una excepción y un error.

Una excepción es una falla en el flujo normal de un programa. Si el programa tiene manejo de excepciones, el programa continuará su ejecución o flujo aunque hayan excepciones. Un ejemplo sería que en una parte del prograam se quiera acceder a un índice inexistente de un vector.

Un error es una falla que romperá el flujo del programa. No hay forma de evitarlo. Por ejemplo, que la máquina en la que se está ejecutando un programa se quede sin memoria o que no tenga energía (eléctrica) para funcionar.

Al igual que Java, C# tiene la capacidad de manejar excepciones. Casi de manera idéntica.

Supongamos que queremos acceder a un índice de una cadena "ABC" que no existe. La forma de manejar esa excepción, de tipo ArgumentOutOfRangeException, sería la siguiente:

 try
 {
   string nuevoString = "ABC".Substring(2, 4);
   Console.WriteLine("{0}", nuevoString);
 } catch (ArgumentOutOfRangeException aoex)
  {
    Console.WriteLine("Ha ocurrido una Excepcion: {0}", aoex.Message);
  }

Al compilar no ocurrirá ningún error. La excepción se lanzará a la hora de la ejecución:

Ha ocurrido una Excepcion: Index and length must refer to a location within the string. (Parameter 'length')

Con esto el programa continuará su flujo a pesar de la excepción.

Para operaciones aritméticas haríamos algo como esto (ArithmeticException): 

var x = 1;
var y = 0;
try
{
  int divide = x / y;
}
catch (ArithmeticException ex)
{ 
  Console.WriteLine("Ha ocurrido una Excepcion: {0}", ex.Message);
}

De igual manera, no ocurrirá nada a la hora de la compilación. Ocurrirá a la hora de ejecución:

Ha ocurrido una Excepcion: Attempted to divide by zero.

Ya que se trata de una división entre cero.

Supongamos que queremos acceder a un elemento inexistente de un arreglo (o vector):

int[] arreglo = { 1, 2, 3 };
try
{
   int numero = arreglo[3];
}
catch (IndexOutOfRangeException ex)
{
  Console.WriteLine("Ha ocurrido una Excepcion: {0}", ex.Message);
}

El compilador no adivinará que queremos acceder a un índice que no existe. Sin embargo, el manejo de la excepción, de tipo IndexOutOfRangeException, se encargará de gestionar esa acción.

Ha ocurrido una Excepcion: Index was outside the bounds of the array.

Nota: Existen excepciones marcadas y no marcadas. Las marcadas son las que el compilador obliga a escribir. Las no marcadas no requieren manejo explícito, pero pueden causar la terminación del programa.

Continuaremos con esta serie de entregas sobre C#.

Enlaces:

https://codemonkeyjunior.blogspot.com/2025/07/programando-en-c-no-7-uso-de-genericos.html


GCP Pub/Sub: un servicio de mensajería asíncrona y escalable

Según la documentación oficial, Pub/Sub es:

Un servicio de mensajería asíncrona y escalable que separa los servicios que producen mensajes de aquellos que procesan esos mensajes.

Lo que nos permite hacer es:

Permitir que los servicios se comuniquen de forma asíncrona, con latencias de alrededor de 100 milisegundos.

Se usa para:

Las canalizaciones de integración de datos y estadísticas de transmisión a fin de cargar y distribuir datos. Es igual de efectivo que el middleware orientado a la mensajería para la integración de servicios o como una cola con el fin de paralelizar las tareas.

Pub/Sub te permite crear sistemas de productores y consumidores de eventos, llamados publicadores y suscriptores. Los publicadores se comunican con los suscriptores de forma asíncrona mediante la transmisión de eventos, en lugar de llamadas de procedimiento remoto (RPC) síncronas.

Los publicadores envían eventos al servicio de Pub/Sub, sin importar cómo o cuándo se procesarán estos eventos. Luego, Pub/Sub entrega eventos a todos los servicios que reaccionan a ellos. En los sistemas que se comunican a través de RPC, los publicadores deben esperar a que los suscriptores reciban los datos.

Sin embargo, la integración asíncrona en Pub/Sub aumenta la flexibilidad y solidez del sistema general.

En pocas palabras, es una tecnología similar a RabbitMQ, Apache Kafka y/o ActiveMQ por mencionar solo algunas.

Casos habituales

  • Transferir la interacción del usuario y los eventos del servidor. 
  • Distribución de eventos en tiempo real. 
  • Replicar datos entre bases de datos. 
  • Procesamiento y flujos de trabajo paralelos. 
  • Bus de eventos empresariales. 
  • Transmisión de datos desde aplicaciones, servicios o dispositivos de la IoT. 
  • Actualización de cachés distribuidas. 
  • Balanceo de cargas para la confiabilidad.

Un ejemplo sencillo de esta tecnología sería crear una aplicación que:

  • Publique mensajes a un tópico de Pub/Sub y 
  • Reciba mensajes desde ese tópico.

Para ello es necesario tener una cuenta de GCP. Tener el JDK más actual.

Creamos una aplicación de Spring Boot y en nuestro pom.xml agregamos la siguiente dependencia:

<dependency>
  <groupId>com.google.cloud</groupId>
  <artifactId>spring-cloud-gcp-starter-pubsub</artifactId>
</dependency>

La configuración del archivo YAML sería algo como esto:

# En application.yml
spring:
  cloud:
    gcp:
      pubsub:
        project-id: tu-id-de-proyecto
        credentials:
          location: classpath:tu-archivo-de-credenciales.json

Crearemos una clase tipo Service que servirá como el "publicador":

@Service
public class PublisherService {
    private final PubSubTemplate pubSubTemplate;

    public PublisherService(PubSubTemplate pubSubTemplate) {
        this.pubSubTemplate = pubSubTemplate;
    }

    public void enviarMensaje(String mensaje) {
        pubSubTemplate.publish("mi-topico", mensaje);
    }
}

Crearemos una clase tipo Component que servirá para escuchar los mensajes:

@Component
public class Subscriber {
    @PubSubSubscriber(subscription = "mi-suscripcion")
    public void recibirMensaje(String mensaje) {
        System.out.println("Mensaje recibido: " + mensaje);
    }
}

Ahora crearemos una clase tipo Controller para invocar el método de envío de mensajes:

@RestController
@RequestMapping("/pubsub")
public class PubSubController {

    private final PublisherService publisherService;

    public PubSubController(PublisherService publisherService) {
        this.publisherService = publisherService;
    }

    @PostMapping("/enviar")
    public ResponseEntity<String> enviar(@RequestBody String mensaje) {
        publisherService.enviarMensaje(mensaje);
        return ResponseEntity.ok("Mensaje publicado: " + mensaje);
    }
}

Usando curl para probar el mensaje:

curl -X POST http://localhost:8080/pubsub/enviar \
     -H "Content-Type: text/plain" \
     -d "Hola desde el controlador!"

También agregando un nuevo método en el controller:

@PostMapping("/enviar-json")
public ResponseEntity<String> enviarJson(@RequestBody Map<String, String> payload) {
    String mensaje = payload.get("mensaje");
    publisherService.enviarMensaje(mensaje);
    return ResponseEntity.ok("Mensaje JSON publicado: " + mensaje);
}

Y usando curl:

curl -X POST http://localhost:8080/pubsub/enviar-json \
     -H "Content-Type: application/json" \
     -d '{"mensaje": "¡Hola JSON!"}'

Continuaremos sobre temas de GCP en próximas entregas.

Enlaces:

https://cloud.google.com/pubsub/docs/overview
https://www.rabbitmq.com/
https://kafka.apache.org/
https://activemq-apache-org.translate.goog/

sábado, 12 de julio de 2025

Programando en C# no. 7 (uso de genéricos)

En el mundo de la programación un genérico nos permite escribir código que trabaja con cualquier tipo de datos sin perder la seguridad de tipos. Es una forma de crear clases, interfaces y métodos que pueden operar sobre varios tipos de datos sin duplicar código. Esto evita castings innecesarios y errores en tiempo de ejecución, ofreciendo reutilización y robustez.

Suponiendo que tenemos esta clase:

public class Caja<T> {
    private T contenido;

    public void Guardar(T elemento) {
        contenido = elemento;
    }

    public T Obtener() {
        return contenido;
    }
}

Lo usaríamos de la siguiente forma:

Caja<int> cajaDeEnteros = new Caja<int>();
cajaDeEnteros.Guardar(42);
Console.WriteLine(cajaDeEnteros.Obtener());  

El tipo T se puede cambiar por cualquier clase o tipo primitivo, lo que lo vuelve extremadamente flexible. Si hacer ninuna conversión o casteo (cast). Veamos otro uso:

var myTypeFloat = new Caja<float>();
myTypeFloat.Guardar(232.0f);
Console.WriteLine("Float: {}", myTypeFloat.Obtener());

var myTypeString = new Caja<string>();
myTypeString.Guardar("232");
Console.WriteLine("String: {}", myTypeString.Obtener());

Usar genéricos nos ahorra tener que hacer conversiones, casteos o incluso crear clases por tipos. ¿Se podrá usar con interfaces? La respuesta es sí.

public interface MyInterface<T> {
        T suma(T num1, T num2);
}

Podríamos crear una clase que lo implemente, además específicamos el tipo a usar, en este caso será un BigInteger.

public class MyInterfaceImpl : MyInterface<BigInteger>
    {
        public BigInteger suma(BigInteger x, BigInteger y)
        {
            return x + y;
        }
    }

Creamos unas variables tipo ``BigInteger`` y se las pasamos al método de suma:

BigInteger x = new BigInteger(23);
var y = new BigInteger(45);
var myInterface = new MyInterfaceImpl();
var resultSuma = myInterface.suma(x, y);
Console.WriteLine("Suma: {}", resultSuma); 

Al final mostramos el resultado de la suma.

Este solo es un ejemplo del uso de clases e interfaces genéricas. Continuaremos con esta serie sobre C# en próximas entregas.

Enlaces:

https://emanuelpeg.blogspot.com/search/label/C%23
Aprender a programar con C# (Libro)

domingo, 29 de junio de 2025

Vibe Coding (la programación vía IA): ¿el futuro de la programación?

Vibe Coding es un nuevo paradigma de programación, una nueva forma de crear código.

Es un enfoque emergente en el desarrollo de software donde la programación se realiza principalmente a través de interacciones en lenguaje natural con una inteligencia artificial, en lugar de escribir código línea por línea. El término fue popularizado por Andrej Karpathy en 2025 y se basa en la idea de que "ver cosas, decir cosas, ejecutar cosas y copiar/pegar cosas" puede ser suficiente para construir software funcional.

¿Cómo funciona?

En lugar de escribir código manualmente, describes lo que quieres lograr —por texto o incluso por voz— y la IA genera el código por ti. Si algo falla, simplemente le explicas el error a la IA y le pides que lo corrija. Es un flujo de trabajo iterativo, conversacional y muy ágil.

En otras palabras:

Una persona, ya sea programador o no (ojo aquí), podrá crear aplicaciones. Esto gracias a la Inteligencia Artificial.

Esto lo vemos en pleno 2025 donde el uso de herramientas como ChatGPT, Grok, Copilot o DeepSeek se ha vuelto más habitual de lo que pensamos.

¿Significará el fin de los programadores? Si y no.

Si, para los programadores que se duermen en sus laureles. Quienes no se preparan constantemente, que no investigan, que se aferran a lo que ya conocen.

No, para los que constantemente crean código. Los que tienen curiosidad por investigar nuevas tecnologías. Los que programan incluso en lenguajes poco comerciales. Los que crean contenido y no son solo consumidores.

Pros y contras del Vibe Coding (o programar vía I.A.)

Pros:

Para quienes saben programar será de mucha ayuda. Automatizará el trabajo que antes erá muy tedioso. Reducirá el tiempo de desarrollo.

Para quienes no saben programar será de ayuda para "minimizar" la complejidad técnica. Esto para ciertas cosas, cabe aclarar.

Contras:

Para quienes saben programar les hará más difícil el mantenimiento de código. Tendrán que rehacer y corregir código inservible arrojado por la I.A., tendrán que revisar lo que es verdaderamente útil y descartar lo que no. La deuda técnica se volverá el pan de cada día.

Para quienes no saben programar les hará creer que son programadores. Creerán que el código arrojado por la I.A. resolverá todo su trabajo, no será así. Tendrán muchos problemas aún mayores. Si desconocen el lenguaje con el que se la I.A. "creó", el código será como entender un idioma indescifrable. Peor cuando salga algún "bug" y no tengan idea de cómo remediarlo, ya que la misma I.A. arrojará resultados nada útiles.

En conclusión: el Vibe Coding es un paradigma para acelerar el desarrollo y "democratizar" la programación, pero requiere criterio técnico para evitar errores, dependencias o problemas de calidad.

Enlaces:

https://codemonkeyjunior.blogspot.com/2025/04/firebase-studio-el-visual-studio-code.html
https://codemonkeyjunior.blogspot.com/2025/03/como-ser-un-mejor-programador-en-un.html

sábado, 28 de junio de 2025

Subir un archivo CSV a una tabla en GCP BigQuery

En una entrega pasada vimos como cargar datos desde un CSV a una tabla en BigQuery.

Esta vez crearemos un programa con Java y BigQuery. Para ello necesitamos tener un archivo con datos el cual llamaremos "DATOS.txt". El contenido del archivo será similar a esto:

20250412,34,450.0
20250412,34,432.0
20250412,122,500.0

Tendremos una tabla llamada ``tkdata`` con los siguientes campos:

fchinf DATE 
numregs INT64 
valor STRING 

Requisitos:

  • Tener acceso a GCP BigQuery.
  • Tener JDK 11 o más actual. 
  • Tener Maven (el más actual).

¿Qué hará el programa?

  1. Verificar si existe el archivo a subir ("DATOS.txt"). 
  2. Obtener su contenido y guardarlo en una lista tipo String. 
  3. Crear un objeto tipo StringBuilder a partir de la lista tipo String. 
  4. Enviar el contenido del objeto StringBuilder a un nuevo archivo ("tkdata.csv") que se guardará en el mismo bucket del archivo original. 
  5. Cargar el contenido del nuevo archivo a la tabla ``tkdata``.
  6. Verificar que los datos hayan sido cargados a la tabla.

BigQueryCsvUploader.java

import com.google.cloud.bigquery.*;
import com.google.cloud.storage.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class BigQueryCsvUploader {
    private static final Logger LOG = LoggerFactory.getLogger(BigQueryCsvUploader.class);
    private static final String NAME_FILE_ORIGINAL = "DATOS.csv";
    private static final String TABLE_NAME = "tkdata";
    private static final String DATASET = "mydataset";
    private static final String PROJECT = "myproject";
    private static final String BUCKET = "mybucket";
    private static final String NAME_NEW_FILE = "tkdata.csv";
    private static final long MAX_SIZE_BYTES = 300 * 1024 * 1024; // 300 MB

   
    public static class Tkdata {
        private Date fchinf;
        private long numregs;
        private String valor;

        public Date getFchinf() { return fchinf; }
        public void setFchinf(String fchinf) throws ParseException {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            this.fchinf = sdf.parse(fchinf);
        }
        public long getNumregs() { return numregs; }
        public void setNumregs(String numregs) { this.numregs = Long.parseLong(numregs); }
        public String getValor() { return valor; }
        public void setValor(String valor) { this.valor = valor; }
    }

   
    public static boolean existFile(String bucketName, String fileName) {
        Storage storage = StorageOptions.getDefaultInstance().getService();
        Blob blob = storage.get(BlobId.of(bucketName, fileName));
        return blob != null && blob.exists();
    }

   
    public static boolean maxSize(String bucketName, 
     String fileName, long maxSizeBytes) {
        Storage storage = StorageOptions.getDefaultInstance().getService();
        Blob blob = storage.get(BlobId.of(bucketName, fileName));
        if (blob == null) {
            LOG.error("El archivo {} no existe en el bucket {}", fileName, bucketName);
            return false;
        }
        return blob.getSize() <= maxSizeBytes;
    }

    
    public static List<Tkdata> getListTkdata(String bucketName, String fileName) {
        List<Tkdata> listaTkdata = new ArrayList<>();
        Storage storage = StorageOptions.getDefaultInstance().getService();
        Blob blob = storage.get(BlobId.of(bucketName, fileName));
        if (blob == null) {
            LOG.error("Archivo{} no encontrado en el bucket {}", fileName, bucketName);
            return listaTkdata;
        }

        String content = new String(blob.getContent(), StandardCharsets.UTF_8);
        try (BufferedReader reader = new BufferedReader(new StringReader(content))) {
            String line;
            reader.readLine();
            while ((line = reader.readLine()) != null) {
                String[] parts = line.split(",", -1); 
                if (parts.length == 3) {
                    Tkdata obj = new Tkdata();
                    try {
                        obj.setFchinf(parts[0].trim());
                        obj.setNumregs(parts[1].trim());
                        obj.setValor(parts[2].trim());
                        listaTkdata.add(obj);
                    } catch (ParseException | NumberFormatException e) {
                        LOG.error("Error parseando linea: {}", line, e);
                    }
                } else {
                    LOG.warn("Linea malformada: {}", line);
                }
            }
        } catch (IOException e) {
            LOG.error("Error al leer el contenido", e);
        }
        return listaTkdata;
    }

    
    public static StringBuilder convertToStringBuilder(List<Tkdata> listaTkdata) {
        StringBuilder sb = new StringBuilder();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        for (Tkdata item : listaTkdata) {
            sb.append(sdf.format(item.getFchinf())).append(",");
            sb.append(item.getNumregs()).append(",");
            sb.append(item.getValor()).append("\n");
        }
        return sb;
    }

   
    public static boolean creaNuevoFileCSV(String bucketName, 
       String fileName, String content) {
        try {
            Storage storage = StorageOptions.getDefaultInstance().getService();
            BlobId blobId = BlobId.of(bucketName, fileName);
            BlobInfo blobInfo = BlobInfo.newBuilder(blobId).setContentType("text/csv").build();
            storage.create(blobInfo, content.getBytes(StandardCharsets.UTF_8));
            return true;
        } catch (Exception e) {
            LOG.error("Error al subir archivo {} al bucket {}", fileName, bucketName, e);
            return false;
        }
    }

    
    public static boolean loadCSVToTableBigQuery(String projectId, 
     String datasetId, String bucketName, String sourceFileName, String tableName) {
        try {
            BigQuery bigquery = BigQueryOptions.getDefaultInstance().getService();
            TableId tableId = TableId.of(projectId, datasetId, tableName);

            
            Schema schema = Schema.of(
                Field.of("fchinf", StandardSQLTypeName.DATE),
                Field.of("numregs", StandardSQLTypeName.INT64),
                Field.of("valor", StandardSQLTypeName.STRING)
            );

           
            JobConfiguration jobConfig = LoadJobConfiguration.newBuilder(
                tableId,
                String.format("gs://%s/%s", bucketName, sourceFileName),
                FormatOptions.csv()
            )
                .setSchema(schema)
                .setSkipLeadingRows(0) 
                .setWriteDisposition(JobInfo.WriteDisposition.WRITE_APPEND) 
                .build();

            
            Job job = bigquery.create(JobInfo.of(jobConfig));
            job = job.waitFor();
            if (job.isDone() && job.getStatus().getError() == null) {
                LOG.info("CSV {} successfully loaded into BigQuery table {}.{}", 
                 sourceFileName, datasetId, tableName);
                return true;
            } else {
                LOG.error("Error cargando CSV a BigQuery: {}", job.getStatus().getError());
                return false;
            }
        } catch (Exception e) {
            LOG.error("Error en el proceso de carga", e);
            return false;
        }
    }

  
    public static boolean validateTableData(String projectId, 
      String datasetId, String tableName, List<Tkdata> expectedData) {
        try {
            BigQuery bigquery = BigQueryOptions.getDefaultInstance().getService();
            String query = String.format("SELECT fchinf, numregs, valor FROM %s.%s.%s", 
                        projectId, datasetId, tableName);
            QueryJobConfiguration queryConfig = QueryJobConfiguration.newBuilder(query).build();

           
            TableResult result = bigquery.query(queryConfig);
            long rowCount = result.getTotalRows();

            if (rowCount == 0) {
                LOG.error("No hay datos en la tabla {}.{}", datasetId, tableName);
                return false;
            }

           
            if (expectedData != null && rowCount != expectedData.size()) {
                LOG.warn("Error en conteo de datos {}, found {}", expectedData.size(), rowCount);
                return false;
            }

            
            LOG.info("Sample data from {}.{} (first 5 rows):", datasetId, tableName);
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            int maxRowsToLog = 5;
            int rowIndex = 0;
            for (FieldValueList row : result.iterateAll()) {
                if (rowIndex >= maxRowsToLog) break;
                String fchinf = row.get("fchinf").getStringValue(); 
                long numregs = row.get("numregs").getLongValue();
                String valor = row.get("valor").getStringValue();
                LOG.info("Row {}: fchinf={}, numregs={}, valor={}", rowIndex + 1, fchinf, numregs, valor);
                rowIndex++;
            }

            LOG.info("Validacion exitosa: {} rows found in table {}.{}", 
          rowCount, datasetId, tableName);
            return true;
        } catch (Exception e) {
            LOG.error("Error validando datos en la tabla {}.{}", datasetId, tableName, e);
            return false;
        }
    }

    public static void main(String[] args) {
     
        if (!existFile(BUCKET, NAME_FILE_ORIGINAL)) {
            LOG.error("Archivo{} no existe en el buckett {}.", NAME_FILE_ORIGINAL, BUCKET);
            return;
        }

        
        if (!maxSize(BUCKET, NAME_FILE_ORIGINAL, MAX_SIZE_BYTES)) {
            LOG.error("El archivo {} excede el tamaño: 300MB.", NAME_FILE_ORIGINAL);
            return;
        }

        
        List<Tkdata> listaTkdata = getListTkdata(BUCKET, NAME_FILE_ORIGINAL);
        if (listaTkdata.isEmpty()) {
            LOG.error("No hay datos para leer {}. ", NAME_FILE_ORIGINAL);
            return;
        }

        StringBuilder sb = convertToStringBuilder(listaTkdata);

        
        if (creaNuevoFileCSV(BUCKET, NAME_NEW_FILE, sb.toString())) {
            LOG.info("Archivo {} cargado al bucket {}", NAME_NEW_FILE, BUCKET);
            LOG.info("Carga de datos {} a la tabla: {}.{}", NAME_NEW_FILE, DATASET, TABLE_NAME);
            if (loadCSVToTableBigQuery(PROJECT, DATASET, BUCKET, NAME_NEW_FILE, TABLE_NAME)) {
                if (validateTableData(PROJECT, DATASET, TABLE_NAME, listaTkdata)) {
                    LOG.info("Validacion correcta {}.{}", DATASET, TABLE_NAME);
                } else {
                    LOG.error("Validacion fallida {}.{}", DATASET, TABLE_NAME);
                }
            } else {
                LOG.error("Fallo al cargar los datos a la tabla de BigQuery  {}.{}", DATASET, TABLE_NAME);
            }
        } else {
            LOG.error("Fallo al cargar el archivo {} al bucket {}", NAME_NEW_FILE, BUCKET);
        }
    }
}

Es importante hacer notar que los tipos de datos en el archivo deben concordar a los datos de la tabla. En caso contrario, no cargarán.

También es importante tener estas dependencias en el pom.xml

<dependencies>
    <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>google-cloud-storage</artifactId>
        <version>2.44.0</version>
    </dependency>
    <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>google-cloud-bigquery</artifactId>
        <version>2.44.0</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>2.0.13</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.13</version>
    </dependency>
</dependencies>

De preferncia usar Eclipse IDE para generar un .jar o ejecutarlo directamente.

Si la ejecución es correcta podremos ver los datos cargados en la tabla. Continuaremos más sobre BigQuery en próximas entregas.

Enlaces:

https://cloud.google.com/bigquery/docs/loading-data-cloud-storage-csv

Kubernetes en un vistazo

Continuamos con esta serie de entregas sobre Docker y Kubernetes Según la documentación oficial Kubernetes (k8s) es una plataforma de...

Etiquetas

Archivo del blog