MyBatis Annotation-Based Persistence: From Basic CRUD to Complex Mappings

Core Annotation-Driven Operations

Essential MyBatis Annotations

Modern Java development favors annotation-based configuration over XML. MyBatis provides a comprehensive set of annotations that eliminate the need for XML mapper files while maintaining full functionality.

Key annotations include:

  • @Select: Executes query statements
  • @Insert: Handles record creation
  • @Update: Manages record modifications
  • @Delete: Performs record removal
  • @Result: Defines individual column-to-field mappings
  • @Results: Container for multiple @Result definitions
  • @One: Configures single-entity associations (one-to-one)
  • @Many: Configures collection associations (one-to-many)

Single-Table CRUD Implementation

Consider a User entity mappped to a users table with fields user_id, username, and email.

Repository Interface Definition

public interface UserRepository {
    @Select("SELECT user_id as id, username as name, email FROM users")
    @Results({
        @Result(property = "id", column = "user_id"),
        @Result(property = "name", column = "username"),
        @Result(property = "email", column = "email")
    })
    List<User> findAll();

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

    @Update("UPDATE users SET username=#{name}, email=#{email} WHERE user_id=#{id}")
    int modify(User user);

    @Delete("DELETE FROM users WHERE user_id=#{uid}")
    int removeById(@Param("uid") Integer userId);
}

Integration Test Demonstration

public class UserRepositoryTest {
    private SqlSessionFactory sessionFactory;
    
    @Before
    public void setup() throws IOException {
        InputStream configStream = Resources.getResourceAsStream("mybatis-config.xml");
        sessionFactory = new SqlSessionFactoryBuilder().build(configStream);
    }
    
    @Test
    public void testFindAll() {
        try (SqlSession session = sessionFactory.openSession()) {
            UserRepository mapper = session.getMapper(UserRepository.class);
            List<User> users = mapper.findAll();
            users.forEach(System.out::println);
        }
    }
    
    @Test
    public void testAddUser() {
        try (SqlSession session = sessionFactory.openSession(true)) {
            UserRepository mapper = session.getMapper(UserRepository.class);
            User newUser = new User("jane.doe", "jane@example.com");
            int rowsAffected = mapper.add(newUser);
            assertEquals(1, rowsAffected);
        }
    }
}

XML Configuration Adjustment

When transitioning from XML mappers to annotations, update the configuration to scan annotated interfaces:

<mappers>
    <package name="com.example.mybatis.repository"/>
</mappers>

Advanced Association Mappings

Complex Relationship Configuration

Annotation-based mapping handles complex relationships through @Results combined with @One or @Many for nested queries.

One-to-One Association

Scenario: Fetch an Order with its associated Customer details.

Customer Lookup Method

public interface CustomerMapper {
    @Select("SELECT customer_id, full_name FROM customers WHERE customer_id = #{cid}")
    Customer fetchById(@Param("cid") Integer customerId);
}

Order Mapping with Nested Customer

public interface OrderMapper {
    @Select("SELECT order_id, order_number, customer_id FROM orders")
    @Results({
        @Result(property = "orderId", column = "order_id"),
        @Result(property = "orderNumber", column = "order_number"),
        @Result(
            property = "buyer",
            column = "customer_id",
            javaType = Customer.class,
            one = @One(select = "com.example.mapper.CustomerMapper.fetchById")
        )
    })
    List<Order> fetchAll();
}

One-to-Many Association

Scenario: Retrieve a Department with its list of Employee records.

Employee Retrieval by Department

public interface EmployeeMapper {
    @Select("SELECT emp_id, emp_name FROM employees WHERE dept_id = #{deptId}")
    List<Employee> getByDepartment(@Param("deptId") Integer departmentId);
}

Department with Employee Collection

