knowledge: Hibernate's not-null check — attributing its exception, and audit columns as update evidence (3 verified insights) - #91
Open
dch0202-rsquare wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Knowledge flush — 3 insight(s)
Queue drained:
~/.dev-loop/queue/cd5970a5-…jsonl(2 rows),~/.dev-loop/queue/d5a117bb-…jsonl(1 row).All three came out of one investigation (Hibernate's application-level not-null check
and what it does to audit columns), so they route to two pages, not three.
Verified best-practice
I1 — attribute
PropertyValueException: not-null property references a null or transient valueby path shape, not by the word "transient"; and exclude@PreUpdatefrom cause and fix on the UPDATE pathClaim. Both throw sites share one hardcoded message literal, so the wording
carries no discriminating information; a dotted path means a composite
(
@Embeddable) sub-attribute, a plain name means a top-level attribute. On UPDATE thecheck runs before
EntityUpdateActionexists, so no@PreUpdate/@PostUpdatelistener has run — a listener is neither a candidate cause nor a viable fix.
Sources checked and how.
Nullability.java(5.6) fetched raw and read on disk: line 108-112 throws withpersister.getPropertyNames()[i]; line 119-123 throws withbuildPropertyPath( persister.getPropertyNames()[i], breakProperties ). Same literalboth times.
checkSubElementsNullability(line 143-167) enterscheckComponentNullabilityonly forisComponentType()(CompositeType) or acollection whose element type is composite — so dots have exactly two producers.
gh api search/code 'q="not-null property references a null or transient value" repo:hibernate/hibernate-orm'→ 5 hits, of which1 is production code (
engine/internal/Nullability.java); the other 4 are tests.So no other class emits this message.
DefaultFlushEntityEventListener.scheduleUpdate— source comment "checknullability but do not doAfterTransactionCompletion command execute" then
new Nullability( session ).checkNullability( values, persister, true )(5.6 line320) immediately followed by
new EntityUpdateAction(...)(line 325). Re-checked onbranch 6.6 (253 → 258) and 7.0 (253 → 258) — same order in all three.
EntityUpdateAction.execute()(line 170) callspreUpdate()(line 176), i.e. afterthe check.
AbstractEntityInsertAction.nullifyTransientReferencesIfNotAlready()(6.6, lines 122-126) runs
nullifyTransientReferences( getState() )and thennew Nullability( getSession() ).checkNullability( getState(), getPersister(), false )— which is where "or transient" in the message comes from.
gh api search/code 'q="object references an unsaved transient instance"'→ 1production hit (
metamodel/mapping/EntityIdentifierMapping.java), a differentmessage.
https://www.baeldung.com/hibernate-not-null-error attributes the same message to two
causes ("null value for a column marked nullable = false" / "an association
referencing an unsaved instance") and never mentions the path shape.
Confidence: verified (primary source read on three branches + reproducible
gh apicode searches).I2 — Hibernate's core not-null check is silently off when Bean Validation is active and
hibernate.check_nullabilityis unset; measure it instead of inferring itSources checked.
—
CHECK_NULLABILITY: "Enable nullability checking, raises an exception if anattribute marked as not null is null at runtime"; "Defaults to disabled if Bean
Validation is present in the classpath and annotations are used, or enabled
otherwise."
TypeSafeActivator.applyCallbackListeners, read on disk for 5.6 (lines 114-115)and 6.6 (lines 116-117): guarded by
modes.contains( CALLBACK ) || modes.contains( AUTO ), then, under the comment "de-activate not-null tracking atthe core level when Bean Validation is present unless the user explicitly asks for
it",
if ( cfgService.getSettings().get( CHECK_NULLABILITY ) == null ) … setCheckNullability( false ). So the toggle is driven by a classpath/dependencychange, exactly as the candidate claimed.
Nullabilityline 69-73 corroborates in-source: "Typically when Bean Validation ison, we don't want to validate null values at the Hibernate Core level. Hence the
checkNullability setting."
SessionFactoryImplementor.getSessionFactoryOptions()(javadoc: "Get the optionsused to build this factory") and
SessionFactoryOptions.isCheckNullability()(
boolean isCheckNullability()).Nullabilitylines 92-104(
getPropertyInsertability/getPropertyUpdateability,UNFETCHED_PROPERTY,GenerationTiming.NEVER); that@UpdateTimestampis in-memory generated wasconfirmed via
UpdateTimestamp.java(@ValueGenerationType(generatedBy = UpdateTimestampGeneration.class)) →UpdateTimestampGeneration.getGenerationTiming() == GenerationTiming.ALWAYSwith a non-nullgetValueGenerator().https://thorben-janssen.com/hibernate-tips-whats-the-difference-between-column-nullable-false-and-notnull/
—
@Column(nullable = false)"adds a not null constraint to the database column, ifHibernate creates the database table definition";
@NotNullis what Bean Validationchecks on pre-update/pre-persist. This is also why that article describes Hibernate
"just executing the SQL UPDATE" — it describes the Bean-Validation-present default,
i.e. the check switched off.
Confidence: verified.
I3 — an audit column is evidence only about the writer that sets it, so all-NULL
update_dtcannot separate "never updated" from "every update failed"Sources checked.
AuditingEntityListenerread on disk:touchForCreateis@PrePersist(line 84-85),touchForUpdateis@PreUpdate(line 104-105) — so@LastModifiedDateis written by a JPA lifecycle callback and inherits the orderingestablished in I1.
(https://docs.hibernate.org/orm/6.6/querylanguage/html_single/Hibernate_Query_Language.html):
"The effect of an
updateordeletestatement is not reflected in the persistencecontext, nor in the state of entity objects held in memory at the time the statement
is executed";
@Versionis untouched unless the statement isversioned. I lookedfor a primary statement that callbacks specifically are not invoked and did not
find one in the 6.6 user guide, the 6.6 query-language guide, or the Jakarta
Persistence 3.1 spec page (§4.10's body was not in the served content). The page
therefore states the doc-backed fact (no persistence-context effect → no flush action
→ no callback) and does not assert a spec quotation it cannot cite.
trigger that is marked
FOR EACH ROWis called once for every row that the operationmodifies." The doc states per-row firing; client-independence is the structural
consequence of the trigger living on the relation, and the page words it that way
rather than quoting the doc for it.
manage.building_tenant_floor_info— 574 rows with a NULL business code, all with
update_dtNULL, whose UPDATEs weredying in
NullabilitybeforeEntityUpdateActionwas created.Confidence: verified for the mechanism (callback ordering,
@PreUpdatewriter,bulk-DML persistence-context semantics); the production observation that motivated it
is labelled as a field observation in the page's Sources.
Existing-layer check
Routed via
INDEX.md→backend(application-code concern, JVM subtree) anddatabases(reading live rows to derive a claim), then read each domain index andevery page whose "load when" overlapped.
Pages read: backend-java-jpa-entity-mapping, backend-java-jpa-persistence-context, backend-java-kotlin-frameworks-and-jpa, databases-schema-design-nullability-and-defaults, databases-data-survey-surveying-live-data-for-a-rule, databases-schema-design-soft-delete
Also read (non-page routing/schema files):
INDEX.md,wiki/backend/index.md,wiki/backend/java/index.md,wiki/databases/index.md,wiki/debugging/index.md,AGENTS.md,templates/page.md.Overlaps and what I did.
backend-java-jpa-persistence-contextscheduleUpdateor about callback orderingbackend-java-kotlin-frameworks-and-jpa@field:NotNulltargets@field:target to it by id rather than restating itdatabases-schema-design-nullability-and-defaultsdatabases-data-survey-surveying-live-data-for-a-ruledebugging/signals/reading-error-messages(index line only)Conflicts flagged. None. No existing directive says the opposite of anything
ingested here.
Related links added. New JPA page →
backend-java-jpa-persistence-context,backend-java-kotlin-frameworks-and-jpa,databases-schema-design-nullability-and-defaults,databases-data-survey-audit-columns-as-update-evidence. New data-survey page →databases-data-survey-surveying-live-data-for-a-rule,databases-schema-design-nullability-and-defaults,backend-java-jpa-not-null-check-and-lifecycle-callbacks. Inline id references alsomade to
databases-schema-design-soft-delete. Back-links intosurveying-live-data-for-a-rule.mdwere deliberately not added: PR #78 edits thatfile's
related:line, and a one-way link satisfies invariant 4 without contendingfor the same line.
Open-PR check
Listed with
gh pr list --repo choiyounggi/dev-loop --state open --search "head:knowledge/"→ 23 open heads: #86, #80, #79, #78, #76, #74, #73, #72, #69, #68, #66, #64, #62, #61,
#58, #57, #56, #55, #52, #51, #50, #49, #47.
Per-head
wiki/**file lists pulled withgh pr view <n> --json files(the heads arecross-repo forks —
git fetch origin <head>fails with "couldn't find remote ref", sogh pr diffwas used for content). Heads touching the two categories I ingest into:wiki/backend/java/jpa/raw-jdbc-inside-a-jpa-transaction.md,wiki/backend/java/index.mdwiki/databases/data-survey/catalog-statistics-as-current-state.md,surveying-live-data-for-a-rule.md,wiki/databases/index.mdrelpages/reltuples/pg_stats) being stale relative to the heap — a staleness-of-statistics case, not a writer-coverage case. Family resemblance onlywiki/databases/index.md(+ query-optimization pages)Verdicts: I1 → new, I2 → new (both into one page, same mechanism/one case per
AGENTS.md rule 1), I3 → new. Nothing folded, nothing dropped.
Contention to expect at merge (not duplication): three additive index rows —
wiki/backend/java/index.md(also touched by #73) andwiki/databases/index.md(alsotouched by #74 and #78). Each is a one-row table append; resolve by keeping both rows.
wiki/backend/index.mdwas intentionally not modified (it routes to the subtree indexonly), which keeps this PR off the file that #76/#72/#68/#58/#55/#51 all edit.
I3 does not link
databases-data-survey-catalog-statistics-as-current-state(#78's new page) even though the pairing would be apt: that id does not exist on
mainyet, and invariant 4 would break if #78 is rejected. Worth adding as a
related:onwhichever of the two merges second.
Routing decision
backend/java/jpa/not-null-check-and-lifecycle-callbacks.md(idbackend-java-jpa-not-null-check-and-lifecycle-callbacks), new page in the existingjpacategorybackend/java/jpaalready exists and holds entity-mapping/persistence-contextdatabases/data-survey/audit-columns-as-update-evidence.md(iddatabases-data-survey-audit-columns-as-update-evidence), new page in the existingdata-surveycategorydata-surveyexists (created forsurveying-live-data-for-a-rule) and its remit is exactly "reading live data as evidence"Why I1 and I2 share one page. They are the same mechanism seen from two sides: the
exception thrown by
Nullability(I1) and whetherNullabilityruns at all (I2). Bothare answered from the same source lines, and a reader arriving with either question
needs the other half — splitting them would produce two pages whose "load when" lines
each pull in the other.
Why I3 is not on the JPA page. Its situation is "I am looking at production rows
and about to state a behavioural conclusion" — a survey, reached from the
databasesdomain, and true for any callback-written audit column (not only Hibernate's). The JPA
page owns the ORM mechanism; the data-survey page owns the evidential rule and links to
it.
Domain hints honoured. Queue hints were
backend(I1, I2) anddatabases(I3);both matched the routing that
INDEX.mdproduced independently.Sizes: 91 and 73 body lines (limit 120). Both pages carry
confidence: verifiedandlast_verified: 2026-08-13.Citation ledger (12 distinct URLs across the two pages, so the reviewer can spot a
gap rather than trust a blanket claim):
engine/internal/Nullability.java(5.6)event/internal/DefaultFlushEntityEventListener.java(6.6)action/internal/AbstractEntityInsertAction.java(6.6)boot/beanvalidation/TypeSafeActivator.java(6.6)ValidationSettingsjavadoc (6.6)SessionFactoryOptionsjavadoc (6.6)AuditingEntityListener.javathorben-janssen.com@column vs @NotNullbaeldung.com/hibernate-not-null-errorCREATE TRIGGERcurl -o /dev/null -w '%{http_code}' -L→ 200 (9 of them; the twoTwo things this flush did not establish, stated so a later revise can close them:
a primary-source quotation that bulk JPQL skips lifecycle callbacks specifically (the
Jakarta Persistence §4.10 body was not in the served spec page), and the Hibernate 7.x
equivalent of
TypeSafeActivator(checked on 5.6 and 6.6 only; the flush-order checkdid cover 7.0).
Decision Log
★ Insightcandidates from one investigation into thewiki as a single reviewed PR: 2 new pages, no merges into existing pages, no
candidate dropped.
backend/java/jpa/not-null-check-and-lifecycle-callbacks)because both answer from the same
Nullabilitysource lines and either questionneeds the other's answer. Rejected splitting them into an "error attribution"
page and a "configuration" page — their "load when" lines would each pull in the
other, which AGENTS.md rule 1 exists to prevent.
databases/data-surveyfor I3. Rejected merging it intosurveying-live-data-for-a-rule(its "When this applies" is scoped to derivingmapping/normalization/enum rules — an audit-column directive would contradict its own
trigger) and rejected putting it on the JPA page (the evidential rule holds for
any callback-written audit column, not only Hibernate's).
surveying-live-data-for-a-rule.md: open PRknowledge: PostgreSQL catalog statistics + a branch masked by another writer (2 ingested, 1 folded into #73, 1 dropped as dup of #52) #78 edits that file's
related:line, and invariant 4 is satisfied by the one-waylink from the new page.
databases-data-survey-catalog-statistics-as-current-state(apt, but it exists only on knowledge: PostgreSQL catalog statistics + a branch masked by another writer (2 ingested, 1 folded into #73, 1 dropped as dup of #52) #78's head — a broken id if knowledge: PostgreSQL catalog statistics + a branch masked by another writer (2 ingested, 1 folded into #73, 1 dropped as dup of #52) #78 is rejected).
wiki/backend/index.md(it routes to the subtreeindex only), which keeps this PR off the file six open PRs already touch.
JPQL "skips callbacks". The page states the doc-backed persistence-context fact and
the mechanism it implies; the callbacks-specifically quotation is listed above as not
established from a primary source.
delegation was outside this invocation's scope. What was done, first-hand this
session: Hibernate sources read on disk for 5.6/6.6/7.0 (flush-order verified on all
three),
gh api search/codeused to prove the message literal has exactly oneproduction occurrence, every frontmatter URL opened (ledger above, 9 additionally
re-checked for HTTP 200), all 8
related:/inline ids resolved againstwiki/byscript, both pages measured under the 120-line limit, and the flush gate's own
Pages read:resolution reproduced locally. Treat the two "not established" itemsabove as the known gaps.