Translate

lunes, 15 de junio de 2026

JDBI en Java: acceso a datos simple


Cuando hablamos de acceso a bases de datos en Java, solemos pensar en JDBC puro o en frameworks pesados como Hibernate.

Pero existe una opción intermedia muy interesante: Jdbi.


JDBI busca mantener la simplicidad y el control de SQL, pero eliminando gran parte del código repetitivo de JDBC.


¿Qué es JDBI?

JDBI es una librería que se construye sobre JDBC y agrega:

  • Mapeo automático de objetos
  • Queries más limpias
  • Manejo simplificado de parámetros
  • Integración con transacciones
  • APIs modernas y funcionales


La idea es poder escribir SQL real sin tener que pelear con ResultSet, PreparedStatement y toneladas de try/catch.


Dependencia Maven:

<dependency>

    <groupId>org.jdbi</groupId>

    <artifactId>jdbi3-core</artifactId>

    <version>3.49.6</version>

</dependency>


Si usamos una base de datos específica, agregamos el driver correspondiente.


Por ejemplo para MySQL:

<dependency>

    <groupId>com.mysql</groupId>

    <artifactId>mysql-connector-j</artifactId>

</dependency>


Crear una conexión:

Jdbi jdbi = Jdbi.create(

    "jdbc:mysql://localhost:3306/test",

    "root",

    "password"

);


Ejecutar una consulta


Supongamos esta tabla:

CREATE TABLE users (

    id BIGINT PRIMARY KEY,

    name VARCHAR(100),

    age INT

);


Y esta clase:


public class User {

    private Long id;

    private String name;

    private Integer age;


    // getters y setters

}


Ahora podemos consultar así:


List<User> users = jdbi.withHandle(handle ->

    handle.createQuery("SELECT * FROM users")

          .mapToBean(User.class)

          .list()

);


Fijate que desaparece todo el manejo manual de ResultSet.


Insertar datos:


jdbi.useHandle(handle ->

    handle.createUpdate("""

        INSERT INTO users(id, name, age)

        VALUES(:id, :name, :age)

    """)

    .bind("id", 1L)

    .bind("name", "Emanuel")

    .bind("age", 30)

    .execute()

);


Parámetros nombrados

Una de las mejores cosas de JDBI es evitar los clásicos:


statement.setString(1, ...);

statement.setInt(2, ...);


En su lugar:

.bind("name", "Juan")


Mucho más legible.


JDBI también permite definir interfaces tipo DAO.


public interface UserDao {


    @SqlQuery("SELECT * FROM users WHERE id = :id")

    User findById(@Bind("id") Long id);


    @SqlUpdate("""

        INSERT INTO users(id, name, age)

        VALUES(:id, :name, :age)

    """)

    void insert(

        @Bind("id") Long id,

        @Bind("name") String name,

        @Bind("age") Integer age

    );

}


Y luego:


UserDao dao = jdbi.onDemand(UserDao.class);

User user = dao.findById(1L);


¿Por qué mucha gente lo elige?

Porque queda en un punto muy cómodo. No es tan automático como JPA y permite hacer optimizaciones fácilmente, pero no es tan trabajoso como JDBC. 


JDBI se integra muy bien con Spring Framework.

Dependencia:

<dependency>

    <groupId>org.jdbi</groupId>

    <artifactId>jdbi3-spring5</artifactId>

    <version>3.49.6</version>

</dependency>


Configuración:


@Configuration

public class JdbiConfig {


    @Bean

    public Jdbi jdbi(DataSource dataSource) {

        Jdbi jdbi = Jdbi.create(dataSource);

        jdbi.installPlugin(new SqlObjectPlugin());

        return jdbi;

    }

}


Crear el DAO como Bean


@Configuration

public class DaoConfig {


    @Bean

    public UserDao userDao(Jdbi jdbi) {

        return jdbi.onDemand(UserDao.class);

    }

}


Usarlo desde un Service


@Service

public class UserService {

    private final UserDao userDao;

    public UserService(UserDao userDao) {

        this.userDao = userDao;

    }


    public User getUser(Long id) {

        return userDao.findById(id);

    }

}


¿Cuándo conviene usar JDBI?

JDBI suele ser una muy buena opción cuando:

  • querés escribir SQL manualmente
  • necesitás buena performance
  • no querés la complejidad de Hibernate
  • trabajás con consultas complejas
  • querés algo más moderno que JDBC puro


Especialmente en microservicios o aplicaciones donde el modelo relacional es importante, JDBI puede resultar mucho más simple y transparente que un ORM completo.


Dejo link: https://jdbi.org


domingo, 14 de junio de 2026

Mundial de Lenguajes de Programación



Le pregunte a la IA que onda si hacemos partidos de diferentes tecnologías con diferentes lenguajes, y este fue el resultado : 



Partido 1: Aplicaciones Web

Java vs C#


🇯🇵 Java sale con Spring Boot.

🇺🇸 C# responde con ASP.NET Core.


Ambos tienen experiencia internacional, planteles maduros y una enorme cantidad de títulos.

Resultado: Empate 1-1.


> Si tenés que hacer una API corporativa, cualquiera de los dos te lleva a octavos.


 🏟️ Partido 2: Concurrencia

Elixir vs Go


🇧🇷 Elixir apuesta por la posesión y el juego asociado con actores y supervisores.

🇩🇪 Go juega más directo con goroutines y channels.


Elixir domina el mediocampo cuando aparecen millones de procesos concurrentes.

Resultado: Elixir 2 - Go 1.

La BEAM sigue siendo una de las ligas más difíciles de enfrentar.


 🏟️ Partido 3: Aplicaciones Standalone

C++ vs Rust


🇮🇹 C++ es el veterano con ocho mundiales encima.

🇳🇱 Rust llega con una generación joven y una defensa impecable.


Rust reduce errores, pero C++ todavía tiene una experiencia enorme.

Resultado: C++ 1 - Rust 1.

La leyenda sigue vigente, pero la promesa pide pista.


 🏟️ Partido 4: Cloud

Java vs Node.js


🇦🇷 Java viene con GraalVM y Spring Boot.

🇪🇸 Node.js juega rápido y con poco peso.


En microservicios y aplicaciones cloud modernas, ambos se sienten cómodos.

Resultado: Empate.

El VAR determina que la elección depende más del equipo que del lenguaje.


