Native SQL with Hibernate and Jakarta Persistence

JPQL and Hibernate Query Language are usually preferable when a query can be expressed in terms of entities. However, native SQL remains useful for database-specific features, complex reporting queries, optimized SQL, common table expressions and existing SQL that an application must reuse.

Jakarta Persistence exposes native SQL through EntityManager.createNativeQuery(...). Hibernate executes that SQL through the JDBC connection associated with the persistence context.

Return scalar values with a native query

If a native query selects individual columns rather than a mapped entity, the result can be consumed as rows of values.

var sql = """
    select handle, email_address
    from player
    order by id
    """;

@SuppressWarnings("unchecked")
var players = (java.util.List<Object[]>)
    entityManager.createNativeQuery(sql).getResultList();

for (var player : players) {
    IO.println(player[0] + " " + player[1]);
}

Notice that the query names its columns instead of using SELECT *. Explicit columns make native queries easier to understand and less sensitive to unrelated schema changes.

Map native SQL directly to a JPA entity

When the SQL returns the columns required by an entity mapping, pass the entity class to createNativeQuery. Hibernate then materializes managed entity instances instead of returning Object[] rows.

var sql = """
    select id, handle, email_address
    from player
    order by id
    """;

var query = entityManager.createNativeQuery(sql, Player.class);

@SuppressWarnings("unchecked")
var players = (java.util.List<Player>) query.getResultList();

players.forEach(player ->
    IO.println(player.loginName() + " " + player.emailAddress())
);

Always bind native-query parameters

Do not construct SQL by concatenating user input. Native queries support parameters, which keep values separate from the SQL statement and avoid SQL injection vulnerabilities.

var sql = """
    select id, handle, email_address
    from player
    where handle = :handle
    """;

var player = entityManager
    .createNativeQuery(sql, Player.class)
    .setParameter("handle", "rocky")
    .getSingleResult();

IO.println(player);

NamedNativeQuery example

For a native query that is reused throughout an application, a named native query gives the SQL a stable application-level name.

import jakarta.persistence.*;

@Entity
@Table(name = "player")
@NamedNativeQuery(
    name = "Player.findAllNative",
    query = """
        select id, handle, email_address
        from player
        order by id
        """,
    resultClass = Player.class
)
public class Player {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "handle", nullable = false, unique = true)
    private String loginName;

    @Column(name = "email_address", nullable = false)
    private String emailAddress;

    protected Player() {
    }

    public Player(String loginName, String emailAddress) {
        this.loginName = loginName;
        this.emailAddress = emailAddress;
    }

    public String loginName() {
        return loginName;
    }

    public String emailAddress() {
        return emailAddress;
    }

    @Override
    public String toString() {
        return loginName + " <" + emailAddress + ">";
    }
}

The named query can then be executed through the EntityManager:

var query = entityManager.createNamedQuery(
    "Player.findAllNative",
    Player.class
);

var players = query.getResultList();
players.forEach(IO::println);

Hibernate NativeQuery

Jakarta Persistence is sufficient for many native SQL use cases. When an application needs Hibernate-specific capabilities, the underlying Hibernate Session can be obtained and Hibernate’s NativeQuery API used directly.

import org.hibernate.Session;

var session = entityManager.unwrap(Session.class);

var players = session
    .createNativeQuery("""
        select id, handle, email_address
        from player
        where email_address like :domain
        """, Player.class)
    .setParameter("domain", "%@example.com")
    .getResultList();

players.forEach(IO::println);

When should you use native SQL?

Native SQL is appropriate when it provides a capability or performance characteristic that JPQL, Criteria queries or normal entity navigation cannot express cleanly. The tradeoff is portability: database-specific SQL can couple the application to a particular database dialect.

Keep native queries focused, bind all external values as parameters, select only the columns you need, and map results to entities or explicit result mappings whenever that makes the code clearer.


Cameron McKenzie Cameron McKenzie is an AWS Certified AI Practitioner, Machine Learning Engineer, Solutions Architect and author of many popular books in the software development and Cloud Computing space. His growing YouTube channel training devs in Java, Spring, AI and ML has well over 30,000 subscribers.