public interface DepartmentMapper {
    @Select("SELECT dept_id, dept_name FROM departments")
    @Results({
        @Result(property = "departmentId", column = "dept_id"),
        @Result(property = "departmentName", column = "dept_name"),
        @Result(
            property = "staff",
            column = "dept_id",
            javaType = List.class,
            many = @Many(select = "com.example.mapper.EmployeeMapper.getByDepartment")
        )
    })
    List<Department> fetchAll();
}

Many-to-Many Association

Scenario: Display Student entities with their enrolled Course collections through a junction table.

Course Retrieval by Student

public interface CourseMapper {
    @Select("SELECT c.course_id, c.title FROM courses c "
          + "INNER JOIN enrollment e ON c.course_id = e.course_id "
          + "WHERE e.student_id = #{studentId}")
    List<Course> getCoursesByStudent(@Param("studentId") Integer studentId);
}

Student with Course Enrollment

public interface StudentMapper {
    @Select("SELECT DISTINCT s.student_id, s.full_name FROM students s")
    @Results({
        @Result(property = "studentId", column = "student_id"),
        @Result(property = "fullName", column = "full_name"),
        @Result(
            property = "enrollments",
            column = "student_id",
            javaType = List.class,
            many = @Many(select = "com.example.mapper.CourseMapper.getCoursesByStudent")
        )
    })
    List<Student> getAllStudents();
}

Dynamic SQL Construction

SQL Provider Mechanism

Hardcoding SQL in annotations becomes unwieldy for complex queries. MyBatis offers provider classes that generate SQL programmatically.

Query Provider Pattern

public class UserSqlProvider {
    public String buildFindByCriteria(final UserCriteria criteria) {
        return new SQL() {{
            SELECT("user_id, username, email");
            FROM("users");
            if (criteria.getName() != null) {
                WHERE("username LIKE #{namePattern}");
            }
            if (criteria.getStatus() != null) {
                WHERE("status = #{status}");
            }
        }}.toString();
    }
}

Repository Usage

@SelectProvider(type = UserSqlProvider.class, method = "buildFindByCriteria")
List<User> searchUsers(UserCriteria criteria);

Insert Provider Pattern

public class InsertSqlProvider {
    public String buildInsertUser(final User user) {
        return new SQL() {{
            INSERT_INTO("users");
            VALUES("username", "#{name}");
            VALUES("email", "#{email}");
            if (user.getPhone() != null) {
                VALUES("phone", "#{phone}");
            }
        }}.toString();
    }
}

Practical Application: Data Access Layer Refactoring

Legacy System Modernization

Refactor a legacy product management system from JDBC to MyBatis annotations.

MyBatis Configuration

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" 
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <properties resource="application.properties"/>
    
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <setting name="logImpl" value="SLF4J"/>
    </settings>
    
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${db.driver}"/>
                <property name="url" value="${db.url}"/>
                <property name="username" value="${db.username}"/>
                <property name="password" value="${db.password}"/>
            </dataSource>
        </environment>
    </environments>
    
    <mappers>
        <package name="com.example.product.persistence"/>
    </mappers>
</configuration>

Refactored Product Repository

public interface ProductRepository {
    @Select("SELECT product_id, product_name, unit_price FROM products")
    @Results({
        @Result(property = "productId", column = "product_id"),
        @Result(property = "productName", column = "product_name"),
        @Result(property = "unitPrice", column = "unit_price")
    })
    List<Product> fetchAllProducts();
    
    @Insert("INSERT INTO products(product_name, unit_price) VALUES(#{productName}, #{unitPrice})")
    @Options(useGeneratedKeys = true, keyProperty = "productId")
    int createProduct(Product product);
    
    @UpdateProvider(type = ProductSqlProvider.class, method = "buildUpdateStatement")
    int modifyProduct(Product product);
    
    @Delete("DELETE FROM products WHERE product_id = #{productId}")
    int removeProduct(@Param("productId") Integer id);
}

Tags: MyBatis annotations Persistence ORM java

Posted on Sun, 16 Aug 2026 16:15:25 +0000 by xengvang