Core Concepts of MyBatis
MyBatis is a lightweight, flexible persistence framework that bridges Java objects and relational databases. It eliminates boilerplate JDBC code—such as resource management, parameter binding, and result set handling—while retaining full control over SQL. Developers define mappings using XML files or annotations, enabling clean separation between data access logic and business code.
Maven Dependencies
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
Database Setup
CREATE DATABASE mybatis_demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE mybatis_demo;
CREATE TABLE account (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
full_name VARCHAR(64) NOT NULL,
email VARCHAR(128) UNIQUE,
balance DECIMAL(12,2) DEFAULT 0.00,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO account (full_name, email, balance) VALUES
('Alice Chen', 'alice@example.com', 5000.00),
('Bob Lee', 'bob@example.com', 3200.50),
('Cara Wang', 'cara@example.com', 7890.25);
Initial Project Configuration
Configuration File (mybatis-config.xml)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="database.properties" />
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
<setting name="cacheEnabled" value="true" />
</settings>
<typeAliases>
<package name="com.example.model" />
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC" />
<dataSource type="POOLED">
<property name="driver" value="${driver}" />
<property name="url" value="${url}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.example.mapper" />
</mappers>
</configuration>
External Properties (database.properties)
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mybatis_demo?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
username=root
password=devpass
Utility Class for Session Menagement
package com.example.util;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
public class SessionFactoryProvider {
private static final SqlSessionFactory FACTORY;
static {
try (InputStream config = Resources.getResourceAsStream("mybatis-config.xml")) {
FACTORY = new SqlSessionFactoryBuilder().build(config);
} catch (IOException e) {
throw new RuntimeException("Failed to initialize SqlSessionFactory", e);
}
}
public static SqlSession openSession() {
return FACTORY.openSession(true); // auto-commit enabled
}
}
Domain Model and Mapper Interface
Entity Class (Account.java)
package com.example.model;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class Account {
private Long id;
private String fullName;
private String email;
private Double balance;
private java.time.LocalDateTime createdAt;
}
Mapper Interface (AccountMapper.java)
package com.example.mapper;
import com.example.model.Account;
import org.apache.ibatis.annotations.*;
import java.util.List;
import java.util.Map;
@Mapper
public interface AccountMapper {
@Select("SELECT * FROM account WHERE id = #{id}")
Account findById(Long id);
@Select("SELECT * FROM account WHERE email = #{email}")
Account findByEmail(String email);
@Select("SELECT * FROM account ORDER BY balance DESC LIMIT #{limit} OFFSET #{offset}")
List<Account> findTopBalances(@Param("limit") int limit, @Param("offset") int offset);
@Insert("INSERT INTO account (full_name, email, balance, created_at) " +
"VALUES (#{fullName}, #{email}, #{balance}, NOW())")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(Account account);
@Update("UPDATE account SET full_name = #{fullName}, email = #{email}, " +
"balance = #{balance} WHERE id = #{id}")
int update(Account account);
@Delete("DELETE FROM account WHERE id = #{id}")
int deleteById(Long id);
@Select("<script>SELECT * FROM account " +
"WHERE balance >= #{minBalance} " +
"<if test='emailPattern != null'>AND email LIKE CONCAT('%', #{emailPattern}, '%')</if> " +
"ORDER BY created_at DESC</script>")
List<Account> searchAccounts(@Param("minBalance") Double minBalance, @Param("emailPattern") String emailPattern);
@Select("SELECT COUNT(*) FROM account")
long countAll();
}
Dynamic SQL and Conditional Queries
XML-Based Mapper (AccountMapper.xml)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.AccountMapper">
<sql id="base_columns">
id, full_name, email, balance, created_at
</sql>
<select id="searchByCriteria" resultType="Account">
SELECT <include refid="base_columns" /> FROM account
<where>
<if test="minBalance != null">AND balance >= #{minBalance}</if>
<if test="maxBalance != null">AND balance <= #{maxBalance}</if>
<if test="namePattern != null and namePattern != ''">
AND full_name LIKE CONCAT('%', #{namePattern}, '%')
</if>
<if test="emailDomain != null and emailDomain != ''">
AND email LIKE CONCAT('%@', #{emailDomain})
</if>
</where>
ORDER BY created_at DESC
</select>
<update id="adjustBalance">
UPDATE account
<set>
<if test="delta != null">balance = balance + #{delta},</if>
<if test="newEmail != null">email = #{newEmail},</if>
<if test="newName != null">full_name = #{newName},</if>
updated_at = NOW()
</set>
WHERE id = #{id}
</update>
<select id="findInIds" resultType="Account">
SELECT <include refid="base_columns" /> FROM account
WHERE id IN
<foreach item="id" collection="ids" open="(" separator="," close=")">
#{id}
</foreach>
</select>
</mapper>
Caching Strategy
Enabling Second-Level Cache
Add <cache /> inside AccountMapper.xml:
<mapper namespace="com.example.mapper.AccountMapper">
<cache
eviction="LRU"
flushInterval="3600000"
size="256"
readOnly="true" />
<!-- other statements -->
</mapper>
Ensure the entity implements Serializable:
package com.example.model;
import java.io.Serializable;
@Data
@NoArgsConstructor
public class Account implements Serializable {
private static final long serialVersionUID = 1L;
// fields...
}
Testing with JUnit
package com.example.test;
import com.example.mapper.AccountMapper;
import com.example.model.Account;
import com.example.util.SessionFactoryProvider;
import org.apache.ibatis.session.SqlSession;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class AccountMapperTest {
private SqlSession session;
private AccountMapper mapper;
@BeforeEach
void setUp() {
session = SessionFactoryProvider.openSession();
mapper = session.getMapper(AccountMapper.class);
}
@Test
void shouldFindAccountById() {
Account account = mapper.findById(1L);
assertNotNull(account);
assertEquals("Alice Chen", account.getFullName());
}
@Test
void shouldSearchAccountsByBalanceRange() {
List<Account> results = mapper.searchByCriteria(3000.0, 6000.0, null, null);
assertTrue(results.size() >= 1);
}
@Test
void shouldHandleBatchOperations() {
List<Long> ids = Arrays.asList(1L, 2L, 3L);
List<Account> accounts = mapper.findInIds(ids);
assertEquals(3, accounts.size());
}
@Test
void shouldSupportPagination() {
List<Account> topTwo = mapper.findTopBalances(2, 0);
assertEquals(2, topTwo.size());
}
}
Logging Configuration
Enable standard logging in mybatis-config.xml:
<settings>
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
For production, integrate Log4j2 via log4j2.xml:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
Best Practices Summary
- Prefer annotation-based mappers for simple CRUD operations.
- Use XML mappers for complex dynamic SQL, joins, or reusable fragments.
- Always configure second-level cache on read-heavy, infrequently updated entities.
- Leverage
@Paramfor multi-parameter methods and avoid rawMapunless necessary. - Validate SQL injection risks: use
#{}for parameters,${}only for static identifiers like table/column names. - Enable logging during development to inspect generated SQL and execution plans.
- Apply Lombok judiciously—avoid
@Dataon entities used in caching unlessserialVersionUIDandequals/hashCodebehavior are fully understood.