Curso Jakarta EE 9 (38). JPA con Hibernate (21): trabajando con SQL

logo Jakarta EE

Idealmente, las consultas en JPA se escriben con JPQL \ Criteria API utilizando el modelo de entidades para que sean agnósticas con respecto a la base de datos. No obstante, contamos con un magnífico soporte para la ejecución de consultas SQL, denominadas «nativas» en la especificación. ¿Por qué querríamos recurrir a él? Ya apunté algunos motivos en la introducción a los capítulos dedicados a JPA.

JPQL incorpora las funcionalidades más habituales del estándar SQL y vimos varias maneras de usar funciones propias de la base de datos subyacente para ampliar sus capacidades. Con todo, de vez en cuando nos toparemos con alguna consulta más o menos compleja que se nos resiste, pero que sabemos escribir en SQL gracias a características tales como funciones analíticas, reuniones laterales o tablas derivadas (subconsultas en el FROM) de las que carece JPQL. Recordemos que es un lenguaje pensado para que los proveedores de JPA puedan ofrecerlo para una amplia variedad de bases de datos.

Incluso aunque seamos capaces de recuperar la información que queremos con JPQL, quizás podamos hacerlo con mayor eficiencia escribiendo una consulta nativa. No siempre el SQL que genera el ORM por nosotros es óptimo, y no podemos perderlo de vista para detectar cuándo se están haciendo operaciones con un mal rendimiento.

Otro caso de uso son los procedimientos almacenados (stored procedures) que analizaré en el próximo capítulo.

Todos esta casuística está contemplada por el gestor de entidades, aprovechando la equivalencia entre las tablas y entidades definidas en nuestro modelo de datos. Por tanto, no tenemos que recurrir al empleo de la engorrosa API JDBC.

Nota 1.Casi todo lo tratado en este capítulo es una repetición de lo visto en las entregas 32 y 33. Seré breve, poniendo el foco en las diferencias.

Nota 2. Obsérvese que todas las consultas de ejemplo serán triviales de escribir con JPQL o Criteria API, y así deberíamos hacerlo. He querido que sean muy simples porque lo importante es cómo trabajar con ellas.

>>>> ÍNDICE <<<<

Ejecución de consultas

Las consultas nativas se configuran y ejecutan con las interfaces Query creadas con los métodos createNativeQuery del gestor de entidades. Aquí apreciamos la primera gran diferencia con respecto a JPQL y Criteria API: no contamos con su versión tipada TypedQuery<T>.

Empecemos con un ejemplo básico.

    @Override
    public List<Coupon> findAll() {
        return em.createNativeQuery("SELECT * FROM coupons ORDER BY name", Coupon.class)
                .getResultList();
    }

El segundo argumento de createQuery puede ser un poco confuso si tenemos en mente JPQL; no sirve para crear una Query tipada, sino que indica la entidad de los resultados (solo en los casos en los que así sea).

Veamos con detalle las opciones para recoger los resultados.

Entidad

Es el ejemplo anterior, vemos que Hibernate es capaz de cargar la proyección en la entidad atendiendo a la relación entre atributos y columnas. La SELECT debe proyectar todas las columnas de la entidad. No más, pero tampoco menos. Lo siguiente funciona.

em.createNativeQuery("SELECT id, expiration, amount, expense_id, name FROM coupons ORDER BY name", Coupon.class)

Esto no.

em.createNativeQuery("SELECT id, name FROM coupons ORDER BY name", Coupon.class)
Caused by: java.sql.SQLException: Column 'amount' not found.
	at com.mysql@8.0.25//com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)

Así pues, recuperar exactamente una entidad completa es trivial. ¿Y si queremos más de una? Pensemos en esta JPQL.

SELECT c
FROM Coupon c JOIN FETCH c.expense
ORDER BY c.name

Obtiene los cupones que tienen un gasto. La reunión de tipo FETCH provoca que también se recupere la entidad Expense asociada a Coupon y que es de tipo LAZY. Intentemos conseguir lo mismo reuniendo las tablas coupons y expenses y proyectando el emparejamiento entre ambas entidades.

    @Override
    public List<Coupon> findAllWithExpense() {
        return em.createNativeQuery("SELECT c.*, e.* FROM coupons c " +
                "JOIN expenses e ON c.expense_id = e.id ORDER BY name", Coupon.class)
                 .getResultList();
    }

