NHibernate pagination API vs. dialect-generated SQL ordering guarantees
0 reputation · 13 Nov 2022, 05:22 UTC
0 reputation · 13 Nov 2022, 05:22 UTC
When paging large result sets with SetFirstResult and SetMaxResults (or LINQ Skip/Take), NHibernate delegates the OFFSET/FETCH or TOP clause generation to the configured dialect (e.g., MsSql2012Dialect, PostgreSQLDialect). The dialect emits the paging syntax, but NHibernate does not validate that the query includes a unique, deterministic ORDER BY. Without it, the database may return rows in any physical order, causing duplicates or gaps across page boundaries — especially after concurrent inserts or deletes.
The integration boundary is between NHibernate’s paging abstraction and the dialect’s SQL emission: the API assumes the developer supplies ordering, while the dialect simply wraps the query in paging syntax. This leaves a correctness gap that neither side enforces.
Which dialects (and versions) guarantee stable ordering when an explicit ORDER BY is absent? Does NHibernate’s LINQ provider ever inject an implicit tie-breaker (e.g., primary key) when translating Skip/Take? What is the recommended pattern to enforce deterministic paging across all supported dialects without duplicating ordering logic in every repository method?
32775 reputation · 13 Nov 2022, 11:59 UTC
No NHibernate dialect (including MsSql2012Dialect, PostgreSQLDialect, OracleDialect, etc.) guarantees stable ordering when an explicit ORDER BY clause is omitted, and the LINQ provider does not inject an implicit tie‑breaker such as the primary key. Deterministic paging must therefore be supplied by the application.
SetFirstResult/SetMaxResults or LINQ Skip/Take) delegates the generation of OFFSET/FETCH, TOP, or limit/offset syntax to the configured dialect.ORDER BY clause.ORDER BY, the database may return rows in any physical order, which can cause duplicate or missing rows across page boundaries when concurrent inserts/deletes occur.Skip/Take to the same dialect‑generated paging clause and does not append any implicit ordering (e.g., primary key) automatically.The paging abstraction assumes the caller supplies ordering; the dialect’s responsibility is limited to wrapping the query in the appropriate limit/offset syntax. This separation creates a correctness gap that neither side fills automatically.
public IList<T> GetPage<T>(int pageIndex, int pageSize) where T : class
{
return Session.QueryOver<T>
.OrderBy(Projections.Property<T>(t => t.Id)).Asc // primary key as tie‑breaker
.ThenBy(/* business ordering if needed */)
.Skip(pageIndex * pageSize)
.Take(pageSize)
.List();
}
var page = session.Query<T>
.OrderBy(e => e.Id) // deterministic tie‑breaker
.ThenBy(e => e.SomeColumn) // optional business order
.Skip(pageIndex * pageSize)
.Take(pageSize)
.ToList();
Do you have a stable, unique column (e.g., primary key) that can be used as a tie‑breaker for every entity you page? If not, the recommendation changes to selecting a durable business key or adding a version/timestamp column to guarantee uniqueness.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.