Home Java Spring Boot + MyBatis: XML vs Annotations – Which to Pick?
Intermediate 5 min · July 14, 2026
Integrating MyBatis with Spring Boot: XML Mappings and Annotations

Spring Boot + MyBatis: XML vs Annotations – Which to Pick?

Learn how to integrate MyBatis with Spring Boot using XML mappings and annotations.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 17+
  • Spring Boot 3.x
  • Basic knowledge of SQL and JDBC
  • Familiarity with Maven or Gradle
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • 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.
✦ Definition~90s read
What is Integrating MyBatis with Spring Boot?

MyBatis is a SQL mapping framework that lets you write your own SQL and map results to Java objects with full control, unlike JPA which automates persistence.

Think of MyBatis as a smart assistant that helps you write SQL and map results to Java objects.
Plain-English First

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.

pom.xmlJAVA
1
2
3
4
5
6
7
8
9
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
🔥Version Compatibility
📊 Production Insight
I once saw a team mix MyBatis and JPA in the same transaction. Hibernate flushed before MyBatis, causing inconsistent reads. Stick to one per transaction or use separate transaction managers.
🎯 Key Takeaway
MyBatis is not a replacement for JPA—it's a tool for when you need fine-grained SQL control.

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.

application.propertiesJAVA
1
2
3
4
5
6
7
8
spring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=false
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
📊 Production Insight
In production, never use StdOutImpl logging. Use Log4j2 or SLF4J with a proper appender. StdOutImpl can cause thread contention under load.
🎯 Key Takeaway
Always enable map-underscore-to-camel-case unless you enjoy pain.

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.

```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.

PaymentMapper.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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);

    @Select("SELECT * FROM payments WHERE status = #{status} ORDER BY created_at DESC")
    List<Payment> findByStatus(@Param("status") String status);
}
💡Use @Param for Method Parameters
📊 Production Insight
I've debugged production issues where a missing @Param caused cryptic errors. Always add it—it's cheap insurance.
🎯 Key Takeaway
Annotations are great for simple queries but become unwieldy for complex SQL.

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

INSERT INTO payments(amount, currency, status) VALUES(#{amount}, #{currency}, #{status})

UPDATE payments SET status = #{status} WHERE id = #{id}

DELETE FROM payments WHERE id = #{id} ```

Notice the and tags. This is dynamic SQL—one of MyBatis's strongest features. You can build queries conditionally without string concatenation. The 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.

PaymentMapper.xmlJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?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>
⚠ Namespace Must Match Fully Qualified Interface
📊 Production Insight
Dynamic SQL with <foreach> is a common source of SQL injection if you concatenate strings. Always use the OGNL expression syntax and let MyBatis handle parameter substitution.
🎯 Key Takeaway
XML mappings are more verbose but offer dynamic SQL and explicit result mapping.

When to Use Annotations vs XML: A Practical Guide

  • 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.

HybridMapper.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
@Mapper
public interface PaymentMapper {
    // Annotation-based simple query
    @Select("SELECT * FROM payments WHERE id = #{id}")
    Payment findById(Long id);

    // XML-based complex query (defined in PaymentMapper.xml)
    List<Payment> searchPayments(@Param("status") String status,
                                  @Param("minAmount") BigDecimal minAmount,
                                  @Param("maxAmount") BigDecimal maxAmount);
}
💡Consistent Naming
📊 Production Insight
I've seen teams split: Java devs prefer annotations, DBAs prefer XML. Find a compromise. I recommend XML for anything that touches multiple tables or uses dynamic SQL.
🎯 Key Takeaway
Mix annotations and XML when it makes sense, but be aware of precedence rules.

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 */.xml 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.

CustomTypeHandler.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@MappedTypes(Map.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonbTypeHandler extends BaseTypeHandler<Map<String, Object>> {
    private static final ObjectMapper mapper = new ObjectMapper();

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Map<String, Object> parameter, JdbcType jdbcType) throws SQLException {
        try {
            ps.setString(i, mapper.writeValueAsString(parameter));
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public Map<String, Object> getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String json = rs.getString(columnName);
        return parseJson(json);
    }

    @Override
    public Map<String, Object> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String json = rs.getString(columnIndex);
        return parseJson(json);
    }

    @Override
    public Map<String, Object> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String json = cs.getString(columnIndex);
        return parseJson(json);
    }

    private Map<String, Object> parseJson(String json) {
        try {
            return mapper.readValue(json, Map.class);
        } catch (IOException e) {
            return null;
        }
    }
}
⚠ Type Handler Registration
📊 Production Insight
I once spent a day debugging a 'TypeException' because the TypeHandler was not registered. Always check the startup logs for 'Registered type handler' messages.
🎯 Key Takeaway
Know the hidden pitfalls: transaction management, classpath issues, lazy loading, and type handlers.

Advanced: Dynamic SQL with ") List searchPayments(@Param("status") String status, @Param("minAmount") BigDecimal minAmount); ``

