skip to content

An entity declares @Table(name = "USER_ACCOUNT") and @Column(name = "emailAddress"). Does a configured Hibernate PhysicalNamingStrategy still get a say in those names? What about the ImplicitNamingStrategy?

level: middleimportance: should knowfreq 25%

answer

  1. annotation = logical name
  2. implicit skipped, physical always runs
  3. physical strategy has the last word
  4. backticks mark Identifier quoted
  5. referencedColumnName matches logical name

basics

~20 s

The implicit strategy is skipped - a name was given. The physical strategy still runs: the annotated string is the logical name, and every logical name is converted by the physical pass, so it can be rewritten.

solid answer

~40 s

Explicit annotations decide the logical name only. Because a name was supplied, Hibernate never calls the ImplicitNamingStrategy for that table or column. But the second pass is unconditional: the logical name USER_ACCOUNT is handed to toPhysicalTableName and emailAddress to toPhysicalColumnName, so a snake-casing or prefixing physical strategy will rewrite them into user_account and email_address. That is the usual surprise - people expect an annotation to be the final word. It is not; the physical strategy is the last word. If you genuinely need one identifier preserved verbatim, the options are to quote it (Hibernate keeps the quoted flag on the Identifier, and a well-written strategy leaves quoted identifiers alone) or to special-case it inside your strategy. Relying on the annotation alone is not enough.

code

java · 7 lines
java
@Override
public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment env) {
    if (name == null || name.isQuoted()) {
        return name;
    }
    return Identifier.toIdentifier(toSnakeCase(name.getText()));
}

go deeper

for a junior

Remember the one-liner: annotations skip the implicit strategy but not the physical one.

for a middle

Explain logical versus physical names and demonstrate the opt-out via quoted identifiers.

for a senior

Anticipate the legacy-schema migration trap and note that cross-references resolve on logical names before the physical pass.

for a principal

Decide policy: whether hand-written names are allowed at all, or whether the schema shape is owned entirely by one strategy plus migrations.

## Logical name versus physical name Hibernate distinguishes the logical name of a table or column - the name used inside the mapping model to resolve cross-references - from the physical name, the identifier that goes into SQL. Annotations set the logical name. The ImplicitNamingStrategy is the fallback that produces a logical name when the annotation is absent or has an empty name attribute. So for @Column(name = "emailAddress") the implicit hook is never consulted: there is nothing to invent. But the pipeline does not stop there. Once the mapping model is built, Hibernate walks it and asks the PhysicalNamingStrategy to convert each logical name into an Identifier for SQL. That call happens for annotated and derived names alike. ## Practical consequences The first consequence is uniformity, and it is the reason to prefer the physical hook for conventions: if the DBA mandates a prefix or snake_case, a physical strategy guarantees it even for entities whose author hand-wrote names. The second consequence is astonishment. A team migrating a legacy schema often annotates a handful of oddly named columns explicitly and then installs a snake-casing strategy for the rest, only to find the hand-written names mangled too - LEGACYCol becomes legacy_col and the statement fails at runtime with an unknown-column error, or worse, schema export quietly creates a second table. ## Opting out There are two honest escape hatches. Quoting: write the name in backticks or JPA-style escaped double quotes, for example @Column(name = "`emailAddress`"). Hibernate marks the resulting Identifier as quoted. A responsible physical strategy checks Identifier#isQuoted and returns quoted names unchanged, because a quoted identifier is a deliberate statement about the exact spelling. Not every strategy does this, so verify - and remember quoting also makes the identifier case-sensitive in the database. Special-casing: keep a small allow-list inside your strategy, or key off a marker. This is honest but adds a maintenance point. ## Where references are resolved Cross-references inside the mapping - @JoinColumn(referencedColumnName = ...), @OrderBy, secondary-table primary key joins - are resolved against logical names, before the physical pass. That is why they must match the annotated or implicitly derived spelling, not the final snake_case identifier. Getting this backwards produces confusing boot-time errors about a column not being found in a table that visibly has it. ## How to verify The fastest check is to enable SQL logging or generate the schema script to a file and read the emitted DDL. The mapping model is built at bootstrap, so the answer is deterministic and does not depend on runtime data.

  • How would you keep exactly one legacy column name untouched while snake-casing everything else?
    Quote that one name in the annotation, for example @Column(name = "`emailAddress`"), and make the physical strategy return quoted Identifiers unchanged by checking Identifier#isQuoted. Be aware that a quoted identifier is case-sensitive in most databases, so the column must exist with that exact spelling.
  • Does @JoinColumn(referencedColumnName = ...) refer to the logical or the physical name?
    The logical name. Cross-references are resolved while the mapping model is being built, before the physical pass runs, so the value must match the annotated or implicitly derived spelling. Writing the final snake_case identifier there causes a boot-time error that the column cannot be found.

saying these in an interview costs you the question

  • Saying an explicit @Column name is final and cannot be rewritten
  • Believing the implicit strategy still runs for annotated names
  • Expecting a custom physical strategy to skip quoted names automatically - it only does so if you code it
  • Writing the post-transformation snake_case name in referencedColumnName
  • Assuming you can tell the outcome without checking generated DDL or SQL logs

context