{"record":{"id":"0649a10c011c0d2a","repo":"hibernate/hibernate-orm","slug":"session-method-called-from-entity-lifecycle-callba","errorCode":null,"errorMessage":"Session method called from entity lifecycle callback or Interceptor method","messagePattern":"Session method called from entity lifecycle callback or Interceptor method","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java","lineNumber":1135,"sourceCode":"\t\tif ( isClosed() ) {\n\t\t\tif ( markForRollbackIfClosed && transactionCoordinator.isTransactionActive() ) {\n\t\t\t\tmarkForRollbackOnly();\n\t\t\t}\n\t\t\tthrow new IllegalStateException( \"Session/EntityManager is closed\" );\n\t\t}\n\t}\n\n\tprivate void startSessionUseProhibited() {\n\t\tsessionUseProhibitedDepth++;\n\t}\n\n\tprivate void finishSessionUseProhibited() {\n\t\tsessionUseProhibitedDepth--;\n\t}\n\n\tprotected void checkSessionReentrancy() {\n\t\tif ( sessionUseProhibitedDepth > 0 ) {\n\t\t\tthrow new IllegalStateException( \"Session method called from entity lifecycle callback or Interceptor method\" );\n\t\t}\n\t}\n\n\tprotected void checksBeforeQueryCreation() {\n\t\tcheckOpen();\n\t\tcheckTransactionSyncStatus();\n\t}\n\n\t@Override\n\tpublic void prepareForQueryExecution(boolean requiresTxn) {\n\t\tchecksBeforeQueryCreation();\n\t\tif ( requiresTxn && !isTransactionInProgress() ) {\n\t\t\tthrow new TransactionRequiredException( \"No active transaction\" );\n\t\t}\n\t}\n\n\t@Override\n\t@Nullable","sourceCodeStart":1117,"sourceCodeEnd":1153,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java#L1117-L1153","documentation":"While Hibernate invokes entity lifecycle callbacks (@PrePersist, @PostLoad, ...) and Interceptor methods it increments sessionUseProhibitedDepth; checkSessionReentrancy() then rejects any reentrant Session API call. JPA forbids EntityManager operations inside lifecycle listeners, and Hibernate enforces it to protect flush-cycle invariants — a session operation from inside a callback would recurse into the flush/persist machinery.","triggerScenarios":"An entity listener or Interceptor callback calls session/em methods: em.persist/find/query inside @PrePersist/@PostLoad/@PreUpdate, session.flush() or session.get() inside Interceptor.onSave/onFlushDirty/onLoad; also user event listeners calling back into the same session during a flush cycle.","commonSituations":"Audit listeners resolving the current user with a query; denormalization/counter updates cascading from callbacks; code ported from Hibernate 5 native Session patterns where reentrancy went undetected; @PostLoad enriching entities from other tables.","solutions":["Move queries and saves out of the callback into the service/repository method that coordinates the operation.","Inside callbacks, only mutate the entity's own fields or use injected collaborators (CDI/Spring support bean injection into entity listeners) instead of the session.","If cross-entity work is unavoidable, collect it in the listener and perform it after the flush completes (e.g., @PostPersist plus outer service step), or use a dedicated mechanism (Hibernate event system, Envers, domain events)."],"exampleFix":"// before\npublic class AuditListener {\n    @PrePersist\n    void prePersist(Auditable a) {\n        a.setCreatedBy(em.find(User.class, currentUserId())); // IllegalStateException\n    }\n}\n// after\npublic class AuditListener {\n    @PrePersist\n    void prePersist(Auditable a) {\n        a.setCreatedBy(currentUserProvider().username()); // no session use\n    }\n}\n// cross-entity work moves to the service after save()","handlingStrategy":"fallback","validationCode":"// Pattern: never touch the session in a callback; defer work out of it\npublic class DeferredWorkListener {\n    private final Queue<Runnable> pending = new ConcurrentLinkedQueue<>();\n\n    @PreUpdate\n    void onUpdate(Auditable a) {\n        a.setUpdatedAt(Instant.now());            // allowed: own fields only\n        pending.add(() -> counterService.touch(a.getClass())); // no session use here\n    }\n\n    public void runPending() { Runnable r; while ((r = pending.poll()) != null) r.run(); } // call after flush, outside callback\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat lifecycle callbacks and Interceptor methods as session-free zones by design","Resolve lookups (current user, defaults) via injected beans or ThreadLocal context, not session queries","Run cross-entity side effects after the flush in the service layer or via domain events"],"tags":["hibernate","jpa","entity-listener","lifecycle-callback","interceptor"],"backgroundTag":"jpa-lifecycle-callback-restrictions","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}