Aunque cada resultado es el par cupón-gasto, si lo devolvemos como la entidad cupón esta no tendrá su gasto. Cuando el resultado consista en varias entidades, configuraremos una conversión del ResultSet tal que así.

@SqlResultSetMapping(
        name = "CouponWithCategory",
        entities = {
                @EntityResult(entityClass = Coupon.class),
                @EntityResult(entityClass = Expense.class, fields = {
                        @FieldResult(name = "id", column = "exp_id"),
                        @FieldResult(name = "amount", column = "exp_amount"),
                        @FieldResult(name = "category", column = "category_id"),
                        @FieldResult(name = "concept", column = "concept"),
                        @FieldResult(name = "comments", column = "comments"),
                        @FieldResult(name = "date", column = "date")}
                )
        }
)
public class Coupon {

@SqlResultSetMapping se declara en una entidad y se usa para definir la asignación del resultado de una consulta nativa a clases. En findAllWithExpense cada resultado consiste en una entidad Coupon y otra Expense, así que declaramos ambas entidades con @EntityResult. Como las dos tienen atributos de igual nombre (id y amount), debemos evitar la confusión. Hacemos la distinción indicando la conversión explícita entre columnas y atributos de una de ellas; he optado por Expense.

Alternativamente, la misma configuración puede realizarse en el fichero /META-INF/orm.xml que ya tenemos en el proyecto de ejemplo porque lo usamos en un capítulo anterior para definir una consulta nombrada JPQL -más adelante haremos lo mismo para SQL-. Las configuraciones en XML hace años que están en desuso, pero en este caso concreto me parece una buena opción para centralizar configuraciones de persistencia y no repartirlas por las entidades.

    <sql-result-set-mapping name="CouponWithCategory">
        <entity-result entity-class="com.danielme.jakartaee.jpa.entities.Coupon"/>
        <entity-result entity-class="com.danielme.jakartaee.jpa.entities.Expense">
            <field-result name="id" column="exp_id"/>
            <field-result name="amount" column="exp_amount"/>
            <field-result name="category" column="category_id"/>
            <field-result name="comments" column="comments"/>
            <field-result name="concept" column="concept"/>
            <field-result name="date" column="date"/>
        </entity-result>
    </sql-result-set-mapping>

Con independencia de la opción elegida, utilizamos el ResultSetMapping llamado CouponWithCategory del siguiente modo.

    @Override
    public List<Coupon> findAllWithExpense() {
        List<Object[]> results = em.createNativeQuery("SELECT c.*, " +
                "e.id as exp_id, e.amount as exp_amount, e.comments, e.concept,e.date, e.category_id FROM coupons c " +
                "JOIN expenses e ON c.expense_id = e.id ORDER BY name", "CouponWithCategory")
                                   .getResultList();
        return results.stream().map(o -> (Coupon) o[0]).collect(Collectors.toList());
    }

Hay dos detalles interesantes. Lo primero es que se indica el nombre del «mapeador» en el método createNativeQuery. Puesto que lo tenemos definido con el mismo nombre tanto en la entidad como en el orm.xml, este último tiene preferencia. Lo segundo es que el resultado es una lista en la que cada elemento representa uno de los resultados, modelado como un de array de Object donde cada posición contiene una entidad. Dado que la primera posición contiene el cupón -se respeta el orden en el que las entidades aparecen en la declaración del mapping– y que Hibernate es «inteligente» y le establece su gasto, nos quedamos con ella para construir el listado de cupones final.

Escalar

Cuando el resultado es un único escalar (un número, una cadena, etc.), podemos devolver el retorno de la consulta tal cual si aplicamos la conversión adecuada. Este método obtiene la suma de todos los gastos.

@Override
public BigDecimal sum() {
    return (BigDecimal) em.createNativeQuery("SELECT SUM(amount)  FROM expenses").getSingleResult();
}

Examinemos a continuación las alternativas genéricas que tenemos para recoger cualquier resultado. Son las mismas que vimos para JPQL y Criteria API. Por consiguiente, no nos detendremos demasiado en ello.

En bruto.

En general, cualquier resultado de una consulta nativa se puede recoger en un array de Object en el que cada posición contiene uno los elementos de la proyección, respetando su orden en la SELECT.

@Override
public List<ExpenseSummaryDTO> getSummaryRaw() {
        List<Object[]> resultList = em.createNativeQuery("SELECT concept, amount, date FROM expenses ORDER BY amount desc, date desc").getResultList();
        return resultList.stream().map(o -> new ExpenseSummaryDTO(
                (String) o[0],
                (BigDecimal) o[1],
                ((java.sql.Date) o[2]).toLocalDate())).collect(Collectors.toList());
    }
LISTA     OBJECT[]
(0) --->  [concept] [amount] [date]
(1) --->  [concept] [amount] [date]

Trabajar con esta estructura de datos no es nada práctico, así que lo habitual será volcarla en una clase. Es lo que he hecho con la ayuda de Stream.

La interfaz Tuple

Lo mismo da que el array provenga de una consulta SQL o JPQL: JPA puede encapsularlo un objeto de tipo Tuple para facilitar su uso.

