Spring Boot + MyBatis: XML vs Annotations – Which to Pick?
Learn how to integrate MyBatis with Spring Boot using XML mappings and annotations.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+
- ✓Spring Boot 3.x
- ✓Basic knowledge of SQL and JDBC
- ✓Familiarity with Maven or Gradle
- Use MyBatis when you need fine-grained SQL control and complex mappings that JPA struggles with.
- Annotations are great for simple CRUD; XML mappings shine for dynamic SQL and legacy databases.
- Always define Mapper interfaces as Spring beans and use @MapperScan or @Mapper.
- For dynamic SQL, MyBatis's
and in XML beat JPA's criteria API hands down. - Beware of N+1 queries: MyBatis doesn't auto-fetch lazy associations like Hibernate.
Think of MyBatis as a smart assistant that helps you write SQL and map results to Java objects. Unlike JPA which tries to hide SQL, MyBatis puts you in control. XML mappings are like detailed blueprints for complex queries; annotations are sticky notes for simple ones.
If you've ever fought with Hibernate's lazy loading exceptions or watched in horror as it generated 10+ SQL queries for a simple join, you're not alone. JPA is powerful, but sometimes you need to take the wheel. That's where MyBatis comes in.
MyBatis is a persistence framework that gives you full control over SQL. You write the queries, you decide the mappings. No magic, no surprises. It's the go-to choice for teams working with legacy databases, complex reporting, or performance-critical systems where every millisecond counts.
In this article, I'll show you how to integrate MyBatis with Spring Boot 3.2, covering both XML mappings and annotations. You'll learn when to use each, how to avoid common pitfalls, and what the official documentation conveniently leaves out. By the end, you'll know exactly which approach fits your project and how to debug it when things go sideways.
Why MyBatis Still Matters in 2025
Let's get one thing straight: JPA is not the answer to everything. I've seen teams spend weeks trying to tune Hibernate's second-level cache, only to rip it out and replace it with MyBatis. MyBatis gives you raw SQL power without the overhead of an ORM. You write the SQL, you control the mapping.
Here's the hard truth: most teams get this wrong. They start with JPA because it's the 'standard,' then hit a wall with complex queries or legacy schemas. MyBatis shines when: - You have a legacy database with weird column names and no control over schema. - You need to write dynamic SQL with conditional WHERE clauses, pagination, and batch updates. - Performance is critical and you can't afford Hibernate's query generation overhead. - You're working with stored procedures or database-specific features.
Spring Boot 3.2 makes integration trivial. Add the starter, configure a datasource, define mappers, and you're off. But the devil is in the details—especially when mixing XML and annotations.
Getting Started: Dependencies and Configuration
First, add the MyBatis Spring Boot Starter. I recommend version 3.0.3 with Spring Boot 3.2. If you're still on Boot 2.x, use 2.3.x. Don't mix versions—I've seen ClassNotFoundException because of incompatible MyBatis core versions.
In your application.properties (or YAML), configure the datasource and MyBatis settings:
```properties spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false&allowPublicKeyRetrieval=true spring.datasource.username=root spring.datasource.password=secret spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.mapper-locations=classpath:mapper/*/.xml mybatis.configuration.map-underscore-to-camel-case=true mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl ```
The map-underscore-to-camel-case setting is a lifesaver. It automatically maps user_name to userName in your Java POJO. Without it, you'll spend hours writing explicit mappings.
Now create a Mapper interface. You can use either XML or annotations—or both. I'll show you both approaches.
Annotation-Based Mappings: Simple and Direct
For straightforward CRUD operations, annotations are perfect. You define SQL directly on the method using @Select, @Insert, @Update, @Delete. No XML files needed.
Here's an example for a Payment entity:
```java @Mapper public interface PaymentMapper { @Select("SELECT * FROM payments WHERE id = #{id}") Payment findById(Long id);
@Insert("INSERT INTO payments(amount, currency, status) VALUES(#{amount}, #{currency}, #{status})") @Options(useGeneratedKeys = true, keyProperty = "id") int insert(Payment payment);
@Update("UPDATE payments SET status = #{status} WHERE id = #{id}") int update(Payment payment);
@Delete("DELETE FROM payments WHERE id = #{id}") int delete(Long id); } ```
Notice @Options(useGeneratedKeys = true). This is critical for auto-increment IDs. If you forget it, the generated key won't be populated in your object.
For complex queries with joins, annotations can get messy. You end up with long strings and no syntax highlighting. That's when XML shines.
XML Mappings: Power and Flexibility
XML mappings give you the full power of MyBatis: dynamic SQL, result maps, and reusable fragments. They're essential for complex queries that would be a nightmare in annotations.
Create an XML file in src/main/resources/mapper/PaymentMapper.xml:
```xml <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.mapper.PaymentMapper">
<resultMap id="PaymentResultMap" type="com.example.model.Payment"> <id property="id" column="id" /> <result property="amount" column="amount" /> <result property="currency" column="currency" /> <result property="status" column="status" /> <result property="createdAt" column="created_at" /> </resultMap>
<select id="findById" resultMap="PaymentResultMap"> SELECT * FROM payments WHERE id = #{id} </select>
<select id="findByStatus" resultMap="PaymentResultMap"> SELECT * FROM payments <where> <if test="status != null and status != ''"> AND status = #{status} </if> </where> ORDER BY created_at DESC </select>
<insert id="insert" useGeneratedKeys="true" keyProperty="id"> INSERT INTO payments(amount, currency, status) VALUES(#{amount}, #{currency}, #{status}) </insert>
<update id="update"> UPDATE payments SET status = #{status} WHERE id = #{id} </update>
<delete id="delete"> DELETE FROM payments WHERE id = #{id} </delete> </mapper> ```
Notice the <where> and <if> tags. This is dynamic SQL—one of MyBatis's strongest features. You can build queries conditionally without string concatenation. The <where> tag automatically handles the first AND.
Also note the resultMap. This gives you explicit control over column-to-field mapping. It's verbose but bulletproof. For simple cases, you can use resultType instead, but I always prefer resultMap for clarity.
When to Use Annotations vs XML: A Practical Guide
Here's my rule of thumb:
- Annotations: Use for simple CRUD, single-table operations, and when you have fewer than 10 queries per mapper. The code is self-contained and easy to read.
- XML: Use for complex queries with joins, dynamic SQL, stored procedures, and when you need result mappings that differ from column names. Also use XML when you want to keep SQL separate from Java code for DBA review.
But here's the twist: you can mix both in the same mapper. MyBatis allows it. I often define simple queries with annotations and complex ones in XML. Just be consistent within your team.
One thing the docs don't tell you: if you have both an annotation and an XML entry for the same method, the XML takes precedence. I've seen confusion when someone adds an annotation and wonders why it's not being used because an old XML file is still on the classpath.
What the Official Docs Won't Tell You
Let me save you hours of debugging with these hard-earned lessons:
1. MyBatis Does Not Manage Transactions The official docs assume you know this, but many don't. MyBatis does not start or commit transactions. It delegates to the underlying JDBC connection. If you're using Spring Boot, you must use @Transactional or a TransactionTemplate. Without it, each statement auto-commits. The production incident earlier is a perfect example.
2. The 'Invalid Bound Statement' Error Is Usually a Classpath Issue You've added the XML file, but MyBatis can't find it. Check: - The namespace matches the interface FQN. - The XML file is in the path specified by mybatis.mapper-locations. - The XML file is not filtered by Maven resource plugin. Add <include>*/.xml</include> in your POM if needed.
3. Lazy Loading in MyBatis Is Not Like Hibernate MyBatis supports lazy loading for associations, but it's not enabled by default. You need to configure lazyLoadingEnabled=true and aggressiveLazyLoading=false. Even then, lazy loading requires a proxy and can lead to N+1 queries if you're not careful. I recommend using join queries instead.
4. Type Handlers Are Your Friend for Custom Types Need to map a PostgreSQL jsonb column to a Java Map? Write a custom TypeHandler. The docs cover it, but the real gotcha is that you must register it in MyBatis configuration. Use @MappedTypes and @MappedJdbcTypes annotations, or register globally via mybatis.type-handlers-package.
5. XML Mapper Files Are Not Hot-Reloaded Unlike JPA's automatic DDL, MyBatis XML files are loaded at startup. If you change an XML file, you must restart the application. In development, you can use Spring Boot DevTools with mybatis.configuration.cache-enabled=false to force reload, but it's not perfect. For production, treat XML files like code—they should be version-controlled and deployed with the app.
Advanced: Dynamic SQL with
⚙ Quick Reference8 commands from this guideFile Command / Code Purpose pom.xml Why MyBatis Still Matters in 2025 application.properties spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false Getting Started PaymentMapper.java @Mapper Annotation-Based Mappings PaymentMapper.xml XML Mappings HybridMapper.java @Mapper When to Use Annotations vs XML CustomTypeHandler.java @MappedTypes(Map.class) What the Official Docs Won't Tell You AdvancedMapper.java @Mapper Advanced PaymentMapperTest.java @MybatisTest Testing MyBatis Mappers
Key takeaways
1MyBatis gives you full SQL control and is ideal for complex queries and legacy databases.2Use annotations for simple CRUD, XML for dynamic SQL and complex mappings.3Always manage transactions explicitly with @Transactional.4Enable camelCase mapping and use @Param for clarity.5Test mappers with @MybatisTest and ensure XML files are on the classpath.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIORExplain the difference between MyBatis and JPA/Hibernate. When would you...Q02JUNIORHow do you configure MyBatis to use camelCase mapping from underscore co...Q03SENIORWhat is the purpose of @Param annotation in MyBatis mapper methods?
Q01 of 03SENIORExplain the difference between MyBatis and JPA/Hibernate. When would you choose MyBatis over JPA?
ANSWERMyBatis is a SQL mapper that gives you full control over SQL, while JPA is an ORM that abstracts SQL. Choose MyBatis when you have legacy databases, complex queries, or need performance optimization that JPA's automatic query generation hinders. Also choose MyBatis when your team prefers writing SQL.
FAQ · 3 QUESTIONS
Frequently Asked Questions
N
Naren
Founder & Principal Engineer
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
Follow
✓ Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥
That's Hibernate & JPA. Mark it forged?
5 min read · try the examples if you haven't
| File | Command / Code | Purpose |
|---|---|---|
| pom.xml | Why MyBatis Still Matters in 2025 | |
| application.properties | spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false | Getting Started |
| PaymentMapper.java | @Mapper | Annotation-Based Mappings |
| PaymentMapper.xml | XML Mappings | |
| HybridMapper.java | @Mapper | When to Use Annotations vs XML |
| CustomTypeHandler.java | @MappedTypes(Map.class) | What the Official Docs Won't Tell You |
| AdvancedMapper.java | @Mapper | Advanced |
| PaymentMapperTest.java | @MybatisTest | Testing MyBatis Mappers |
Key takeaways
Interview Questions on This Topic
Explain the difference between MyBatis and JPA/Hibernate. When would you choose MyBatis over JPA?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Hibernate & JPA. Mark it forged?
5 min read · try the examples if you haven't