LearnAI ToolsCareerPractice BuildsPlayContact
Lesson 2319 min read

Native SQL Queries in Hibernate

Learn how to run native SQL from Hibernate with createNativeQuery, and when dropping down to raw SQL makes sense for cases HQL cannot express cleanly.

Introduction

HQL covers the vast majority of queries you will write against your entities, but it is not the only option. Sometimes you need a database-specific function, a complex analytical query, or a hand-tuned statement that HQL simply cannot express cleanly. For those cases, Hibernate lets you drop down to native SQL while still getting entity mapping and Session management.

What You Will Learn
  • When native SQL is the right tool over HQL
  • How to run native SQL with createNativeQuery
  • Mapping native query results back to entities
  • Returning scalar values and DTOs from native queries
  • Defining reusable named native queries

Why Native SQL?

HQL is deliberately database-agnostic and object-oriented, which means it does not expose every feature a specific database offers. Common reasons to reach for native SQL include vendor-specific functions (like MySQL's GROUP_CONCAT), complex window functions, stored procedure calls, or a query that is already tuned and tested as raw SQL that you do not want to rewrite.

createNativeQuery Basics

Session.createNativeQuery() accepts a plain SQL string, written exactly as your database expects it — real table and column names, not entity and field names.

import org.hibernate.query.NativeQuery;
import java.util.List;
Session session = factory.openSession();
NativeQuery<Object[]> query = session.createNativeQuery(
"SELECT id, name, salary FROM employees WHERE salary > :minSalary"
);
query.setParameter("minSalary", 50000);
List<Object[]> rows = query.getResultList();
for (Object[] row : rows) {
System.out.println("ID: " + row[0] + ", Name: " + row[1] + ", Salary: " + row[2]);
}
session.close();
Console Output

Click Run to see what this code prints.

Mapping to Entities

If you select every column of a mapped entity, Hibernate can return fully-managed entity instances instead of raw Object[] rows, by telling createNativeQuery which entity class to map onto.

NativeQuery<Employee> query = session.createNativeQuery(
"SELECT * FROM employees WHERE department = :dept",
Employee.class
);
query.setParameter("dept", "Engineering");
List<Employee> employees = query.getResultList();
for (Employee e : employees) {
System.out.println(e.getName() + " - " + e.getSalary());
}
Console Output

Click Run to see what this code prints.

Entities Are Managed

Because the results are mapped to Employee, these objects are attached to the persistence context just like an HQL or Criteria API result — changes to them are tracked and flushed on commit.

Scalar and DTO Results

For queries that do not map cleanly onto one entity — like aggregations across a join — you can select individual columns and map them into a plain constructor, similar to HQL's constructor expressions.

NativeQuery<Object[]> query = session.createNativeQuery(
"SELECT department, COUNT(*), AVG(salary) FROM employees GROUP BY department"
);
List<Object[]> results = query.getResultList();
for (Object[] row : results) {
String department = (String) row[0];
Number count = (Number) row[1];
Number avgSalary = (Number) row[2];
System.out.println(department + ": " + count + " employees, avg salary " + avgSalary);
}
Console Output

Click Run to see what this code prints.

Named Native Queries

Just like named HQL queries, a native query can be declared once on an entity with @NamedNativeQuery and reused by name, keeping the raw SQL out of your business logic.

import javax.persistence.*;
@Entity
@Table(name = "employees")
@NamedNativeQuery(
name = "Employee.findHighEarners",
query = "SELECT * FROM employees WHERE salary > :minSalary",
resultClass = Employee.class
)
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double salary;
// Getters and setters
}
Using the named native query
List<Employee> highEarners = session
.createNamedQuery("Employee.findHighEarners", Employee.class)
.setParameter("minSalary", 60000)
.getResultList();

HQL vs Native SQL

AspectHQLNative SQL
PortabilityDatabase-agnosticTied to a specific database dialect
Syntax targetsEntity names and fieldsActual table and column names
Entity mappingAutomaticAutomatic only if you specify the entity class
Vendor-specific featuresNot accessibleFully accessible
Best forEveryday CRUD and object-oriented queriesComplex reports, vendor functions, tuned legacy SQL

Common Mistakes

Avoid These Mistakes
  • Concatenating user input directly into the native SQL string instead of using named/positional parameters, opening the door to SQL injection.
  • Forgetting to select every column an entity needs when mapping results to that entity class, causing mapping errors.
  • Reaching for native SQL out of habit for queries HQL could express just as well, losing database portability for no reason.
  • Assuming native query results are always managed entities — they are Object[] rows unless you specify a result class.
  • Hardcoding table/column names that later drift from the entity mapping, silently breaking the query.

Best Practices

  • Reach for HQL first; use native SQL only when HQL genuinely cannot express what you need.
  • Always use setParameter() for dynamic values in native queries — never string concatenation.
  • Use @NamedNativeQuery to keep native SQL declared alongside the entity rather than scattered through business logic.
  • Document why a native query was necessary, so future maintainers know it was a deliberate choice.
  • Keep native SQL as portable as reasonably possible if the application may need to support more than one database.

Frequently Asked Questions

Only if you use setParameter() with named or positional placeholders. Concatenating raw strings into the query text defeats that protection entirely.

Yes, using a SqlResultSetMapping to describe how columns from a joined query map onto more than one entity or DTO.

Yes, setFirstResult() and setMaxResults() work on native queries the same way they do on HQL queries.

Not inherently — both ultimately execute as SQL against the database. Native SQL just gives you direct control over exactly what SQL runs.

Key Takeaways

  • createNativeQuery() runs raw SQL while still going through the Session and persistence context.
  • Passing an entity class maps results to managed entities; otherwise you get Object[] rows.
  • @NamedNativeQuery keeps reusable native SQL declared on the entity itself.
  • Always parameterize dynamic values — never concatenate user input into native SQL.
  • Prefer HQL for everyday queries; reserve native SQL for cases it genuinely cannot handle.

Summary

Native SQL is Hibernate's escape hatch for the queries HQL was never meant to express — vendor-specific functions, tuned reports, and complex aggregations — while still benefiting from parameter binding and, where possible, entity mapping. Use it deliberately, and keep HQL as your default.

Next Lesson →

Hibernate with Spring