    @Override
    public List<ExpenseSummaryDTO> getSummaryTuple() {
        List<Tuple> resultList = em.createNativeQuery("SELECT concept, amount, date FROM expenses ORDER BY amount desc, date desc", Tuple.class).getResultList();
        return resultList.stream()
                         .map(t -> new ExpenseSummaryDTO(
                                 t.get(0, String.class),
                                 t.get(1, BigDecimal.class),
                                 t.get(2, LocalDate.class)))
                         .collect(Collectors.toList());
    }

Es más práctico acceder con el alias de cada columna.

    @Override
    public List<ExpenseSummaryDTO> getSummaryTuple() {
        List<Tuple> resultList = em.createNativeQuery("SELECT * FROM expenses ORDER BY amount desc, date desc", Tuple.class).getResultList();
        return resultList.stream()
                         .map(t -> new ExpenseSummaryDTO(
                                 t.get("concept", String.class),
                                 t.get("amount", BigDecimal.class),
                                 t.get("date", java.sql.Date.class).toLocalDate()))
                         .collect(Collectors.toList());
    }

Constructor

Recordemos que en JPQL podemos volcar el resultado en un constructor gracias al operador NEW.

SELECT NEW com.danielme.jakartaee.jpa.dto.ExpenseSummaryDTO(e.concept, e.amount, e.date)
FROM Expense e

Es la manera más práctica de transformar el resultado en un DTO ya que Hibernate se encarga de ello. Aunque esta proyección no es trasladable a SQL, podemos usar un constructor recurriendo a @SqlResultSetMapping, en esta ocasión empleando @ConstructorResult para indicar el orden en el que las columnas deben proporcionarse. Esta es la configuración equivalente al operador NEW anterior.

@SqlResultSetMapping(
        name = "ExpenseSummaryMapping",
        classes = @ConstructorResult(
                targetClass = ExpenseSummaryDTO.class,
                columns = {
                        @ColumnResult(name = "concept"),
                        @ColumnResult(name = "amount"),
                        @ColumnResult(name = "date", type = LocalDate.class)
                }))
public class Expense {
public ExpenseSummaryDTO(String concept, BigDecimal amount, java.sql.Date date) {
    this(concept, amount, date.toLocalDate());
}

Además de ser cuidadosos con el orden, hay que considerar los tipos de los objetos. La propiedad type permite indicar la clase que espera el constructor. La usaremos cuando no coincida con la clase que de forma predeterminada retorne la consulta y ambas sean convertibles. Es el caso del ejemplo: se ha especificado LocalDate porque el controlador JDBC nos da la columna date en la forma de java.sql.Date.

Si la definición es errónea, veremos una excepción de este tipo que, por desgracia, no indica los campos problemáticos.

java.lang.IllegalArgumentException: Could not locate appropriate constructor on class : com.danielme.jakartaee.jpa.dto.ExpenseSummaryDTO

Lo misma configuración en el fichero orm.xml. Ambas tienen el mismo nombre, tiene preferencia la del fichero.

<sql-result-set-mapping name="ExpenseSummaryMapping">
    <constructor-result target-class="com.danielme.jakartaee.jpa.dto.ExpenseSummaryDTO">
        <column name="concept"/>
        <column name="amount"/>
        <column name="date" class="java.time.LocalDate"/>
   </constructor-result>
</sql-result-set-mapping>

Aplicamos el SqlResultSetMapping como ya sabemos: indicando su nombre en el momento de creación del objeto Query.

    @Override
    public List<ExpenseSummaryDTO> getSummaryConstructor() {
        return em.createNativeQuery("SELECT concept, amount, date " +
                "FROM expenses ORDER BY amount desc, date desc", "ExpenseSummaryMapping")
                 .getResultList();
    }

En cualquier caso, usar constructores con SQL es fastidioso, nada que ver con JPQL y Criteria API. A continuación veremos alternativas mejores ofrecidas por Hibernate.

ResultTransformer

Este mecanismo, exclusivo de Hibernate, también está disponible para consultas nativas, pues se aplica a su objeto Query. Vimos que un transformador de resultados es una implementación de una interfaz con dos métodos.