Notice the > for >—XML escaping inside a Java string is a nightmare. This is why I prefer XML files for dynamic SQL. But if you must keep everything in annotations, this works.

Another advanced feature is @ResultMap which lets you reuse a result map defined in XML from an annotation. This is useful when you want the clarity of annotations for simple queries but the power of XML result maps for complex mappings.

AdvancedMapper.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Mapper
public interface PaymentMapper {
    // Dynamic SQL in annotation (ugly but works)
    @Select("<script>" +
            "SELECT * FROM payments " +
            "<where>" +
            "<if test='status != null'> AND status = #{status} </if>" +
            "<if test='minAmount != null'> AND amount &gt;= #{minAmount} </if>" +
            "</where>" +
            "</script>")
    List<Payment> searchPayments(@Param("status") String status,
                                  @Param("minAmount") BigDecimal minAmount);

    // Reuse XML result map
    @ResultMap("PaymentResultMap")
    @Select("SELECT * FROM payments WHERE id = #{id}")
    Payment findByIdWithResultMap(Long id);
}
🔥XML Escaping in Annotations
📊 Production Insight
I've seen SQL injection vulnerabilities from poorly escaped dynamic SQL in annotations. Always use parameterized queries ( #{} ) never string concatenation.
🎯 Key Takeaway
Dynamic SQL in annotations is possible but ugly. Stick to XML for anything beyond simple conditions.

Testing MyBatis Mappers

Testing MyBatis mappers is straightforward with Spring Boot's test slice @MybatisTest. It auto-configures an embedded database and MyBatis components.

```java @MybatisTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) class PaymentMapperTest {

@Autowired private PaymentMapper paymentMapper;

@Test void testInsertAndFindById() { Payment payment = new Payment(); payment.setAmount(new BigDecimal("100.00")); payment.setCurrency("USD"); payment.setStatus("PENDING");

int rows = paymentMapper.insert(payment); assertThat(rows).isEqualTo(1); assertThat(payment.getId()).isNotNull();

Payment found = paymentMapper.findById(payment.getId()); assertThat(found.getAmount()).isEqualByComparingTo(new BigDecimal("100.00")); } } ```

Note: @MybatisTest does not roll back transactions by default. Use @Transactional on your test class or method to ensure cleanup. Also, if you use XML mappers, ensure they are on the test classpath. A common mistake is forgetting to copy XML files to test resources.

For integration tests with a real database, use @SpringBootTest and @Testcontainers. I'll cover that in a future article.

PaymentMapperTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
@Transactional
class PaymentMapperTest {

    @Autowired
    private PaymentMapper paymentMapper;

    @Test
    void testInsertAndFindById() {
        Payment payment = new Payment();
        payment.setAmount(new BigDecimal("100.00"));
        payment.setCurrency("USD");
        payment.setStatus("PENDING");

        int rows = paymentMapper.insert(payment);
        assertThat(rows).isEqualTo(1);
        assertThat(payment.getId()).isNotNull();

        Payment found = paymentMapper.findById(payment.getId());
        assertThat(found.getAmount()).isEqualByComparingTo(new BigDecimal("100.00"));
    }

    @Test
    void testFindByStatus() {
        // Given
        Payment payment1 = new Payment();
        payment1.setAmount(new BigDecimal("50.00"));
        payment1.setCurrency("USD");
        payment1.setStatus("COMPLETED");
        paymentMapper.insert(payment1);

        Payment payment2 = new Payment();
        payment2.setAmount(new BigDecimal("75.00"));
        payment2.setCurrency("EUR");
        payment2.setStatus("PENDING");
        paymentMapper.insert(payment2);

        // When
        List<Payment> completed = paymentMapper.findByStatus("COMPLETED");

        // Then
        assertThat(completed).hasSize(1);
        assertThat(completed.get(0).getAmount()).isEqualByComparingTo(new BigDecimal("50.00"));
    }
}
💡Test XML Mapper Discovery
📊 Production Insight
I've seen CI pipelines fail because XML mappers were missing from test resources. Always verify with a simple test that loads the mapper context.
🎯 Key Takeaway
Use @MybatisTest for slice tests and ensure XML files are on the test classpath.

● Production incidentPOST-MORTEMseverity: high

The Case of the Vanishing Transactions

Symptom
Payment reconciliation batch job ran successfully (no exceptions), but account balances did not update. Some transactions appeared to be lost.
Assumption
The developer assumed MyBatis auto-committed after each statement, so no explicit transaction management was needed.
Root cause
MyBatis was configured to use auto-commit mode, but the batch job executed multiple updates across different tables. Without a Spring @Transactional wrapper, a failure after the first update left the database in an inconsistent state. The job was running without any transaction boundaries.
Fix
Wrapped the batch method with @Transactional(rollbackFor = Exception.class) and added a DataSourceTransactionManager bean. Also set 'spring.datasource.hikari.auto-commit=false' to enforce explicit transaction management.
Key lesson
  • Never rely on auto-commit for multi-statement operations. Always use @Transactional.
  • MyBatis does not manage transactions; it delegates to Spring's transaction manager.
  • Test batch jobs with failure scenarios to ensure rollback works as expected.
  • Enable SQL logging (mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl) during development to catch unexpected behavior.
  • Use a transaction template for programmatic transaction control when annotations aren't enough.
