High-Throughput Bulk Operations with NHibernate StatelessSession
Learn how to use NHibernate's IStatelessSession for bulk inserts, updates, and deletes while bypassing the first-level cache and change tracking to reduce memory overhead and improve throughput for ETL and batch jobs.
19 Jul 2025, 05:45 UTC

Desired Outcome
Execute bulk data operations—such as importing 100,000+ rows—with a flat memory profile and maximum throughput. NHibernate's IStatelessSession bypasses the first-level cache, change tracking, cascade saves, and lifecycle events, making it suitable for ETL, data migration, and batch jobs where the overhead of a regular ISession becomes a bottleneck.
Prerequisites
- NHibernate 5.x or later.
- A configured
ISessionFactorywith mapped entities. - Entities that do not rely on interceptors, lazy loading, or cascade saves for the bulk operation.
- Database connection string with
adonet.batch_sizeset (e.g.,adonet.batch_size=100) to enable ADO.NET batching for further gains. - Identity generation strategy compatible with batching (identity, sequence, or hi-lo). Assigned identifiers work without extra round-trips.
Procedure
- Open a stateless session from the session factory:
using var session = sessionFactory.OpenStatelessSession(); - Begin a transaction. Keep the transaction scope as short as practical to limit lock contention.
using var tx = session.BeginTransaction(); - Persist entities in batches. For each entity, call
Insert,Update, orDelete. Because there is no first-level cache, repeated reads of the same entity hit the database every time—avoid read-heavy patterns in the same session.foreach (var entity in batch) { session.Insert(entity); // or Update / Delete if (++count % batchSize == 0) { tx.Commit(); tx = session.BeginTransaction(); // checkpoint } } - Commit the final transaction and close the session (the
usingblocks handle disposal).
Important: Do not mix IStatelessSession with a regular ISession in the same transaction. They operate on different connection and transaction contexts.
Expected Checks
- Memory usage should remain flat during the run (monitor via GC allocations or process memory).
- Row counts verified with a SQL profiler or
SELECT COUNT(*)after commit. - No lazy-load exceptions: accessing uninitialized collections or proxies on entities loaded via
IStatelessSessionthrowsLazyInitializationException. Pre-fetch required data with HQL or SQL before the bulk loop if needed. - Identity generation works as expected—check that identity/sequence values are assigned after each
Insert(requires a database round-trip per row unless the dialect supports batched identity).
Recovery Options
- Rollback on failure: because there is no first-level cache, a rolled-back transaction leaves no dirty state in memory. Simply catch the exception, call
tx.Rollback(), and retry or abort. - Checkpointing for partial commits: commit every N rows (e.g., 1,000) and track processed primary keys externally (in a staging table or log file). On restart, skip already-processed IDs.
- Idempotency via database constraints: use unique constraints or
MERGE/UPSERTlogic so that re-running a batch does not create duplicates.
Important Cautions
- No cascade operations: child collections must be persisted explicitly in the correct order (parents before children).
- No lazy loading: accessing uninitialized collections or proxies throws
LazyInitializationException. - No first-level cache: repeated reads of the same entity hit the database each time.
- Event listeners and interceptors are not invoked:
IPreInsertEventListener,IPostUpdateEventListener, etc., will not fire. Audit trails or business logic that depend on them will not run. - Identity generation nuances: some generators (assigned, hi-lo) work without extra round-trips; identity and sequence rely on a database round-trip per row unless the dialect and batcher support batched identity.
- Long-running transactions hold locks: size batches to balance throughput against lock contention and transaction log growth.
Verification Steps
- Run a benchmark: insert 50,000 entities via
ISessionvs.IStatelessSession; measure GC allocations and elapsed time. - Enable NHibernate SQL logging (log4net, Serilog, or Microsoft.Extensions.Logging) to confirm batched statements appear when
adonet.batch_size > 1. - Attempt to access a collection on a stateless-loaded entity; it should throw
LazyInitializationException. - Verify that database triggers, constraints, and identity columns behave as expected after the bulk run.
- Profile database locks and transaction log growth during batch execution to size batches appropriately.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.