  • transformTuple. Se invoca para cada resultado con un Object[] que contiene los elementos de la proyección. Lo usaremos para crear y devolver el objeto que represente al resultado.
  • transformList.se invoca tras el procesamiento completo del ResultSet y, por tanto, después de todas las llamadas a transformTuple. Aquí recibimos la lista con todos los resultados para hacer cualquier tipo de procesamiento final.
    private static class ExpenseSummaryTransformer implements ResultTransformer {

        private static final long serialVersionUID = 1L;

        @Override
        public Object transformTuple(Object[] tuple, String[] aliases) {
            return new ExpenseSummaryDTO(
                    (String) tuple[0],
                    (BigDecimal) tuple[1],
                    ((java.sql.Date) tuple[2]).toLocalDate());
        }

        @Override
        public List transformList(List collection) {
            return collection;
        }

    }

Aplicamos el transformador con el método setResultTransformer al objeto Query propio de Hibernate.

@Override
public List<ExpenseSummaryDTO> getSummaryCustomResultTransformer() {
    return em.createNativeQuery("SELECT concept, amount, date " +
            " FROM expenses ORDER BY amount desc, date desc")
             .unwrap(org.hibernate.query.Query.class)
             .setResultTransformer(new ExpenseSummaryTransformer())
             .getResultList();
}

Nota. setResultTransformer fue marcado obsoleto en Hibernate 5.2, pero no se ofrece una alternativa hasta Hibernate 6.0, versión en la que sigue disponible esta funcionalidad tal y como la presento en este capítulo.

Cuando vimos los transformadores aplicados a JPQL y Criteria API, afirmé que no aportan gran cosa, más allá de organizar el tratamiento del resultado en bruto. Pero esto cambia ahora que lidiamos con consultas nativas porque el transformador AliasToBeanResultTransformer nos da algo parecido a la proyección automática en constructor de JPQL. Con él seremos capaces de recoger el resultado en un DTO sin tener que hacer nada, siempre y cuando posea atributos, accesibles con setters, de nombres coincidentes con los alias de las columnas y que los tipos de ambos sean compatibles.

@Override
public List<ExpenseSummaryDTO> getSummaryAliasResultTransformer() {
   return em.createNativeQuery("SELECT concept, amount, date FROM expenses ORDER BY amount desc, date desc")
             .unwrap(org.hibernate.query.Query.class)
             .setResultTransformer(new AliasToBeanResultTransformer(ExpenseSummaryDTO.class))
             .getResultList();
}

Si no existe el set para alguna columna, se lanza una excepción.

org.hibernate.PropertyNotFoundException: Could not resolve PropertyAccess for

Pan comido, pero la magia del transformador tiene sus límites y el código anterior no funciona porque el DTO no ofrece un set válido para el tipo de la fecha que devuelve la consulta (java.sql.Date), así que lo creamos, teniendo en cuenta que nunca será nula. Es el mismo problema que apareció al definir ExpenseSummaryMapping.

