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@Resultdefinitions@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);
}