🏟️ Partido 5: Inteligencia Artificial

Python vs el resto del mundo


🇫🇷 Python entra a la cancha.

Resultado: Python 5 - 0.

Hubo partido durante cinco minutos.


 🏟️ Partido 6: Programación Funcional

Scala vs Haskell

🇺🇾 Scala combina técnica y pragmatismo.

🇭🇷 Haskell juega un fútbol hermoso que pocos entienden.


Resultado: Scala 2 - Haskell 1.

La posesión fue 90% para Haskell, pero Scala convirtió las que tuvo.


🏟️ Partido 7: Frontend

TypeScript vs JavaScript


🇵🇹 JavaScript es el capitán histórico.

🇵🇹 TypeScript es el mismo equipo, pero con preparador físico.

Resultado: TypeScript 3 - JavaScript 1.

 Los errores en tiempo de compilación fueron la figura del partido.


 🏟️ Partido 8: Bases de Datos

PostgreSQL vs MySQL


🇨🇴 PostgreSQL juega con mucha técnica.

🇲🇽 MySQL apuesta por la experiencia.


Resultado: PostgreSQL 2 - MySQL 1.

PostgreSQL ganó por posesión y calidad de juego.


Y listo, no hice mucho, ni dice mucho pero es divertido :P 

FFM API en Java: Accediendo a Memoria Nativa y Código Nativo sin JNI parte 2

 


Seguimos viendo FFM API

Dónde podemos usar FFM API:

Librerías de machine learning

Integración con:

  • CUDA,
  • TensorRT,
  • ONNX Runtime.


Motores gráficos

Bindings contra:

  • Vulkan,
  • OpenGL,
  • SDL.


Bases de datos

Drivers nativos de:

  • SQLite,
  • RocksDB.


Sistemas embebidos

  • Acceso a APIs del sistema operativo.
  • Upcalls: cuando C llama a Java


La FFM API también soporta: upcalls

Es decir:

  • código nativo llamando código Java.
  • Algo parecido a callbacks.


Veamos un ejemplo práctico: strlen


import java.lang.foreign.*;

import java.lang.invoke.MethodHandle;

import static java.lang.foreign.ValueLayout.*;


public class Main {

    public static void main(String[] args) throws Throwable {

         Linker linker = Linker.nativeLinker();

         MemorySegment strlenAddr =

                linker.defaultLookup()

                        .find("strlen")

                        .orElseThrow();

        MethodHandle strlen = linker.downcallHandle(

                strlenAddr,

                FunctionDescriptor.of(JAVA_LONG, ADDRESS)

        );


        try (Arena arena = Arena.ofConfined()) {

            MemorySegment text =

                    arena.allocateFrom("Hola Mundo");

            long size = (long) strlen.invoke(text);

            System.out.println(size);

        }

    }

}


FFM API esta disponible desde Java 21


La FFM API es probablemente uno de los cambios más importantes del ecosistema Java moderno.

Porque permite:

  • interoperabilidad nativa,
  • manejo eficiente de memoria,
  • reemplazar gran parte de JNI,
  • acceso a APIs del sistema,
  • y programación de bajo nivel.


Todo usando una API moderna y mucho más amigable.


Project Panama abre la puerta a una nueva generación de aplicaciones Java capaces de integrarse con el mundo nativo sin el dolor histórico de JNI.

FFM API en Java: Accediendo a Memoria Nativa y Código Nativo sin JNI


La FFM API (Foreign Function & Memory API) es una de las incorporaciones más interesantes de Java moderno.


Su objetivo es permitir que una aplicación Java pueda:

  • Llamar funciones escritas en C.
  • Acceder a memoria fuera del heap de la JVM.
  • Reemplazar gran parte del uso de JNI.
  • Interactuar con librerías nativas de forma más segura y eficiente.


La FFM API forma parte del proyecto Project Panama.

Pero demos un paso para atras,  ¿Por qué existía JNI?


Durante años, si una aplicación Java necesitaba:

  • acceder a una librería en C,
  • usar código del sistema operativo,
  • trabajar con drivers,
  • o ejecutar operaciones de bajo nivel,

la única opción era usar JNI (Java Native Interface)


El problema es que JNI:

  • es complejo,
  • verboso,
  • inseguro,
  • difícil de debuggear,
  • y propenso a crashes de la JVM.


Además, obliga a escribir:

  • código Java,
  • código C,
  • headers,
  • compilación nativa,
  • glue code.


Puff si no habre puteado con JNI.


¿Qué propone la FFM API?

La FFM API permite hacer esto:

printf("Hola desde C!\n");


...directamente desde Java.


Y además:

  • manejar memoria nativa,
  • mapear estructuras,
  • trabajar con punteros,
  • invocar funciones dinámicamente.


Todo usando una API moderna.


La FFM API tiene dos pilares:

1. Foreign Function


Permite invocar funciones nativas.


Por ejemplo:

  • printf
  • strlen
  • malloc
  • funciones de librerías C


2. Foreign Memory

Permite manejar memoria fuera del heap de Java.


Esto es importante porque:

  • la JVM normalmente controla toda la memoria,
  • pero muchas librerías nativas usan memoria propia.


La FFM API permite trabajar con esa memoria de manera segura.


Primer ejemplo: llamar a printf


import java.lang.foreign.*;

import java.lang.invoke.MethodHandle;

import static java.lang.foreign.ValueLayout.*;


public class Main {


    public static void main(String[] args) throws Throwable {

        Linker linker = Linker.nativeLinker();

        SymbolLookup stdlib = linker.defaultLookup();


        MemorySegment printfAddress =

                stdlib.find("printf")

                      .orElseThrow();


        FunctionDescriptor printfSignature =

                FunctionDescriptor.of(JAVA_INT, ADDRESS);


        MethodHandle printf = linker.downcallHandle(

                printfAddress,

                printfSignature

        );


        try (Arena arena = Arena.ofConfined()) {

            MemorySegment text =

                    arena.allocateFrom("Hola desde C!\n");


            printf.invoke(text);

        }

    }

}


¿Qué está pasando acá?


Linker linker = Linker.nativeLinker();

Obtiene un linker capaz de interactuar con funciones nativas.


SymbolLookup stdlib = linker.defaultLookup();

Busca símbolos exportados por librerías nativas.


stdlib.find("printf")

Obtiene la dirección de memoria de la función.

