Practical MyBatis Integration and Advanced Configuration Guide

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 &gt;= #{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 &gt;= #{minBalance}</if>
      <if test="maxBalance != null">AND balance &lt;= #{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 @Param for multi-parameter methods and avoid raw Map unless 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 @Data on entities used in caching unless serialVersionUID and equals/hashCode behavior are fully understood.

Tags: MyBatis java Persistence sql ORM

Posted on Wed, 05 Aug 2026 16:01:02 +0000 by cityboy101