You inherit a codebase where three @Entity classes are annotated normally, a fourth is mapped through a stray orm.xml nobody remembers writing, and somewhere in src/main/resources there’s a .hbm.xml file dated 2014 that everyone is afraid to touch. Is that hbm.xml file a ticking time bomb, or does it just work? Does the orm.xml override the annotations, or the other way around? Most “annotations vs. XML” guides answer this in the abstract. At @ankurm, this one boots Hibernate 7.4.5.Final three different ways and reports what actually happens.
The short version: there are really three XML dialects in play, not the two most articles compare, the legacy one is not dead, and the interesting story isn’t “annotations vs. XML” at all — it’s what each format can express and what happens when two of them describe the same attribute differently.
Verified against: Hibernate ORM 7.4.5.Final, Spring Boot 4.1.1, Jakarta Persistence 3.2.0, Java 25, H2 2.4.240. Every boot log and generated SQL statement quoted below is copied verbatim from the companion repo’s test output — nothing here was retyped or paraphrased from documentation.
| Part | Best suited for |
|---|---|
| Part 1 — Beginner | picking a mapping format for a new entity, and seeing the smallest working example of each |
| Part 2 — Intermediate | mixing annotations and XML on the same entity, and what happens when they disagree |
| Part 3 — Advanced | the metadata-complete kill switch, and the Hibernate-native XML dialect most teams have never heard of |
Part 1 — Three formats, not two
Annotations put mapping metadata next to the field it describes. You don’t leave src/main/java, a matching column name needs no annotation at all, and most IDEs will flag an obviously wrong reference before you run anything — though that’s an IDE feature, not something javac itself checks. The cost is that your domain classes now carry a hard dependency on Jakarta Persistence, and any environment-specific difference (a different schema name per deployment, say) requires a code change and a recompile.
package com.ankurm.model;
import jakarta.persistence.*;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.Comment;
@Entity
@Table(name = "employees", indexes = {
@Index(name = "idx_employee_email", columnList = "email_address")
})
@DynamicUpdate // Hibernate-specific: only updates changed columns
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "emp_seq")
@SequenceGenerator(name = "emp_seq", sequenceName = "employee_sequence", allocationSize = 1)
private Long id;
@Column(name = "first_name", nullable = false, length = 100)
@Comment("The legal first name of the employee") // Hibernate-specific
private String firstName;
@Column(name = "email_address", unique = true)
private String email;
@Version
private Integer version;
}
“XML mapping” is where most comparisons oversimplify. Hibernate 7.4.5 actually ships three distinct XML dialects, and they are not interchangeable:
| Dialect | Root namespace | Status in 7.4.5 |
|---|---|---|
hbm.xml | hibernate-mapping (DTD) | works unmodified, WARN-level deprecated |
orm.xml (JPA standard) | https://jakarta.ee/xml/ns/persistence/orm | fully supported, portable across JPA providers |
mapping.xml (Hibernate native) | http://www.hibernate.org/xsd/orm/mapping | fully supported, Hibernate-only |
The first correction to make: hbm.xml is not a shim you have to opt into, and it is not on its way to a hard failure. A plain, annotation-free POJO mapped purely through a .hbm.xml file boots and round-trips correctly in 7.4.5.Final with hibernate.transform_hbm_xml.enabled left completely unset. The only artifact is a single WARN line, logged once at boot:
HHH90000028: Support for `<hibernate-mappings/>` is deprecated [RESOURCE : com/ankurm/hibernatedemo/mappingstyle/HbmEmployee.hbm.xml]; migrate to orm.xml or mapping.xml, or enable `hibernate.transform_hbm_xml.enabled` for on the fly transformation
The companion repo tried this three ways — unset, explicitly true, explicitly false — and all three boot successfully and behave identically for a basic mapping (chapter 04). If you’ve read that this flag is required, or that hbm.xml throws without it, that’s wrong for 7.4.5 — it’s a live code path with a warning label, not a shim.
<hibernate-mapping package="com.ankurm.hibernatedemo.mappingstyle">
<class name="HbmEmployee" table="hbm_employees">
<id name="id" column="id">
<generator class="native"/>
</id>
<property name="firstName" column="first_name" not-null="true" length="100"/>
<property name="email" column="email_address" unique="true"/>
</class>
</hibernate-mapping>
orm.xml, the JPA-standard format, buys you something different: portability across JPA providers, and Java classes that carry zero Jakarta Persistence dependency. That claim is easy to state and rarely demonstrated — here it is proven with an entity that has no annotations at all, not even @Entity:
<entity-mappings xmlns="https://jakarta.ee/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence/orm https://jakarta.ee/xml/ns/persistence/orm/orm_3_0.xsd"
version="3.0">
<entity class="com.ankurm.hibernatedemo.mappingstyle.OrmXmlOnlyEntity" access="FIELD">
<table name="xml_only_widgets"/>
<attributes>
<id name="id">
<generated-value strategy="IDENTITY"/>
</id>
<basic name="label">
<column name="label_text" length="80"/>
</basic>
</attributes>
</entity>
</entity-mappings>
Wired in with spring.jpa.mapping-resources=orm-xml-only-mapping.xml, Hibernate creates xml_only_widgets and a JPQL query against OrmXmlOnlyEntity resolves successfully — the entity name and every attribute come purely from the XML:
RESULT[orm-xml-only]: persisted+queried id=1 via JPQL against entity mapped ENTIRELY by orm.xml (table xml_only_widgets), zero annotations on the Java class.
Boot 4 moved this property’s home.spring.jpa.mapping-resourcesis still the property name, but its backing class is noworg.springframework.boot.jpa.autoconfigure.JpaProperties, not the pre-4.0org.springframework.boot.autoconfigure.orm.jpapackage — a side effect of Boot 4’s autoconfigure split (confirmed viajavaponspring-boot-jpa-4.1.1.jar). Any tutorial pointing at the old package for a custom bean definition needs the new one.
Two more traditional arguments for orm.xml are worth a line each, since neither was tested for this rewrite. Teams sometimes keep large native or HQL queries in <named-query> elements to keep Java classes shorter — chapter 14 confirms orm.xml-declared named queries work with zero extra registration effort, which is a real, verified version of this claim. The other traditional argument — that XML drifts silently out of type-sync with a refactored field (e.g. private Integer age becomes private Long age but the XML still says type="int", producing a ClassCastException) — is plausible but wasn’t reproduced here; treat it as an unverified but reasonable risk of any format where the type lives in two places.
Part 2 — When annotations and XML disagree
Real projects rarely commit fully to one format. The usual pattern is annotations for the base mapping, with an orm.xml overlay for something environment-specific — a different schema name in production, say. That only works safely if you know the merge rule, and the rule is unconditional: XML always wins.
Proven, not just documented: an entity field annotated @Column(name = "annotation_name"), with a matching orm.xml entry mapping the same field to xml_name. Reading the live runtime metamodel and running a native query against the literal column name confirms which one Hibernate actually used:
RESULT[override]: runtime column name for OverrideEntity.value = xml_name (annotation said 'annotation_name', orm.xml said 'xml_name')
RESULT[override]: native query against column 'xml_name' returned: hello
The figure below is the actual bootstrap sequence this test exercises — annotations are read first to build a base metadata map, then any registered XML is merged on top of it, and only a same-attribute conflict causes XML to overwrite what the annotation said.
Two operational pitfalls follow directly from that merge order:
- The hidden override. A developer renames a
@Column, forgets there’s a registeredorm.xmlmapping the same field, and the database keeps writing to the old column name with no error anywhere. Checkspring.jpa.mapping-resources(orpersistence.xml’s<mapping-file>entries) before assuming an annotation change took effect. - Multiple
persistence.xmlfiles on the classpath (common when a shared module and an app module both ship one) can make Hibernate pick up the wrong one, surfacing as an unrelated “entity not managed” error. This wasn’t reproduced for this rewrite, so treat it as a known failure shape to watch for, not a measured one.
On whether annotation scanning meaningfully slows startup versus XML parsing: this repo did not measure it, and the honest answer is it depends far more on classpath scanning scope (@EntityScan package breadth) than on annotations-vs-XML as a category. Treat any specific number you read for this as unverified until you’ve measured it in your own app.
Part 3 — The kill switch, and the dialect that does everything
The natural reading of <xml-mapping-metadata-complete/> is “XML overrides annotations for the attributes it mentions” — the same rule as Part 2, just declared up front instead of inferred per-attribute. That reading is wrong, and the gap between the two is dangerous enough to earn its own warning.
xml-mapping-metadata-completeis all-or-nothing, not per-attribute. It disables annotation processing for the entire persistence unit, not just the fields the XML repeats. An entity whose@Idis left untouched by the XML doesn’t fall back to the annotation — it loses its identifier entirely.
Proof: the same OverrideEntity from Part 2, with <xml-mapping-metadata-complete/> set and an orm.xml entry that maps only value, never mentioning id at all. The untouched @Id @GeneratedValue on the field simply stops existing:
Failed to initialize JPA EntityManagerFactory: Entity 'com.ankurm.hibernatedemo.mappingstyle.OverrideEntity' has no identifier (every '@Entity' class must declare or inherit at least one '@Id' or '@EmbeddedId' property)
Caused by: org.hibernate.AnnotationException: Entity 'com.ankurm.hibernatedemo.mappingstyle.OverrideEntity' has no identifier (every '@Entity' class must declare or inherit at least one '@Id' or '@EmbeddedId' property)
If you flip that switch, every attribute the annotations were supplying — identifiers included — needs an explicit XML entry, or boot fails.
That leads to the part of this article most comparisons skip entirely. “What can XML express that annotations can’t” is usually answered with nothing, because the comparison silently assumes orm.xml is the only XML option. It isn’t. Hibernate 7 ships a third dialect — mapping-7.0.xsd, namespace http://www.hibernate.org/xsd/orm/mapping — and its own schema documentation describes it as extending the JPA orm.xml XSD with Hibernate-specific features. Grepping both schemas for Hibernate-only concepts:
| Element | orm_3.x.xsd (JPA-standard) | mapping-7.0.xsd (Hibernate-native) |
|---|---|---|
<natural-id> | absent | present |
<formula> / <discriminator-formula> / <join-formula> | absent | present |
<filter>-related elements | absent | present (11 occurrences) |
In other words, the accurate framing is the inverse of how this comparison usually gets pitched: the portable orm.xml dialect can express less than annotations (no Hibernate extensions at all), while Hibernate’s own mapping.xml dialect can express anything annotations can, including a natural id, purely in XML — you trade JPA portability for that power, you don’t gain a capability annotations lack. Proven with a zero-annotation entity whose natural id is declared entirely in mapping-7.0.xsd syntax:
<entity-mappings xmlns="http://www.hibernate.org/xsd/orm/mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="7.0">
<entity class="com.ankurm.hibernatedemo.mappingstyle.MappingXmlNaturalIdEntity" access="FIELD">
<table name="mapping_xml_naturalid_widgets"/>
<attributes>
<id name="id">
<generated-value strategy="IDENTITY"/>
</id>
<natural-id mutable="false">
<basic name="sku">
<column name="sku" unique="true" nullable="false"/>
</basic>
</natural-id>
<basic name="name"/>
</attributes>
</entity>
</entity-mappings>
RESULT[mapping-xml-natural-id]: session.byNaturalId() resolved an entity whose @NaturalId-equivalent was declared ENTIRELY in Hibernate's native mapping.xml dialect, zero Java annotations. name=XML Widget
For what @NaturalId actually buys you at runtime — L1 versus L2 cache behavior, the composite-key form, the mutability trap — see the dedicated natural IDs guide; this article only needed the annotation-vs-XML angle on it.
Which format, honestly
Annotations for the base mapping, always — the IDE support and locality are worth more than the coupling costs for the vast majority of entities. Reach for orm.xml only for genuinely environment-specific overrides (a schema name that differs per deployment), knowing it always wins on conflict. Reach for mapping.xml only if you specifically need a Hibernate-only feature — a natural id, a formula, a filter — on a class that must stay free of any persistence-framework dependency; otherwise it’s strictly more ceremony than annotations for the same result. Don’t treat an inherited hbm.xml file as an emergency — it still works, the WARN is cosmetic, and a rewrite driven by fear rather than a concrete need is where most migration bugs come from.
FAQ
What is the difference between orm.xml and hbm.xml in Hibernate 7? hbm.xml is the legacy, Hibernate-proprietary format, still fully functional in 7.4.5 with a single WARN-level deprecation notice and no required opt-in flag. orm.xml is the JPA-standard equivalent, portable across providers, capped at what the JPA XSD can express — no Hibernate-only concepts. A third option, mapping.xml, is Hibernate’s own non-portable dialect that closes that gap.
Can annotations and XML conflict, and who wins? Yes, whenever the same attribute is mapped by both. XML always wins, for either XML dialect, confirmed above against the live runtime metamodel.
Can I auto-convert an old hbm.xml file to annotations? Not with an official one-click Hibernate tool. IDE “persistence” views that reverse-engineer entities from a live schema get you most of the way, but there is no requirement to convert at all — see above.
Should you introduce XML mapping into a new project?
Usually no. If you’re starting fresh with Spring Boot and Hibernate 7, annotations alone will carry you the whole way for the overwhelming majority of entities, and every one of these XML formats adds a second place to look when something doesn’t match what the Java class says. Reach for XML deliberately, for one of the three narrow reasons above — not because a blog post from the Hibernate 5 era told you XML was more “enterprise.”
Further reading
- Repo: Chapter 04 — Annotations vs. XML mappings, with the full test suite behind every claim above
- JPA persistence annotations in Hibernate 7 — what each annotation does and where it silently misbehaves
- Hibernate 7 natural IDs — the runtime behavior of the feature this article only mapped in XML
- Association mappings in Hibernate 7 —
@OneToMany,@ManyToMany, and the fetch-strategy pitfalls XML doesn’t change - Hibernate 7 with Spring Boot 4 — annotation-driven configuration in the Spring ecosystem
- Hibernate 7 Hello World example — hibernate.cfg.xml and annotation-based setup side by side, for the smallest possible starting point
- Hibernate 7 documentation index
- Official Hibernate 7 User Guide — Entity Mapping
- Hibernate 7 Migration Guide — hbm.xml deprecation notes
No Comments yet!