Muy parecido a: void* ptr = dlsym(...)


FunctionDescriptor.of(JAVA_INT, ADDRESS)

Describe la firma de la función.

En este caso: int printf(char*)


linker.downcallHandle(...)

Crea un MethodHandle que permite invocar la función nativa.


Manejo de memoria con Arena

Uno de los conceptos más importantes es:

Un Arena administra memoria nativa.


try (Arena arena = Arena.ofConfined()) {


}


Cuando el bloque termina:

  • la memoria se libera automáticamente.
  • Esto evita muchísimos memory leaks.


La memoria nativa se representa con: MemorySegment


Es básicamente:

  • un bloque de memoria,
  • con límites,
  • seguro,
  • y controlado.


Y para reservar memoria hacemos: 

MemorySegment segment = arena.allocate(4);

Por ejemplo escribir y leer un entero: 

segment.set(JAVA_INT, 0, 42);

int value = segment.get(JAVA_INT, 0);


Supongamos esta estructura:

struct Point {

    int x;

    int y;

};


Podemos modelarla en Java.

StructLayout POINT = MemoryLayout.structLayout(

        JAVA_INT.withName("x"),

        JAVA_INT.withName("y")

);


Y para reservar memoria hacemos:

MemorySegment point = arena.allocate(POINT);


Escribir los campos:


point.set(JAVA_INT, 0, 10);

point.set(JAVA_INT, 4, 20);


¿Qué ventajas tiene?

Menos complejidad


No hace falta:

  • generar headers,
  • compilar JNI,
  • usar glue code.


¿Puede FFM API reemplaza completamente JNI?

Todavía no en todos los casos.

Hay escenarios avanzados donde JNI sigue siendo necesario.


Pero para muchísimos casos:

  • bindings simples,
  • acceso a librerías,
  • llamadas nativas,
  • estructuras,


la FFM API es mucho mejor.



miércoles, 10 de junio de 2026

Native AOT vs GraalVM Native Image


¿Quién compila mejor a código nativo: .NET o Java?


Durante años, tanto Java como .NET estuvieron dominados por el mismo modelo:

  • bytecode/intermediate language
  • máquinas virtuales
  • JIT compilation
  • optimizaciones runtime


Ese enfoque funcionó extremadamente bien durante décadas.

Pero el mundo cambió.

Hoy vivimos en un ecosistema dominado por:

  • Kubernetes
  • microservicios
  • containers efímeros
  • serverless
  • autoscaling
  • cold starts


Y en ese contexto, el tiempo de arranque y el consumo de memoria comenzaron a importar muchísimo más.


Por eso aparecieron dos tecnologías muy similares:

GraalVM

Native AOT de .NET


Ambas prometen algo revolucionario: Compilar aplicaciones administradas a binarios nativos standalone


Pero aunque el objetivo es parecido, las filosofías y enfoques son bastante diferentes.

En java tradicionalmente:

Java -> Bytecode -> JVM -> JIT -> Código Máquina


En .NET:

C# -> IL -> CLR -> JIT -> Código Máquina


Ambos mundos dependen fuertemente de:

  • compilación dinámica
  • reflection
  • metadata
  • optimizaciones runtime


En java con GraalVM Native Image:

Java -> Bytecode -> análisis estático -> binario nativo


Native AOT:

C# -> IL -> Native AOT toolchain -> binario nativo


En ambos casos:

  • desaparece el JIT
  • desaparece gran parte del runtime
  • mejora muchísimo el startup


El gran objetivo: Cloud Native

Ambas tecnologías existen por el mismo motivo: reducir startup time y consumo de memoria. Y los dos lo hacen muy bien. 


En el tamaño del binario hay diferencias interesantes.

En GraalVM, los binarios suelen ser bastante grandes. Especialmente con frameworks pesados.

Native AOT, también genera binarios relativamente grandes. Aunque frecuentemente más pequeños en aplicaciones simples.


Y los dos sufren por no poder usar reflection.

AOT y GraalVM necesita saber todo el código que será utilizado.

Eso se llama: Closed World Assumption


El compilador asume: “Sé exactamente qué código será ejecutado.”

Pero reflection rompe esa idea.


Frameworks: acá aparece una diferencia importante

Java históricamente fue MUY dinámico


Frameworks como:

  • Spring
  • Hibernate
  • Jakarta EE


usan reflection masivamente.

Eso vuelve más complejo el AOT.


.NET históricamente fue menos dinámico

ASP.NET Core moderno:

  • usa menos magia runtime
  • menos reflection extrema
  • más generación compile-time


Eso hace que Native AOT encaje bastante naturalmente.


Por eso aparecieron frameworks especiales en Java

Para adaptarse a GraalVM nacieron:

  • Quarkus
  • Micronaut
  • Helidon


Todos intentan:

  • minimizar reflection
  • mover trabajo al compile-time
  • ser AOT friendly


Mientras tanto en .NET. ASP.NET Core Minimal APIs ya estaba bastante alineado con esa filosofía.

Por eso Native AOT se siente más “natural” dentro del ecosistema.


Con respecto a Build Time, GraalVM suele sufrir más. La compilación puede tardar muchísimo. Especialmente en aplicaciones grandes.


Porque realiza:

  • análisis estático agresivo
  • optimizaciones complejas
  • tree shaking profundo


Native AOT, también aumenta el tiempo de build.

Pero generalmente menos extremo.

Ambos son excelentes para containers.


Permiten:

  • imágenes más pequeñas
  • menor consumo
  • escalado más rápido
  • menor costo cloud


GraalVM parece:  “Java intentando escapar de la JVM clásica”


Porque el ecosistema Java tradicional:

  • depende muchísimo del runtime
  • reflection
  • proxies
  • bytecode generation


Native AOT parece: “La evolución natural de .NET moderno”


ASP.NET Core ya venía moviéndose hacia:

  • menos magia
  • más compile-time
  • menos configuración runtime
  • minimalismo


¿Entonces quién gana?

Depende completamente del escenario.


GraalVM brilla especialmente si:

  • ya vivís en el ecosistema Java
  • necesitás Spring/Quarkus/Micronaut
  • querés mejorar startup brutalmente


Native AOT brilla especialmente si:

  • trabajás con ASP.NET Core
  • hacés microservicios
  • usás Minimal APIs
  • querés DX muy integrada