    public void setDate(java.sql.Date date) {
        this.date = date.toLocalDate();
    }

Por último, dejo un ejemplo del uso de AliasToBeanConstructorResultTransformer, otro transformador que encontramos en el paquete org.hibernate.transform. Permite usar un constructor sin necesidad de crear un ConstructorResult , pero necesitamos declararlo con un objeto de tipo Constructor. Su ventaja sobre AliasToBeanResultTransformer es que no se requieren los métodos setters. En consecuencia, el DTO puede ser inmutable (atributos finales), algo deseable en este tipo de clases, e irrenunciable si usamos los records de Java 17.

public ExpenseSummaryDTO(String concept, BigDecimal amount, java.sql.Date date) {
    this(concept, amount, date.toLocalDate());
}
@Override
public List<ExpenseSummaryDTO> getSummaryConstructorResultTransformer() {
    Constructor<ExpenseSummaryDTO> constructor;
    try {
        constructor = ExpenseSummaryDTO.class.getConstructor(String.class, BigDecimal.class, java.sql.Date.class);
    } catch (NoSuchMethodException e) {
        throw new RuntimeException("constructor for " + ExpenseSummaryDTO.class + "not found !!!");
    }
    return em.createNativeQuery("SELECT concept, amount, date FROM expenses ORDER BY amount desc, date desc")
            .unwrap(org.hibernate.query.Query.class)
            .setResultTransformer(new AliasToBeanConstructorResultTransformer(constructor))
            .getResultList();
}

Parámetros de entrada

El sistema de gestión de parámetros o variables de entrada a reemplazar en tiempo de ejecución que define la especificación para las consultas nativas es el mismo de JDBC. Se indican en la consulta con el caracter ‘?’ y sus valores se proporcionan indicando el orden en el que aparecen.

em.createNativeQuery("SELECT * FROM expenses WHERE " +
                        "(? IS NULL OR date >= ?) AND (? IS NULL OR date <= ?) ORDER BY date DESC, amount DESC",
                Expense.class)
                 .setParameter(1, dateFrom)
                 .setParameter(2, dateFrom)
                 .setParameter(3, dateTo)
                 .setParameter(4, dateTo)
                 .getResultList();

Es un sistema confuso y propenso a errores. En el ejemplo, hay parámetros repetidos a los que siempre hay que darles valor aunque sea el mismo. Además, viendo la cadena con la consulta no queda claro este detalle. Si hay muchos parámetros, hay que andar contándolos con cuidado. Creánme cuando digo que he perdido tiempo con detalles de este tipo

Por fortuna, Hibernate admite en SQL el sistema de parámetros de JPQL, más práctico. Recordemos que consiste en dar a cada parámetro distinto un nombre único precedido por dos puntos. Al darles valor, se usa el nombre (sin los dos puntos) para indicar el parámetro. Solo se le da valor a cada parámetro una vez.

em.createNativeQuery("SELECT * FROM expenses WHERE (:dateFrom IS NULL OR date >= :dateFrom) " +
                        "AND (:dateTo IS NULL OR date <= :dateTo) ORDER BY date DESC, amount DESC",
                Expense.class)
                 .setParameter("dateFrom", dateFrom)
                 .setParameter("dateTo", dateTo)
                 .getResultList();

En vez de nombres, se pueden usar números precedidos por «?». No tienen por qué seguir un orden secuencial, aunque es lo más lógico.

em.createNativeQuery("SELECT * FROM expenses WHERE " +
                       "(?1 IS NULL OR date >= ?1) AND (?2 IS NULL OR date <= ?2) ORDER BY date DESC, amount DESC",
                Expense.class)
                 .setParameter(1, dateFrom)
                 .setParameter(2, dateTo)
                 .getResultList();

Si no es problema salir del estándar, mejor decantarse por una de estas dos opciones. Lo que jamás debemos hacer es «pegar» los valores a la cadena con la consulta porque nos expone a problemas de seguridad de tipo SQL injection que comenté en su momento.

Consultas nombradas

Esta es otra de las funcionalidades aplicables a JPQL que está disponible para las consultas nativas. Consiste en registrar una consulta en la unidad de persistencia con un nombre y usarla con el método createNamedQuery. Si son nativas, se definen con la anotación repetible @NamedNativeQuery.

@NamedNativeQuery(name = "expense.findByText", query = "SELECT * FROM expenses " +
        "WHERE UPPER(concept) like UPPER(:text) ORDER BY concept")
public class Expense {

El resultado será una lista de Object[].

List<Object[]> resultList = em.createNamedQuery("Expense.FindByText")
                            .setParameter("text", text == null ? "%" : "%" + text + "%")
                            .getResultList();

Si el resultado sea una entidad, la indicamos con la propiedad resultClass.

@NamedNativeQuery(name = "Expense.findByText", query = "SELECT * FROM expenses " +
        "WHERE UPPER(concept) like UPPER(:text) ORDER BY concept",
        resultClass = Expense.class)

Asimismo, podemos aplicar un ResultSetMapping.

@SqlResultSetMapping(
        name = "ExpenseSummaryMapping",
        classes = @ConstructorResult(
                targetClass = ExpenseSummaryDTO.class,
                columns = {
                        @ColumnResult(name = "concept"),
                        @ColumnResult(name = "amount"),
                        @ColumnResult(name = "date", type = LocalDate.class)
                }))
@NamedNativeQuery(name = "expenseSummary", query = "SELECT concept, amount, date " +
        "FROM expenses ORDER BY amount desc, date desc",
        resultSetMapping = "ExpenseSummaryMapping")

En ambos casos, HIbernate conoce la clase en la que terminará recogiendo los resultados y podemos crear la consulta nombrada como una TypedQuery.

@Override
public List<ExpenseSummaryDTO> getSummaryNamedQuery() {
    return em.createNamedQuery("Expense.Summary", ExpenseSummaryDTO.class)
            .getResultList();
}

Tal y como cabría esperar, la consulta nombrada se puede definir en el fichero orm.xml.

<named-native-query name="expenseFindByText"
                        result-class="com.danielme.jakartaee.jpa.entities.Expense">
   <query>SELECT * FROM expenses WHERE UPPER(concept) like UPPER(:text) ORDER BY concept</query></named-native-query>

O de forma programática, porque lo que registramos es un objeto Query.

Query nativeQuery = em.createNativeQuery("SELECT * FROM expenses " +
                "WHERE UPPER(concept) like UPPER(:text) ORDER BY concept", Expense.class);
em.getEntityManagerFactory().addNamedQuery("Expense.findByText", nativeQuery);

Paginación

Podemos hacerla en la consulta según la base de datos que usemos. Por ejemplo, en MySQL con LIMIT.

@Override
public List<Expense> findAllNativePagination(int first, int max) {
     return em.createNativeQuery("SELECT * FROM expenses ORDER BY concept LIMIT :first, :max", Expense.class)
                 .setParameter("first", first)
                 .setParameter("max", max)
                 .getResultList();
}

De forma más cómoda, invocando a los métodos de Query que ya estamos acostumbrados a usar con JPQL y Criteria API.

@Override
public List<Expense> findAllStandardPagination(int first, int max) {      
        return em.createNativeQuery("SELECT * FROM expenses ORDER BY concept", Expense.class)
                 .setFirstResult(first)
                 .setMaxResults(max)
                 .getResultList();
}

En todos los casos, tengamos la precaución de aplicar una ordenación con el fin de asegurar la coherencia de las páginas.

Operaciones de escritura

Además de las SELECT, las sentencias INSERT, UPDATE y DELETE pueden ejecutarse con el gestor de entidades. Se siguen creando con el método createNativeQuery como objetos de tipo Query, pero se ejecutan con executeUpdate, al igual que las consultas de modificación de JPQL \ HQL. También aplica la misma limitación: los cambios se realizan directamente en las tablas y no afectan a las entidades que ya existieran en el contexto de persistencia, ni se ejecutan sus posibles eventos (@PrePersist, @PreUpdate, etcétera).

