skip to content

When you run a JPQL/HQL query in Hibernate, what is the difference between calling getResultList() and consuming the results through getResultStream() or Hibernate's scroll()?

level: juniorimportance: should knowfreq 40%

answer

  1. getResultList = materialize everything
  2. scroll/getResultStream = cursor, one row at a time
  3. Session + transaction must stay open
  4. try-with-resources or leak the cursor
  5. Entities still managed → read-only + clear()

basics

~20 s

getResultList() reads every row and builds every entity before returning. getResultStream() and scroll() keep a database cursor open and hand you rows as you pull them, so peak memory depends on how much you keep, not on the result size. Both must be closed.

solid answer

~50 s

`getResultList()` is eager: Hibernate drains the whole `ResultSet`, instantiates every entity, and returns a fully materialized `List`. Peak memory is proportional to the number of rows — fine for a page of results, fatal for millions. `Session.scroll()` returns a `ScrollableResults` backed by an open JDBC cursor; you call `next()` and pull one row at a time. JPA's `getResultStream()` is implemented on top of scrolling in Hibernate, so it behaves the same way with a `Stream` API on top. Three consequences people miss: - The cursor lives on the connection, so the **session and transaction must stay open for the whole consumption**, and the stream or `ScrollableResults` must be closed (try-with-resources) or you leak a cursor and a statement. - The entities you pull are still **managed**, so they pile up in the persistence context; you still need read-only plus a periodic `clear()`. - Streaming plus a **join-fetched collection** is unsafe, because one logical entity spans several rows.

code

java · 11 lines
java
session.beginTransaction();
try (Stream<Order> stream = session
        .createQuery("from Order o where o.status = :s", Order.class)
        .setParameter("s", Status.EXPORTED)
        .setHint("org.hibernate.readOnly", true)
        .setHint("org.hibernate.fetchSize", 500)
        .getResultStream()) {

    stream.forEach(writer::write);
}
session.getTransaction().commit();

go deeper

for a junior

State the core contrast: getResultList() builds every entity up front, streaming pulls rows one at a time from an open cursor and must be closed.

for a middle

Add the obligations — session and transaction open for the whole consumption, try-with-resources, entities still managed — and mention that the driver must be told to stream.

for a senior

Treat it as a recipe: read-only, explicit fetch size, periodic clear, no collection fetch joins, resource scoping — and be able to explain why a naive switch to streaming often shows no memory improvement.

for a principal

Ask whether entities are needed at all; a narrow projection or a set-based database-side operation usually beats streaming entities, and streaming's long-lived transaction has operational costs worth weighing.