Lo más interesante: el regreso del compile-time

Durante años, Java y .NET apostaron fuertemente al runtime dinámico.


Ahora ambos ecosistemas están regresando a:

  • compile-time analysis
  • generación estática
  • tree shaking
  • optimización anticipada


Curiosamente, acercándose cada vez más a ideas tradicionales de:

  • C++
  • Rust
  • Go


 ¿Se está muriendo el JIT?

No.


El JIT sigue siendo extremadamente poderoso.


Especialmente para:

  • aplicaciones grandes
  • workloads largos
  • optimizaciones runtime avanzadas
  • desktop
  • sistemas complejos


Pero el cloud moderno cambió las prioridades.


Hoy muchas veces importa más:

  • arrancar rápido
  • consumir menos memoria
  • escalar rápido
  • reducir costos cloud


Y ahí AOT tiene muchísimo sentido.


Tanto GraalVM como Native AOT representan uno de los cambios más importantes en la evolución moderna de las plataformas administradas.


Ambos intentan resolver el mismo problema: Cómo llevar runtimes modernos al mundo cloud-native

Y aunque utilizan estrategias similares, sus ecosistemas muestran diferencias profundas:

  • Java todavía lucha contra décadas de dinamismo runtime
  • .NET parece haber llegado más preparado al mundo AOT


Sin embargo, ambos demuestran algo muy claro:

El futuro del desarrollo moderno probablemente combine JIT y AOT dependiendo del tipo de aplicación y del contexto donde será ejecutada.


Java Streams vs LINQ


A simple vista, Java y C# parecen ofrecer lo mismo:


users.stream()

     .filter(u -> u.getAge() > 50)

     .toList();


users.Where(u => u.Age > 50).ToList();


  • Sintaxis similar
  • Misma intención


Pero internamente… son dos mundos completamente distintos.

  • En Java, una lambda es código ejecutable
  • En LINQ, puede ser datos (un AST)


Y eso cambia todo.


En Java, esto:

Predicate<User> p = u -> u.getAge() > 50;


Se compila a bytecode usando invokedynamic

Es equivalente a:


class Lambda implements Predicate<User> {

    public boolean test(User u) {

        return u.getAge() > 50;

    }

}


No hay forma de saber:

  • qué propiedad se accede (age)
  • qué operador se usa (>)
  • qué constante (50)

Solo podés ejecutarla, no analizarla

Por lo tanto, siempre ocurre en memoria. Nunca puede transformarse en sql o en nada


En .NET LINQ, esto:

users.Where(u => u.Age > 50)


Puede ser interpretado como:

Expression<Func<User, bool>>


Esto NO es una función

Es un árbol de expresión (AST)


¿Cómo se ve ese AST?

Conceptualmente:

GreaterThan

 ├── MemberAccess (Age)

 └── Constant (50)


O en código:

BinaryExpression(

    left: MemberExpression("Age"),

    operator: GreaterThan,

    right: Constant(50)

)


Ahora sí podés:

  • traducir a SQL
  • optimizar
  • serializar
  • ejecutar en distintos motores


¿Por qué Java eligió este camino?

Java priorizó:

  • simplicidad
  • performance
  • compatibilidad


Las lambdas fueron diseñadas como:  “syntactic sugar” sobre interfaces funcionales

No como estructuras analizables.


 ¿Se podría implementar LINQ en Java?

Sí… pero requeriría:

  • lambdas introspectables
  • un AST estándar en el lenguaje
  • cambios en el compilador


Streams y LINQ no son equivalentes.

  • Streams son pipelines de ejecución
  • LINQ es un lenguaje de queries embebido

domingo, 7 de junio de 2026

¿Se puede tener Java Linq?


Sí, podría… pero no “gratis” ni sin romper varias cosas del lenguaje como hoy está diseñado.

La clave es entender qué significa realmente “expresión” al estilo LINQ.


Primero: ¿qué es una “expresión” en LINQ?


En .NET LINQ no estás pasando una función común.

x => x.Age > 50


Eso puede ser dos cosas distintas en C#:


Func<User, bool>                 // código ejecutable

Expression<Func<User, bool>>    // árbol de expresión (AST)


En el segundo caso, el compilador no genera bytecode directo, sino una estructura tipo:


GreaterThan

 ├── MemberAccess (Age)

 └── Constant (50)


En Java eso NO existe

x -> x.getAge() > 50


Siempre es: Predicate<User>


Y eso es:

  • bytecode
  • opaco
  • no analizable


Entonces… ¿por qué Java no puede simplemente agregarlo?

Puede… pero implicaría cambios profundos 


1. Cambiar cómo funcionan las lambdas

Hoy en Java:

Predicate<User> p = x -> x.getAge() > 50;



 Se compila usando invokedynamic

Es básicamente una función


Para soportar expresiones, Java necesitaría algo como:

Expression<User, Boolean> expr = x -> x.getAge() > 50;


Eso implica que el compilador:

  • capture la estructura del código
  • no solo lo ejecute


2. Falta un AST estándar en el lenguaje

Java no tiene algo como:

expr.getBody(); // árbol de nodos


Tendrías que definir:

  • nodos (BinaryExpression, MethodCall, etc.)
  • un modelo estándar
  • APIs para recorrerlo

Básicamente: meter un mini compilador dentro del lenguaje


3. Problema de ambigüedad (esto es clave)


¿Qué pasa con esto?

x -> x.getAge() > calcularEdadMinima()


¿Eso es SQL?

 ❌ No siempre

 ❌ Puede depender de lógica Java

 ❌ Puede tener efectos secundarios


LINQ tiene reglas estrictas para esto… Java no.


4. Compatibilidad hacia atrás (el verdadero monstruo)

Java es ultra conservador.

Si agregás expresiones:

  • ¿cómo diferenciás `Predicate` vs `Expression`?
  •  ¿rompés código existente?
  • ¿cómo inferís tipos?


C# lo resolvió desde el diseño… Java llegó tarde (lambdas en Java 8).


5.  Filosofía del lenguaje

Java prioriza:

  • simplicidad
  • previsibilidad
  • menos “magia”


LINQ introduce:

  • traducción implícita
  • múltiples targets (SQL, XML, memoria)
  • comportamiento distinto según contexto


Eso no encaja del todo con el ADN de Java


Entonces… ¿es imposible?

No. Para nada.