    @Override
    public int updateUsedPrefixBulk() {
        return em.createNativeQuery("UPDATE coupons SET name = CONCAT(:prefix, name) " +
                "WHERE expense_id IS NOT NULL " +
                "AND SUBSTRING(name, 1, :length) NOT LIKE :prefix")
                .setParameter("length", Coupon.USED_PREFIX.length())
                .setParameter("prefix", Coupon.USED_PREFIX)
                .executeUpdate();
    }

Consultas programáticas

JPA no define ninguna API para la construcción de consultas SQL como sí hace para JPQL con Criteria API. Por fortuna, existen varias librerías que ofrecen esta funcionalidad. La más afamada en estos momentos es jOOQ. Con ella generaremos con cierta facilidad cualquier consulta usando un tipado fuerte basado en un modelo de clases que un plugin de Maven es capaz de generar a partir de las tablas. Además, las ejecutaremos con el gestor de entidades.

org.jooq.Query queryDSL = DSL.using(configuration)
    .select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, BOOK.TITLE)
    .from(AUTHOR)
    .join(BOOK).on(AUTHOR.ID.eq(BOOK.AUTHOR_ID))
    .orderBy(BOOK.ID));
Query query = em.createNativeQuery(queryDSL.getSQL());

La API es muy intuitiva y más amigable que la de Criteria y sin las limitaciones de JPQL. Merece la pena echarle un vistazo y evaluar su empleo si necesitamos -o preferimos- trabajar con SQL en JPA de forma programática, sobre todo si las consultas son dinámicas. Eso sí, la compatibilidad con las distintas bases de datos está limitada si usamos la versión de código abierto (gratuita): solo admite las últimas versiones de las bases de datos libres más populares.

Código de ejemplo

El código de ejemplo se encuentra en GitHub (todos los proyectos son independientes pero están en un único repositorio). Para más información sobre cómo utilizar GitHub, consultar este artículo.

>>>> ÍNDICE <<<<

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.