## Two ways to get rows out of a query Every JPA/Hibernate query eventually runs a SQL statement and reads a JDBC `ResultSet`. What differs is *when* Hibernate reads it. **`getResultList()` (and `getSingleResult()`, `list()`, `uniqueResult()`) is eager.** Hibernate loops the entire `ResultSet`, hydrates an entity (or a projection tuple) for every row, registers each managed entity in the persistence context, resolves any eager associations, and only then returns a `List`. When the method returns, the cursor and statement are already closed. Everything is in memory: the row data, the entities, and the loaded-state snapshots used for dirty checking. Memory scales linearly with the number of rows, and the caller waits for the last row before seeing the first. **`scroll()` is lazy.** `session.createQuery(...).scroll(ScrollMode.FORWARD_ONLY)` returns a `ScrollableResults`, a cursor handle. Nothing is materialized until you call `next()`; each call advances the cursor and hydrates just that row. Peak memory is driven by what you retain, not by the size of the result — provided the driver is genuinely streaming (which depends on the JDBC fetch size and driver-specific settings) and provided you are not silently accumulating entities in the persistence context. **`getResultStream()`** is the JPA 2.2 API for the same idea. In Hibernate it is implemented over scrolling, so it gives you a `java.util.Stream` whose spliterator advances the cursor. It is the more idiomatic form today; `scroll()` is the native API with extra abilities (positioning, `ScrollMode`). ## The obligations streaming puts on you ### 1. Keep the session and transaction open A cursor is server-side (or driver-side) state attached to the JDBC connection, inside the transaction that opened it. If you return the `Stream` from a method that closes the session, or commit before consuming, you get an exception or a fully buffered result — the resource is gone. Streaming is therefore an *inside-the-unit-of-work* technique: open session, begin transaction, consume fully, close. ### 2. Close the resource `ScrollableResults` and the `Stream` returned by `getResultStream()` are `AutoCloseable`. Closing releases the cursor and the JDBC statement. Not closing leaks statements and, on some drivers, blocks further use of the connection until the result is drained. Always `try (Stream<Order> s = query.getResultStream()) { ... }`. ### 3. Rows still become managed entities Streaming does not bypass the persistence context. Each entity you pull is registered, with a snapshot, and stays strongly referenced. A stream over ten million rows will still exhaust the heap unless you make the load read-only and call `session.clear()` (or `evict()`) every few hundred/thousand rows. This is the single most common "I streamed and still ran out of memory" cause. ### 4. The driver must cooperate Streaming is only real if the JDBC driver hands rows over incrementally. Several popular drivers buffer the entire result by default and require an explicit fetch size (and, on some, extra connection settings) before they will use a server-side cursor. Without that, `getResultStream()` is a lazy façade over a result the driver has already fully materialized in client memory. ### 5. Collection join fetch does not scroll safely If your query `join fetch`es a `@OneToMany`, one logical parent spans as many rows as it has children. To return a fully built parent, Hibernate would have to read ahead until the parent id changes, which breaks the one-row-at-a-time contract. Hibernate documents scrolling with collection fetches as unsafe: you can see partially populated collections or duplicated parents. Either drop the fetch join and stream parents only, or don't stream that query. ## Choosing between them Use `getResultList()` for anything bounded — a page, a lookup, a small report. It is simpler, the resource lifecycle is trivial, and the eager materialization means the caller can close the session immediately. Use `getResultStream()`/`scroll()` when the row count is unbounded or large enough that materializing it is a heap risk, or when you want to start emitting output before the query finishes (piping rows to a file, a message queue, an HTTP response). Pair it with: a read-only load, an explicit JDBC fetch size, periodic `clear()`, no collection fetch joins, and a `try`-with-resources block. A third option worth naming: don't fetch entities at all. Selecting a narrow projection into a DTO or an `Object[]` avoids the persistence context entirely and is usually both faster and simpler than streaming entities — streaming is what you reach for when you genuinely need the mapped objects.

  • Can you return the Stream from a method and let the caller consume it later?
    Only if the session and transaction are still open when the caller consumes it, which usually means the caller controls the unit of work. Returning a stream from a method that closes the session leaves you with a dead cursor. The safe shape is to pass a consumer *into* the streaming method so the session, transaction, and stream are all opened and closed in one scope.
  • You switched a job from getResultList() to getResultStream() and memory did not improve. What would you check first?
    Two things: whether the JDBC driver is actually streaming — many buffer the whole result unless a fetch size (and sometimes extra connection settings) is set — and whether the entities are accumulating in the persistence context. Streaming does not detach anything, so without a read-only load and a periodic `clear()` you retain every row anyway.

getResultList() is having the whole library delivered to your desk before you read the first page; scroll() is reading it one page at a time at the counter — much lighter, but you have to stay in the building and hand the book back when you leave.

saying these in an interview costs you the question

  • Believing getResultStream() is memory-safe by itself, regardless of driver settings and the persistence context.
  • Returning the Stream from a repository-style method and consuming it after the session is closed.
  • Forgetting to close the Stream or ScrollableResults, leaking cursors and statements.
  • Scrolling a query that join-fetches a collection and expecting fully populated parents.
  • Thinking scroll() and getResultStream() are different mechanisms rather than the same cursor with different APIs.

context