De hecho, Java ya hace algo parecido, pero explícito:

  • JPA Criteria API
  • Querydsl
  • jOOQ


Todos construyen un AST… pero a mano


La posta (la diferencia real)

No es que Java “no pueda”.


Es que hoy:

  • Las lambdas en Java son comportamiento
  • En LINQ son datos


 Conclusión

Java podría implementar algo tipo LINQ si:

  • introduce un tipo `Expression`
  • cambia el compilador
  • define un AST estándar
  • acepta más complejidad en el lenguaje


Pero eso implicaría:

cambiar una de las decisiones fundamentales de cómo Java entiende las lambdas



viernes, 5 de junio de 2026

¿Por qué Java Streams no pueden ser LINQ?


Imaginá esto:

repoPersona

    .filter(p -> p.getEdad() > 50)

    .toList();



Y que mágicamente se transforme en:

SELECT * FROM persona WHERE edad > 50;


Sería hermoso.

Pero… ¿no son lazy los streams?

Sí, los streams en Java son lazy:


users.stream()

     .filter(u -> u.getAge() > 50)

     .toList();


No se ejecuta hasta el final


Peeeero...

Lazy no es lo mismo que interpretable


En Java, esto:

u -> u.getAge() > 50


Se compila a bytecode

No queda como estructura accesible


Es decir:

  • No podés analizarla
  • No podés transformarla
  • No podés convertirla en SQL


 ¿Por qué en C# sí se puede?


En .NET LINQ pasa algo distinto:

repo.Where(x => x.Age > 50)


Eso no es una función normal

Es: Expression<Func<T, bool>>


El código es datos (un AST)


