What is MyBatis?

MyBatis is a first-class persistence framework that provides an alternative to traditional Object-Relational Mapping (ORM) solutions like Hibernate or JPA. Often referred to as a “SQL Mapper,” MyBatis distinguishes itself by embracing SQL. Instead of abstracting SQL away, it puts developers in full control, mapping SQL statements to Java methods.

The core philosophy of MyBatis is to decouple SQL from application logic while allowing you to leverage the full power of your database. It achieves this by mapping:

  • Result Mapping: The results of a SQL query to Java objects.
  • Parameter Mapping: Java objects and parameters to SQL statement placeholders.

This approach makes MyBatis an excellent choice for projects that require precise control over SQL, such as financial systems, high-performance transaction platforms, or applications with complex, performance-sensitive queries.

In a Spring Boot ecosystem, MyBatis can be integrated primarily in two ways:

  1. Mapper Interface + XML (Recommended): Offers maximum flexibility for writing and maintaining complex SQL.
  2. Mapper Interface + Annotations: Suitable for simple queries and smaller projects.

Integrating MyBatis with Spring Boot

Setting up MyBatis in a Spring Boot project is straightforward.

1. Add Dependencies

You’ll need the mybatis-spring-boot-starter and your database driver.

<!-- pom.xml -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

<!-- Example for MySQL -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

2. Configure application.yml

Configure the datasource and tell MyBatis where to find your mapper files and model classes.

# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/your_db?serverTimezone=UTC
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  # Location of your mapper XML files
  mapper-locations: classpath:mapper/*.xml
  # Package where your domain objects are located, creating aliases
  type-aliases-package: com.example.demo.model
  # Optional: Enables logging of executed SQL statements
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

Common Implementation Patterns

Let’s explore the three most common ways to implement MyBatis mappers.

1. Mapper Interface + XML (Most Flexible)

This is the most powerful and maintainable approach, especially for large-scale applications with complex queries.

a. Define the Model

It’s a best practice to separate your database entity from your DTOs (Data Transfer Objects) to avoid tight coupling.

// src/main/java/com/example/demo/model/User.java
public class User {
    private Long id;
    private String name;
    private Integer age;
    // Getters and setters
}

b. Create the Mapper Interface

This interface defines the methods that will be mapped to SQL statements. The @Mapper annotation marks it as a MyBatis mapper bean.

// src/main/java/com/example/demo/mapper/UserMapper.java
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface UserMapper {
    User findById(Long id);
    List<User> findAll();
    int insert(User user);
}

c. Write the Mapper XML

This XML file contains the actual SQL. The namespace must match the fully qualified name of the mapper interface.

<!-- src/main/resources/mapper/UserMapper.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.demo.mapper.UserMapper">

    <select id="findById" resultType="User">
        SELECT id, name, age
        FROM users
        WHERE id = #{id}
    </select>

    <select id="findAll" resultType="User">
        SELECT id, name, age
        FROM users
    </select>

    <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO users (name, age)
        VALUES (#{name}, #{age})
    </insert>
</mapper>

Note: resultType="User" works because we defined type-aliases-package.

2. Mapper Interface + Annotations (Concise)

For simple queries, annotations can reduce boilerplate by eliminating the need for XML files.

4 basic anntations: @Select, @Insert, @Update, @Delete

// src/main/java/com/example/demo/mapper/UserMapper.java
@Mapper
public interface UserMapper {
    @Select("SELECT id, name, age FROM users WHERE id = #{id}")
    User findById(Long id);

    @Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")
    @Options(useGeneratedKeys = true, keyProperty = "id")
    int insert(User user);

    @Update("UPDATE users SET name=#{name}, age=#{age} WHERE id=#{id}")
    int update(User user);

    @Delete("DELETE FROM users WHERE id = #{id}")
    int delete(Long id);
}

Automatic fullfill primary key

@Insert("INSERT INTO users(name, age) VALUES(#{name}, #{age})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);

Parameters transfer

@Select("SELECT * FROM users WHERE name = #{name} AND age = #{age}")
User findByNameAndAge(@Param("name") String name, @Param("age") int age);

Result mapping

@Select("SELECT user_id, user_name FROM users WHERE user_id = #{id}")
@Results({
    @Result(property = "id", column = "user_id"),
    @Result(property = "name", column = "user_name")
})
User findUser(Long id);

Cross table relationship

@Select("SELECT * FROM users WHERE id = #{id}")
@Results({
    @Result(property = "orders", column = "id",
        many = @Many(select = "com.example.demo.mapper.OrderMapper.findByUserId"))
})
User findUserWithOrders(Long id);

Dynamic SQL (@SelectProvider, @InsertProvider, @UpdateProvider, @DeleteProvider)

Annotations have no <if> <choose> like XML, but support provider for dynamic SQL.

@SelectProvider(type = UserSqlProvider.class, method = "search")
List<User> search(@Param("name") String name, @Param("age") Integer age);

Batch operation

@Insert({
    "<script>",
    "INSERT INTO users(name, age) VALUES",
    "<foreach collection='list' item='user' separator=','>",
    "(#{user.name}, #{user.age})",
    "</foreach>",
    "</script>"
})
int batchInsert(@Param("list") List<User> users);

Cache namespace

@CacheNamespace
@Mapper
public interface UserMapper {
    @Select("SELECT * FROM users WHERE id = #{id}")
    User findById(Long id);
}

However, for complex or dynamic SQL, annotations can become cumbersome and hard to read.

3. Dynamic SQL (A Powerful Feature)

Dynamic SQL is one of MyBatis’s most powerful features. It allows you to build queries based on conditional logic, avoiding messy if-else blocks in your Java code.

Consider a search scenario where users can filter by name and/or age.

<!-- src/main/resources/mapper/UserMapper.xml -->
<select id="search" parameterType="map" resultType="User">
    SELECT id, name, age FROM users
    <where>
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>
  • The <where> element intelligently adds the WHERE clause only if conditions are met and strips leading AND or OR operators.
  • The <if> elements check for the presence of parameters.

You would call this from your mapper interface:

// In UserMapper.java
List<User> search(Map<String, Object> params);

// In your service
Map<String, Object> params = new HashMap<>();
params.put("name", "Adam");
// params.put("age", 30); // Age is optional
List<User> users = userMapper.search(params);

Advanced Topic: ResultMaps

When column names don’t match your Java object’s properties, or for complex joins, resultType is not enough. ResultMap gives you full control over the mapping.

<resultMap id="UserResultMap" type="User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
    <result property="age" column="user_age"/>
</resultMap>

<select id="findById" resultMap="UserResultMap">
    SELECT user_id, user_name, user_age
    FROM users
    WHERE user_id = #{id}
</select>

Service and Controller Layers

Finally, let’s see how the mapper is used in the service and controller layers.

Service Layer

The service layer encapsulates business logic and orchestrates calls to one or more mappers.

// src/main/java/com/example/demo/service/UserService.java
@Service
public class UserService {
    private final UserMapper userMapper;

    // Constructor-based dependency injection
    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    public User getUserById(Long id) {
        return userMapper.findById(id);
    }

    public User createUser(String name, int age) {
        User user = new User();
        user.setName(name);
        user.setAge(age);
        userMapper.insert(user);
        return user; // The user object now contains the generated ID
    }
}

Controller Layer

The controller exposes the functionality via a REST API.

// src/main/java/com/example/demo/controller/UserController.java
@RestController
@RequestMapping("/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUserById(id);
    }

    @PostMapping
    public User createUser(@RequestBody CreateUserRequestDTO request) {
        return userService.createUser(request.getName(), request.getAge());
    }
}

// DTO for the request body
class CreateUserRequestDTO {
    private String name;
    private int age;
    // getters
}