QueryBuilder es una API fluida para construir consultas JPA-QL de forma programática, inspirada en Hibernate Criteria API pero diseñada específicamente para JPA 1.0.
- Características
- Requisitos
- Instalación
- Guía Rápida
- Ejemplos de Uso
- API Principal
- Mejores Prácticas
- Tests
- Arquitectura
- Contribución
- API Fluida: Construcción de queries mediante method chaining
- Type-Safe: Construcción de consultas con validación de tipos
- Prevención de SQL Injection: Uso de parámetros vinculados
- Expresiones Complejas: Soporte para condiciones anidadas (AND, OR, NOT)
- Funciones SQL: AVG, SUM, COUNT, MAX, MIN, etc.
- Joins: INNER, LEFT, FULL JOIN
- Agrupamiento y Ordenamiento: GROUP BY, HAVING, ORDER BY
- Paginación: LIMIT, OFFSET
- Transformadores: Transformación de resultados customizable
- Encapsulamiento Robusto: Vistas inmutables para prevenir modificaciones accidentales
- Validaciones Exhaustivas: Validación de parámetros nulos y valores inválidos
- Java: 7 o superior
- JPA: 1.0 o superior
- JUnit: 4.x (para tests)
<dependency>
<groupId>com.querybuilder</groupId>
<artifactId>querybuilder</artifactId>
<version>1.0.0</version>
</dependency>implementation 'com.querybuilder:querybuilder:1.0.0'- Descarga el JAR desde releases
- Agrega el JAR al classpath de tu proyecto
import static com.querybuilder.expression.ExpressionFactory.*;
import com.querybuilder.QueryCreator;
import javax.persistence.EntityManager;
// Inicializar QueryCreator con EntityManager
QueryCreator qc = QueryCreator.init(entityManager);
// Construir y ejecutar consulta
List<Usuario> usuarios = qc
.select(get(path("u.nombre"), "nombre"), get(path("u.email"), "email"))
.from(entity(Usuario.class, "u"))
.whereAll(
eq("u.activo", true),
gt("u.edad", 18)
)
.orderBy("u.nombre asc")
.all();QueryCreator qc = QueryCreator.init(entityManager);
List<Producto> productos = qc
.select(get(path("p"), "producto"))
.from(entity(Producto.class, "p"))
.whereAll(eq("p.disponible", true))
.all();SQL Generado:
SELECT p as producto
FROM Producto p
WHERE ( p.disponible = :e0 )List<Usuario> usuarios = qc
.select(get(path("u"), "usuario"))
.from(entity(Usuario.class, "u"))
.whereAll(
eq("u.activo", true),
gt("u.edad", 18),
like("u.email", "%@example.com")
)
.all();SQL Generado:
SELECT u as usuario
FROM Usuario u
WHERE ( u.activo = :e0 AND u.edad > :e1 AND u.email LIKE :e2 )List<Pedido> pedidos = qc
.select(get(path("p"), "pedido"))
.from(entity(Pedido.class, "p"))
.whereAny(
eq("p.estado", "PENDIENTE"),
eq("p.estado", "PROCESANDO")
)
.all();SQL Generado:
SELECT p as pedido
FROM Pedido p
WHERE ( p.estado = :e0 OR p.estado = :e1 )List<Object[]> resultado = qc
.select(
get(path("u.nombre"), "nombre"),
get(path("d.nombre"), "departamento")
)
.from(entity(Usuario.class, "u"))
.join(joinTo("u.departamento", "d", JoinType.LEFT))
.whereAll(eq("u.activo", true))
.all();SQL Generado:
SELECT u.nombre as nombre, d.nombre as departamento
FROM Usuario u
LEFT JOIN u.departamento as d
WHERE ( u.activo = :e0 )List<Object[]> estadisticas = qc
.select(
get(path("p.categoria"), "categoria"),
get(count("p.id"), "total"),
get(avg("p.precio"), "promedio")
)
.from(entity(Producto.class, "p"))
.groupBy("p.categoria")
.havingAll(gt("count(p.id)", 5))
.orderBy("total desc")
.all();SQL Generado:
SELECT p.categoria as categoria, COUNT(p.id) as total, AVG(p.precio) as promedio
FROM Producto p
GROUP BY p.categoria
HAVING ( count(p.id) > :e0 )
ORDER BY total desc// Obtener 10 resultados a partir del registro 20
List<Producto> productos = qc
.select(get(path("p"), "producto"))
.from(entity(Producto.class, "p"))
.orderBy("p.nombre asc")
.take(20, 10) // offset: 20, limit: 10
.all();
// Alternativa usando startAt y limit
List<Producto> productos2 = qc
.select(get(path("p"), "producto"))
.from(entity(Producto.class, "p"))
.orderBy("p.nombre asc")
.startAt(20)
.limit(10)
.all();// (activo = true AND edad > 18) OR (vip = true)
ConditionExpression condicion = any(
all(
eq("u.activo", true),
gt("u.edad", 18)
),
eq("u.vip", true)
);
List<Usuario> usuarios = qc
.select(get(path("u"), "usuario"))
.from(entity(Usuario.class, "u"))
.whereAll(condicion)
.all();List<Integer> ids = Arrays.asList(1, 2, 3, 4, 5);
List<Producto> productos = qc
.select(get(path("p"), "producto"))
.from(entity(Producto.class, "p"))
.whereAll(in("p.id", ids))
.all();SQL Generado:
SELECT p as producto
FROM Producto p
WHERE ( p.id IN :e0 )// Obtener un único resultado (lanza excepción si hay más de uno)
Usuario usuario = qc
.select(get(path("u"), "usuario"))
.from(entity(Usuario.class, "u"))
.whereAll(eq("u.id", 123))
.one();
// Obtener el primer resultado
Usuario primerUsuario = qc
.select(get(path("u"), "usuario"))
.from(entity(Usuario.class, "u"))
.whereAll(eq("u.activo", true))
.orderBy("u.nombre asc")
.first();Transformer<UsuarioDTO> transformer = new AbstractTransformer<UsuarioDTO>() {
@Override
public UsuarioDTO transform(Object o) {
Object[] row = (Object[]) o;
UsuarioDTO dto = new UsuarioDTO();
dto.setNombre((String) row[0]);
dto.setEmail((String) row[1]);
return dto;
}
};
List<UsuarioDTO> usuarios = qc
.select(
get(path("u.nombre"), "nombre"),
get(path("u.email"), "email")
)
.from(entity(Usuario.class, "u"))
.whereAll(eq("u.activo", true))
.all(transformer);Clase principal para construir queries con interfaz fluida.
QueryCreator init(EntityManager entityManager)| Método | Descripción |
|---|---|
select(Select... selects) |
Define las columnas a seleccionar |
from(From... froms) |
Define las entidades raíz |
join(Join... joins) |
Agrega joins (INNER, LEFT, FULL) |
whereAll(ConditionExpression... conditions) |
Condiciones con AND |
whereAny(ConditionExpression... conditions) |
Condiciones con OR |
groupBy(String... fields) |
Agrega GROUP BY |
havingAll(ConditionExpression... conditions) |
Condiciones HAVING con AND |
havingAny(ConditionExpression... conditions) |
Condiciones HAVING con OR |
orderBy(String... fields) |
Agrega ORDER BY |
take(int offset, int limit) |
Paginación (offset + limit) |
startAt(int offset) |
Define offset para paginación |
limit(int limit) |
Define límite de resultados |
| Método | Descripción |
|---|---|
all() |
Retorna lista de resultados |
one() |
Retorna un único resultado |
first() |
Retorna el primer resultado |
all(Transformer<T> transformer) |
Retorna lista transformada |
one(Transformer<T> transformer) |
Retorna un resultado transformado |
first(Transformer<T> transformer) |
Retorna el primer resultado transformado |
Métodos estáticos para crear expresiones.
SimpleCondition eq(String property, Object value) // Igual
SimpleCondition ne(String property, Object value) // No igual
SimpleCondition gt(String property, Object value) // Mayor que
SimpleCondition ge(String property, Object value) // Mayor o igual
SimpleCondition lt(String property, Object value) // Menor que
SimpleCondition le(String property, Object value) // Menor o igual
SimpleCondition like(String property, Object value) // LIKE
SimpleCondition notLike(String property, Object value) // NOT LIKESimpleCondition isNull(String property) // IS NULL
SimpleCondition isNoNull(String property) // IS NOT NULLSimpleCondition in(String property, Object value) // IN
SimpleCondition notin(String property, Object value) // NOT INConditionExpression all(ConditionExpression... conditions) // AND
ConditionExpression any(ConditionExpression... conditions) // OR
ConditionExpression not(ConditionExpression condition) // NOTFunctionExpression count(String expression)
FunctionExpression sum(String expression)
FunctionExpression avg(String expression)
FunctionExpression max(String expression)
FunctionExpression min(String expression)ValueExpression value(Object value) // Valor parametrizado
PathExpression path(String path) // Path de propiedad
LiteralExpression literal(String literal) // Literal SQLSelect get(Expression expression, String alias) // SELECT expression AS alias
From entity(Class<?> entityClass, String alias) // FROM Entity AS alias
Join joinTo(String path, String alias, JoinType type) // JOIN path AS alias✅ Correcto:
whereAll(eq("u.nombre", nombreUsuario)) // Usa parámetros vinculados❌ Incorrecto:
whereAll(literal("u.nombre = '" + nombreUsuario + "'")) // Vulnerable a SQL injectionif (nombreUsuario == null || nombreUsuario.trim().isEmpty()) {
throw new IllegalArgumentException("Nombre de usuario requerido");
}
List<Usuario> usuarios = qc
.select(get(path("u"), "usuario"))
.from(entity(Usuario.class, "u"))
.whereAll(eq("u.nombre", nombreUsuario))
.all();❌ Incorrecto:
QueryCreator qc = QueryCreator.init(entityManager);
List<Usuario> usuarios1 = qc.from(...).all();
List<Producto> productos = qc.from(...).all(); // Estado corrupto✅ Correcto:
QueryCreator qc1 = QueryCreator.init(entityManager);
List<Usuario> usuarios = qc1.from(...).all();
QueryCreator qc2 = QueryCreator.init(entityManager);
List<Producto> productos = qc2.from(...).all();// En lugar de mapear manualmente
List<Object[]> results = qc.select(...).all();
List<UsuarioDTO> dtos = new ArrayList<>();
for (Object[] row : results) {
UsuarioDTO dto = new UsuarioDTO();
dto.setNombre((String) row[0]);
dtos.add(dto);
}
// Usar transformador
List<UsuarioDTO> dtos = qc.select(...).all(new UsuarioDTOTransformer());El QueryObject retornado por getQueryObject() tiene vistas inmutables:
QueryObject qo = qc.getQueryObject();
qo.getSelects(); // ✅ Lectura permitida
qo.getSelects().clear(); // ❌ Lanza UnsupportedOperationException// Siempre usar ORDER BY con paginación para resultados consistentes
List<Producto> productos = qc
.select(get(path("p"), "producto"))
.from(entity(Producto.class, "p"))
.orderBy("p.id asc") // ← Importante para paginación
.startAt(0)
.limit(10)
.all();El proyecto incluye una suite completa de tests con cobertura ~70-80%.
# Maven
mvn test
# Gradle
gradle testtest/
├── com/querybuilder/
│ ├── QueryCreatorValidationTest.java # Tests de validaciones
│ ├── expression/
│ │ ├── BugFixTest.java # Tests de bugs corregidos
│ │ ├── ConditionTest.java # Tests de condiciones
│ │ └── QueryBuilderTest.java # Tests generales
│ └── query/
│ └── QueryObjectEncapsulationTest.java # Tests de encapsulamiento
- Tests Funcionales: Verifican funcionalidad correcta
- Tests de Regresión: Previenen reaparición de bugs
- Tests de Validación: Verifican validaciones de entrada
- Tests de Seguridad: Verifican encapsulamiento
- Tests de Edge Cases: Casos límite y situaciones especiales
El proyecto implementa el patrón Interpreter:
- Context:
QueryObject- mantiene el estado de la consulta - Abstract Expression:
Expression- interfaz para todas las expresiones - Concrete Expressions: Implementaciones específicas (WHERE, SELECT, etc.)
com.querybuilder/
├── QueryCreator.java # API fluida principal
├── query/
│ ├── QueryObject.java # Context (estado de la consulta)
│ ├── Select.java # DTO para SELECT
│ ├── From.java # DTO para FROM
│ └── Join.java # DTO para JOIN
├── expression/
│ ├── Expression.java # Interfaz base
│ ├── QueryExpression.java # Parser principal
│ ├── ConditionExpression.java # Base para condiciones
│ ├── ValueExpression.java # Valores parametrizados
│ ├── PathExpression.java # Paths de propiedades
│ ├── FunctionExpression.java # Funciones SQL
│ ├── ExpressionFactory.java # Factory para crear expresiones
│ ├── conditions/ # Implementaciones de condiciones
│ │ ├── SimpleCondition.java
│ │ ├── AllCondition.java # AND
│ │ ├── AnyCondition.java # OR
│ │ └── NotCondition.java # NOT
│ └── clausules/ # Expresiones de cláusulas
│ ├── SelectExpression.java
│ ├── FromExpression.java
│ ├── WhereExpression.java
│ ├── JoinExpression.java
│ ├── GroupExpression.java
│ ├── HavingExpression.java
│ └── OrderExpression.java
└── transformer/
├── Transformer.java # Interfaz para transformación
└── AbstractTransformer.java # Implementación base
- Encapsulamiento: Vistas inmutables en getters públicos
- Validación: Validaciones exhaustivas en todos los métodos
- Prevención de SQL Injection: Uso obligatorio de parámetros vinculados
- Fail-Fast: Validaciones tempranas con excepciones claras
- Fluent API: Method chaining para construcción intuitiva
Las contribuciones son bienvenidas. Por favor:
- Fork el repositorio
- Crea una rama para tu feature (
git checkout -b feature/AmazingFeature) - Commit tus cambios (
git commit -m 'Add some AmazingFeature') - Push a la rama (
git push origin feature/AmazingFeature) - Abre un Pull Request
- Sigue el estilo de código existente
- Agrega tests para nuevas funcionalidades
- Actualiza la documentación según sea necesario
- Asegúrate de que todos los tests pasen
Este proyecto está bajo la Licencia MIT. Ver el archivo LICENSE para más detalles.
- Inspirado en Hibernate Criteria API
- Construido para JPA 1.0
- Mejorado con contribuciones de la comunidad
- Autor: rtincar
- Issues: GitHub Issues
- Pull Requests: GitHub PRs
- API fluida para construcción de queries
- Soporte completo para JPA-QL
- Encapsulamiento robusto con vistas inmutables
- Validaciones exhaustivas
- Suite completa de tests (cobertura ~70-80%)
- HavingExpression: Eliminado operador extra al final
- WhereExpression: Manejo correcto de condiciones vacías
- JoinExpression: Refactorizado a if-else-if para eficiencia
- Vistas inmutables en getters públicos
- Métodos package-private para acceso interno
- API pública controlada (addParameter, addAllParameters)
- JavaDoc exhaustivo con ejemplos
- Eliminado código duplicado
- Eliminados TODOs auto-generados
- Eliminado código comentado
- Mejorada documentación
¡Disfruta construyendo queries dinámicas con QueryBuilder! 🚀