Production debug guideSymptom to Action5 entries
Symptom · 01
Mapper method returns null or empty list but SQL works in DB client
Fix
Check result mapping: ensure column names match Java property names (case-sensitive). Use @Results or XML <resultMap> to map explicitly. Verify mybatis.configuration.map-underscore-to-camel-case is true.
Symptom · 02
'Invalid bound statement (not found)' error
Fix
Check that XML mapper namespace matches the fully qualified Mapper interface. Ensure XML files are in 'classpath:mapper//.xml' and mybatis.mapper-locations is set correctly.
Symptom · 03
Slow query performance in production but fast in dev
Fix
Check if N+1 queries are happening due to lazy loading. Use MyBatis's <collection> or <association> with join fetch. Enable slow query logging in MySQL (long_query_time=2).
Symptom · 04
Transaction not rolling back on exception
Fix
Ensure @Transactional is on a public method called from outside the class (proxy limitation). Check that exceptions are not caught within the method. Verify transaction manager is configured.
Symptom · 05
MyBatis caching returns stale data
Fix
Local cache (SqlSession level) can cause stale reads across method calls. Disable caching for critical queries: add 'useCache="false"' to <select> or set 'mybatis.configuration.cache-enabled=false'.
★ Quick Debug Cheat SheetCommon MyBatis issues and immediate actions
Mapper not found
Immediate action
Check @MapperScan base packages
Commands
mvn dependency:tree | grep mybatis
grep -r 'namespace' src/main/resources/mapper/
Fix now
Add @MapperScan("com.example.mapper") to main class
Result mapping mismatch+
Immediate action
Enable camelCase mapping
Commands
mybatis.configuration.map-underscore-to-camel-case=true
Check column names in SQL vs property names
Fix now
Add @Results annotation or <resultMap>
Transaction not rolling back+
Immediate action
Verify @Transactional on public method
Commands
Check if method is public and called from external bean
Check exception type: by default only RuntimeException triggers rollback
Fix now
Add rollbackFor = Exception.class
N+1 queries+
Immediate action
Check for <collection> without join
Commands
Enable SQL logging to see all queries
mybatis.configuration.log-impl=...
Fix now
Rewrite query with JOIN and use <collection> with columnPrefix
Stale cache+
Immediate action
Disable cache temporarily
Commands
mybatis.configuration.cache-enabled=false
Add flushCache="true" to <select>
Fix now
Use @Options(flushCache = FlushCachePolicy.TRUE) on mapper method
FeatureAnnotationsXML Mappings
SetupSimple, no extra filesRequires XML file per mapper
ReadabilityGood for simple queriesBetter for complex queries
Dynamic SQLPossible with <script> (ugly)Native support with <if>, <where>
Result MappingVia @ResultsVia <resultMap> (more explicit)
SQL SeparationSQL in Java codeSQL in separate XML files
Hot ReloadYes (with DevTools)No (requires restart)
Type SafetyLow (string SQL)Low (string SQL)
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
pom.xmlWhy MyBatis Still Matters in 2025
application.propertiesspring.datasource.url=jdbc:mysql://localhost:3306/mydb?useSSL=falseGetting Started
PaymentMapper.java@MapperAnnotation-Based Mappings
PaymentMapper.xmlXML Mappings
HybridMapper.java@MapperWhen to Use Annotations vs XML
CustomTypeHandler.java@MappedTypes(Map.class)What the Official Docs Won't Tell You
AdvancedMapper.java@MapperAdvanced
PaymentMapperTest.java@MybatisTestTesting MyBatis Mappers

Key takeaways

1
MyBatis gives you full SQL control and is ideal for complex queries and legacy databases.
2
Use annotations for simple CRUD, XML for dynamic SQL and complex mappings.
3
Always manage transactions explicitly with @Transactional.
4
Enable camelCase mapping and use @Param for clarity.
5
Test mappers with @MybatisTest and ensure XML files are on the classpath.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Explain the difference between MyBatis and JPA/Hibernate. When would you...
Q02JUNIOR
How do you configure MyBatis to use camelCase mapping from underscore co...
Q03SENIOR
What is the purpose of @Param annotation in MyBatis mapper methods?
Q01 of 03SENIOR

Explain the difference between MyBatis and JPA/Hibernate. When would you choose MyBatis over JPA?

ANSWER
MyBatis 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

01
Can I use both MyBatis and JPA in the same Spring Boot application?
02
How do I handle pagination with MyBatis?
03
What is the difference between #{} and ${} in MyBatis?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Hibernate & JPA. Mark it forged?

5 min read · try the examples if you haven't

Previous
Introduction to Spring Data REST: Exposing JPA Repositories as REST APIs
21 / 28 · Hibernate & JPA
Next
Introduction to Spring Data Redis: CRUD Operations and Caching