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:
- Mapper Interface + XML (Recommended): Offers maximum flexibility for writing and maintaining complex SQL.
- 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 theWHERE
clause only if conditions are met and strips leadingAND
orOR
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
}