Por esta razón necesitamos de cosas como Specification de jpa para tipar nuestras querys :(


Java Streams no pueden ser LINQ porque:  Java no puede “leer” lambdas como datos


Y eso obliga a usar soluciones como Specifications.


Specifications de JPA: Queries dinámicas y tipadas en Java


Cuando trabajamos con bases de datos en Java usando Spring Data JPA, tarde o temprano aparece un problema:

¿Cómo construyo queries dinámicas sin escribir SQL a mano?

Ahí es donde entra el patrón Specification.

Una Specification<T> es básicamente un predicado tipado que se traduce a SQL.


Permite expresar condiciones de forma declarativa:

Specification<User> isAdult = (root, query, cb) ->

    cb.greaterThanOrEqualTo(root.get("age"), 18);


Esto termina siendo:

WHERE age >= 18


Composición: el verdadero poder. Lo más interesante es que las specifications se pueden combinar:


Specification<User> isAdult = (root, query, cb) ->

    cb.greaterThanOrEqualTo(root.get("age"), 18);


Specification<User> nameStartsWithA = (root, query, cb) ->

    cb.like(root.get("name"), "A%");


Specification<User> combined =

    isAdult.and(nameStartsWithA);


WHERE age >= 18 AND name LIKE 'A%'


Se puede usar en el dao o repositorio así:


List<User> result = userRepository.findAll(combined);


Simple, expresivo y reutilizable.


Ventajas:

  • Queries dinámicas
  • Composición (`and`, `or`)
  • Tipado fuerte
  • Evita strings SQL


Desventajas:

  • Verboso (Criteria API no es linda)
  • Poco intuitivo al principio
  • Debug complicado


¿Qué está pasando por debajo?


Todo esto se apoya en JPA Criteria API

  • No estás ejecutando código
  • Estás construyendo un árbol de expresión (AST)


Por eso puede traducirse a SQL.

Specification es una forma poderosa de:

construir queries dinámicas, reutilizables y tipadas en Java


Pero… deja una pregunta interesante. ¿No podríamos hacer esto mismo con Streams?

Lo veremos en próximos posts ...



miércoles, 3 de junio de 2026

Jakarta Persistence 4.0: El mayor salto de JPA en años



Durante mucho tiempo, cuando hablábamos de persistencia en Java, hablábamos de JPA. Con la migración de Java EE a Jakarta EE, JPA pasó a llamarse Jakarta Persistence, pero durante varias versiones los cambios fueron relativamente modestos.

Con Jakarta Persistence 4.0 eso cambia.

Esta nueva versión incorpora características largamente esperadas, mejora la seguridad de tipos y prepara el terreno para trabajar junto a Jakarta Data. Veamos qué trae de nuevo.


¿Qué es Jakarta Persistence?

Jakarta Persistence es la especificación estándar para el mapeo objeto-relacional (ORM) en Java.

Permite representar tablas como objetos Java y realizar operaciones CRUD sin escribir SQL para cada interacción.


@Entity

public class Cliente {


    @Id

    private Long id;

    private String nombre;

}


Implementaciones populares incluyen:

  • Hibernate ORM
  • EclipseLink


Novedades más importantes de Jakarta Persistence 4.0:

1. EntityAgent: trabajar sin Persistence Context

Una de las novedades más llamativas es la incorporación de EntityAgent.

Hasta ahora, casi todas las operaciones pasaban por un EntityManager y un Persistence Context.


entityManager.persist(cliente);


Con EntityAgent se pueden realizar operaciones sobre entidades desacopladas del contexto de persistencia, simplificando ciertos escenarios y reduciendo overhead. 


Conceptualmente:

entityAgent.insert(cliente);


Esto acerca la API a modelos más ligeros y modernos.


2. Carga masiva por ID

Un problema clásico:


for(Long id : ids) {

    entityManager.find(Cliente.class, id);

}


Esto puede generar el famoso problema N+1.

Jakarta Persistence 4.0 incorpora soporte para obtener múltiples entidades por identificador en una sola operación. 


La ventaja:

  • Menos viajes a la base de datos.
  • Mejor rendimiento.
  • Código más simple.


3. Consultas más seguras con Static Query

Una crítica frecuente a JPA era que las consultas JPQL eran simples Strings.


@Query("select c from Cliente c")


Si el nombre de una propiedad cambiaba, el error aparecía recién en tiempo de ejecución.

Persistence 4.0 incorpora una API de Static Queries que permite mayor verificación en compilación. 


Beneficios:

  • Más seguridad de tipos.
  • Menos errores en producción.
  • Mejor soporte para herramientas y refactorización.


4. Result Set Mapping programático

Hasta ahora era habitual definir mapeos complejos mediante anotaciones.


@SqlResultSetMapping(...)


La nueva versión agrega una API programática para definir estos mappings. 


Ventajas:

  • Más flexible.
  • Más reutilizable.
  • Menos anotaciones gigantes.


5. Nuevas capacidades para Entity Graphs

Los Entity Graphs fueron introducidos para controlar qué relaciones se cargan.


@EntityGraph(attributePaths = {

    "pedidos",

    "direccion"

})


Persistence 4.0 mejora significativamente esta funcionalidad:

Nuevas anotaciones.

Mejor integración con operaciones existentes.

Uso de Entity Graphs en refresh(). 


6. Soporte para entidades Read-Only

En muchos escenarios solo queremos leer datos.

Antes:


Cliente cliente =

    entityManager.find(Cliente.class, id);


La entidad quedaba administrada por el contexto.

Persistence 4.0 agrega soporte explícito para carga en modo solo lectura. 


Beneficios:

  • Menor consumo de memoria.
  • Menor costo de dirty checking.
  • Mejor rendimiento.


7. @PreMerge

Hasta ahora existían callbacks como:


@PrePersist

@PostPersist

@PreUpdate

@PostUpdate



Ahora aparece:

@PreMerge

public void beforeMerge() {

    System.out.println("Fusionando entidad");

}


Esto permite interceptar el proceso de merge antes de que ocurra. 


8. Excluir campos del Optimistic Locking

Hasta ahora cualquier modificación podía afectar el control de versiones.

Persistence 4.0 incorpora una anotación para excluir ciertos atributos del mecanismo de optimistic locking. 


Ideal para:

  • Campos calculados.
  • Contadores.
  • Metadatos auxiliares.


9. select new implícito

Actualmente:


select new ClienteDTO(

    c.id,

    c.nombre

)

from Cliente c


Persistence 4.0 simplifica este escenario permitiendo inferir automáticamente el DTO cuando se especifica el tipo de resultado. 


10. Preparándose para Jakarta Data

Quizás el cambio más importante no sea una característica puntual.

Muchas de las nuevas APIs fueron diseñadas para trabajar mejor con Jakarta Data

La idea es ofrecer una experiencia similar a:

  • Spring Data
  • Micronaut Data
  • Quarkus Panache


pero de forma estandarizada dentro del ecosistema Jakarta EE. 


¿Vale la pena actualizar?

  • Si estás usando:
  • Hibernate moderno
  • Jakarta EE 11 o futuro Jakarta EE 12
  • Nuevos proyectos


la respuesta es sí.


Las mejoras apuntan a problemas reales que los desarrolladores vienen sufriendo desde hace años:

  • Más type safety
  • Menos Strings mágicos
  • Mejor rendimiento
  • Mejor integración con Jakarta Data
  • APIs más modernas


Jakarta Persistence 4.0 representa probablemente la evolución más importante de JPA desde la llegada de los Entity Graphs y los Stored Procedures.

La incorporación de EntityAgent, Static Queries, Result Set Mapping programático, mejoras en Entity Graphs y nuevas capacidades de carga hacen que la especificación se vea mucho más moderna y preparada para competir con las soluciones de persistencia actuales. 

Para quienes pensaban que JPA había quedado estancado, Jakarta Persistence 4.0 demuestra exactamente lo contrario.


Dejo link: 

https://jakarta.ee/specifications/persistence/4.0/



¿Qué es Native AOT en .NET?


Durante muchos años, las aplicaciones .NET funcionaron utilizando un modelo basado en JIT (Just-In-Time Compilation).

Esto significa que el código C# no se ejecuta directamente como código máquina, sino que primero se compila a un lenguaje intermedio llamado IL (Intermediate Language), y luego el runtime de .NET lo convierte a código nativo en tiempo de ejecución.

Este modelo funciona muy bien… pero tiene costos:

  • startup más lento
  • mayor consumo de memoria
  • necesidad de instalar el runtime
  • contenedores más pesados


Con la llegada del cloud, Kubernetes y serverless, estos problemas comenzaron a ser cada vez más importantes.

Para resolverlos, Microsoft introdujo Native AOT.

Native AOT (Ahead-Of-Time Compilation) es una tecnología de .NET que permite compilar una aplicación directamente a código máquina nativo.


En lugar de:

C# -> IL -> JIT -> Código Máquina


tenemos:

C# -> IL -> Código Máquina Nativo


El resultado es un ejecutable standalone que no necesita el runtime de .NET instalado en la máquina destino.


Conceptualmente, es muy similar a:

  • GraalVM
  • Go
  • Rust
  • Zig


Crearemos una aplicación Native AOT

Paso 1: Crear el proyecto

dotnet new console -n HolaNativeAot

cd HolaNativeAot


Paso 2: Modificar el .csproj

Agregamos:

<PropertyGroup>

    <PublishAot>true</PublishAot>

</PropertyGroup>


El archivo queda algo así:

<Project Sdk="Microsoft.NET.Sdk">


  <PropertyGroup>

    <OutputType>Exe</OutputType>

    <TargetFramework>net9.0</TargetFramework>

    <PublishAot>true</PublishAot>

  </PropertyGroup>


</Project>


Paso 3: Compilar


en Windows

dotnet publish -c Release -r win-x64


en Linux

dotnet publish -c Release -r linux-x64


Se genera algo similar a:

bin/Release/net9.0/linux-x64/publish/


Y dentro encontraremos un ejecutable nativo:

HolaNativeAot


o en Windows:

HolaNativeAot.exe


¿Qué ventajas tiene?

Al no existir JIT:

  • a aplicación arranca casi instantáneamente
  • ideal para serverless
  • ideal para microservicios


Menor consumo de memoria, porque:

  • no hay compilación runtime
  • menos metadata
  • runtime reducido


No hace falta instalar .NET.

El ejecutable ya contiene todo lo necesario.

Native AOT funciona muy bien junto con Docker.

Podemos generar imágenes muy livianas y rápidas de iniciar.


No todo es perfecto.

Native AOT necesita saber en tiempo de compilación qué código será utilizado.

Pero reflection rompe esa idea.


Ejemplo:

var type = Type.GetType("MiClase");

Activator.CreateInstance(type);


El compilador no puede garantizar que ese tipo será usado.

Entonces puede eliminarlo durante el trimming.

Native AOT utiliza una técnica llamada: IL Trimming

El compilador elimina código que considera innecesario.

Muy parecido al concepto de: Closed World Assumption utilizado por GraalVM.


Las librerías más dinámicas suelen tener problemas.


Por ejemplo:

  • reflection pesada
  • generación dinámica de código
  • plugins
  • proxies dinámicos
  • serializers mágicos


Native AOT funciona especialmente bien con:

  • Minimal APIs
  • gRPC
  • Workers
  • herramientas CLI
  • microservicios simples


ASP.NET Core tiene soporte parcial para Native AOT.


Se complica si usamos :

  • MVC tradicional
  • Razor Pages dinámicas
  • plugins
  • reflection intensiva


Existe también: ReadyToRun (R2R)


Se activa con:

<PublishReadyToRun>true</PublishReadyToRun>


No elimina completamente el JIT, pero mejora considerablemente el startup.

Sería una especie de “semi-AOT”.


Native AOT aparece en un contexto muy concreto:

  • Kubernetes
  • containers efímeros
  • serverless
  • autoscaling
  • cold starts


En estos escenarios:

  • arrancar rápido importa
  • consumir menos RAM importa
  • tener imágenes pequeñas importa


El modelo JIT sigue siendo excelente para:

  • aplicaciones grandes
  • desktop
  • sistemas dinámicos
  • frameworks complejos
  • escenarios con mucha reflection


Native AOT está pensado principalmente para:

  • cloud-native
  • microservicios
  • serverless
  • APIs livianas
  • herramientas CLI


Native AOT representa uno de los cambios más importantes en la evolución moderna de .NET.


donde el resultado final es:

  • un único binario
  • startup instantáneo
  • menor consumo de memoria
  • deploy extremadamente simple


Sin embargo, también obliga a abandonar parte de la flexibilidad dinámica que históricamente caracterizó a plataformas como .NET y Java.


Y justamente ahí es donde comienza uno de los debates más interesantes del desarrollo moderno: ¿El futuro es AOT o JIT?




lunes, 1 de junio de 2026

Problema: Java consumiendo mucho CPU (sin GUI ni puertos)


Si no podés usar VisualVM ni JMX remoto, la estrategia correcta es:

CLI + correlación de datos


1. Encontrar el proceso

jps -l

o directamente:

ps aux | grep java

guardate el PID


2. Confirmar que el problema es CPU

top -p <pid>

o mejor:

top -H -p <pid>

Esto muestra threads individuales


Acá está la clave:

  • Vas a ver un thread con CPU alto (ej: 200%)
  • Anotá el TID (thread id)


3. Convertir TID a HEX (clave para Java)


Java muestra threads en hexadecimal, Linux en decimal.

printf "%x\n" <tid>


Ejemplo:

12345 → 3039


4.Sacar thread dump

jstack <pid> > dump.txt


5. Buscar el thread problemático


Dentro del dump:

grep -i 3039 dump.txt


Ese es el thread que está consumiendo CPU

¿Qué vas a encontrar? Generalmente algo así:

"id=123 nid=0x3039 runnable"


Y abajo el stack:

at com.miapp.CalculoPesado.procesar(CalculoPesado.java:42)

at com.miapp.Service.loop(Service.java:88)


¡encontraste el problema!


¿Dónde entra jstat en todo esto?

Acá es donde sumás contexto.


Ejecutá:

jstat -gcutil <pid> 1000


 Qué mirar para CPU alto


Caso 1: GC excesivo

  • YGC sube MUY rápido
  • GCT crece constantemente


CPU alto por Garbage Collector


Caso 2: Full GC frecuentes

  • FGC aumenta
  • FGCT alto


pausas pesadas → CPU + latencia


Caso 3: GC normal

  • GC estable
  • memoria OK


Entonces el problema es código, no GC

domingo, 31 de mayo de 2026

jstat en Java: cómo entender el Garbage Collector desde la consola


Cuando hablamos de monitoreo en Java, muchos van directo a VisualVM.

Pero en entornos reales (servidores, contenedores, CI), necesitás herramientas sin GUI. Ahí aparece jstat.

Una herramienta liviana, incluida en la JVM, que te permite ver qué está pasando con la memoria y el Garbage Collector en tiempo real.

jstat (Java Virtual Machine Statistics Monitoring Tool) es una utilidad de consola que permite consultar métricas internas de la JVM.

  • Uso de memoria
  • Actividad del GC
  • Cantidad de recolecciones
  • Tiempos acumulados


Primero necesitás el PID del proceso:


jps -l


Después ejecutás:


jstat -gc <pid> 1000


Esto imprime estadísticas cada 1 segundo.


 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU     YGC   YGCT   FGC    FGCT     GCT

1024.0 1024.0  0.0   512.0   8192.0   4096.0   16384.0   8000.0   5120.0 4800.0 1024.0  900.0     10    0.25    2     0.50     0.75


Ahora lo importante: entender esto 👇

Generación joven (Young Gen)


S0C / S1C → tamaño de Survivor 0 y 1

S0U / S1U → uso actual

EC → tamaño de Eden

EU → uso de Eden

Acá viven los objetos nuevos.


Generación vieja (Old Gen)

OC → capacidad

OU → uso


👉 Si esto crece mucho y no baja → posible problema de memoria.


Metaspace

MC / MU → capacidad y uso

CCSC / CCSU → Compressed Class Space


👉 Relacionado con clases cargadas.


YGC → cantidad de GC jóvenes

YGCT → tiempo total en GC joven

FGC → cantidad de Full GC

FGCT → tiempo total en Full GC

GCT → tiempo total de GC


Veamos un caso saludable

  • Eden sube y baja constantemente
  • YGC aumenta progresivamente
  • Old Gen estable


👉 Todo OK.


Posible memory leak

  • OU (Old Gen) sube y nunca baja
  • Full GC frecuentes


👉 Algo está reteniendo objetos.


🟠 Problema de GC

  • YGC muy alto en poco tiempo
  • GCT creciendo rápido


👉 La app está gastando demasiado tiempo en GC.


⚙️ Otros modos útiles

-gcutil (más simple y legible)


jstat -gcutil <pid> 1000


Salida en porcentajes:

 S0     S1     E      O      M     CCS    YGC   YGCT   FGC   FGCT     GCT

 0.00  50.00  60.00  48.00  93.75 87.89   10    0.25    2     0.50     0.75


👉 Ideal para monitoreo rápido.


Ejecutar N veces

jstat -gc <pid> 1000 10


👉 Ejecuta 10 veces y termina.


jstat es una herramienta simple pero extremadamente poderosa.


✔ Ideal para servidores sin GUI

✔ Perfecta para debugging en vivo

✔ Fundamental para entender el GC

jueves, 28 de mayo de 2026

Implementando reduce y construyendo map en Scala


Una de las ideas más interesantes de la programación funcional es que muchas operaciones sobre colecciones pueden construirse a partir de una sola función general: fold (o un reduce más flexible).


Por ejemplo, este reduce recursivo:


def reduce[A, B](list: List[A], initial: B)(f: (B, A) => B): B =

  if (list.isEmpty) initial

  else reduce(list.tail, f(initial, list.head))(f)


Recorre la lista acumulando un resultado.

¿Qué hace exactamente?

La función recibe:

  • una lista
  • un valor inicial
  • una función acumuladora


La función f recibe:

(B, A) => B


Es decir:

  • el acumulador actual (B)
  • el elemento actual (A)
  • y devuelve un nuevo acumulador (B)


Ejemplo: sumar números

val numbers = List(1, 2, 3, 4)

val result =  reduce(numbers, 0)((acc, n) => acc + n)

println(result)


Salida: 10


El proceso sería algo así:

(((0 + 1) + 2) + 3) + 4


Ejemplo: concatenar strings


val words = List("Hola", "Scala", "!")

val result =  reduce(words, "")((acc, word) => acc + " " + word)

println(result)


Salida: Hola Scala !


Acá viene la parte interesante.

map transforma cada elemento de una lista:


List(1, 2, 3).map(_ * 2)


Resultado: List(2, 4, 6)


Pero podemos implementarlo usando únicamente nuestro reduce.


def map[A, B](list: List[A])(f: A => B): List[B] =

  reduce(list, List.empty[B]) { (acc, elem) =>

    acc :+ f(elem)

  }


val numbers = List(1, 2, 3)

val doubled =  map(numbers)(_ * 2)


println(doubled)


Salida: List(2, 4, 6)


En cada iteración:

acc :+ f(elem)


1. Se transforma el elemento con f

2. Se agrega al acumulador


Por ejemplo:

List()

List(2)

List(2, 4)

List(2, 4, 6)


Lo interesante es que:

  • sum
  • filter
  • map
  • count
  • flatMap

y muchas otras operaciones funcionales pueden construirse a partir de un único patrón: recorrer una estructura acumulando un resultado. Ese patrón es justamente fold.



martes, 26 de mayo de 2026

Cómo detectar cuando MySQL elige un mal plan de ejecución?


El optimizador de MySQL es muy bueno… pero no es perfecto.


A veces elige un plan que:

❌ No usa índices

❌ Hace scans innecesarios

❌ Escala mal con más datos


El problema es que la query funciona… pero lento.


Primero: qué es un “mal plan”. Un mal plan no significa que falle.

Significa que:

  • Hace más trabajo del necesario
  • No usa la mejor estrategia disponible


Señales claras de un mal plan

❌ 1. Gran diferencia entre estimado y real

rows=1000 (estimated)

actual rows=10


Esto es una alarma 🚨

MySQL toma decisiones basadas en estimaciones.


Si están mal:

  • Puede elegir un índice incorrecto
  • O directamente no usar índice

❌ 2. Full Table Scan inesperado

Table scan on libros


Si tenés índice y aun así escanea todo:

🚨 Algo está mal


Posibles causas

  • Falta de índice
  • Índice no selectivo
  • Uso de funciones en columnas
  • LIKE '%texto%'


❌ 3. Índice ignorado

Tenés índice:

CREATE INDEX idx_nombre ON autores(nombre);


Pero el plan dice:

Table scan on autores


❌ No lo está usando


Causas típicas

  • Baja selectividad
  • Estadísticas desactualizadas
  • El optimizador cree que el scan es más barato


❌ 4. Nested loops con muchas filas

Nested loop (loops=10000)


Esto escala MUY mal


Problema

Por cada fila de A:

  • Busca en B


Complejidad tipo: O(n * m)


❌ 5. Filtros aplicados tarde


👉 Si aparece después de un scan:

❌ Primero lee todo

❌ Después filtra


❌ 6. Uso excesivo de “Using temporary” o “Sort”

Using temporary

Sort: ...


Señales de:

  • Falta de índices
  • Ordenamientos costosos


Ejemplo real:

SELECT *

FROM libros

WHERE titulo LIKE '%Java%';


Plan:

Table scan on libros


Aunque tengas índice en titulo:

No se usa por el `%` inicial


Caso interesante: estimaciones incorrectas

rows=1.25 actual rows=1

Esto es normal.


Pero:

rows=10000 actual rows=1

Problema serio


Consecuencia


MySQL puede pensar:

“Esto devuelve muchas filas → mejor full scan”

Pero en realidad devuelve pocas


Cómo confirmar que el plan es malo


1. Usar EXPLAIN ANALYZE

EXPLAIN ANALYZE SELECT ...


Comparar:

  • estimado vs real
  • tiempos


2. Medir tiempo real

A veces el plan “feo” igual es rápido.


3. Probar alternativas

  • Reescribir query
  • Forzar índice:

SELECT * FROM libros FORCE INDEX (idx_titulo)

WHERE titulo LIKE 'Java%';


Cómo corregir un mal plan

1. Actualizar estadísticas

ANALYZE TABLE libros;

Muchas veces esto solo ya arregla todo


2. Crear índices correctos

Pensar en:

  • WHERE
  • JOIN
  • ORDER BY


3. Cambiar la query


Ejemplo:

LIKE '%Java%' ❌

LIKE 'Java%'  ✔️


4. Reducir datos

Menos filas = mejores decisiones


5. Forzar plan (último recurso)

USE INDEX / FORCE INDEX

Solo si estás seguro


Error común

“Si EXPLAIN dice que usa índice, está todo bien”

❌ No necesariamente

👉 Puede usarlo… pero mal


Regla de oro

Siempre mirar:

  • rows vs actual rows
  • loops
  • tipo de acceso


El optimizador de MySQL:

✔️ Es bueno

❌ Pero depende de estadísticas


Y cuando se equivoca:

  • No falla
  • Solo se vuelve lento