@Override public List<Dept> list() { return deptMapper.list(); } }
Mapper Layer:
1 2 3 4 5
@Mapper publicinterfaceDeptMapper { @Select("select id, name, create_time, update_time from dept order by update_time desc") List<Dept> list(); }
Key Points:
@RestController = @Controller + @ResponseBody, returns JSON data
@RequestMapping(“/depts”) extracts the common path to avoid repetition
@Slf4j is Lombok’s logging annotation, more standard than System.out.println()
Delete Department Requirement: Delete a department by ID.
Controller Layer:
1 2 3 4 5 6
@DeleteMapping("/{id}") public Result delete(@PathVariable Integer id) { log.info("Deleting department with ID: {}", id); deptService.delete(id); return Result.success(); }
@Delete("delete from dept where id = #{id}") voiddeleteById(Integer id);
Important: The @PathVariable annotation retrieves parameters from the URL path. Make sure the variable name matches the path variable name, or specify it explicitly like @PathVariable(“id”) Integer deptId. (See Path Variable Binding)
Add Department Requirement: Add a new department, setting creation and update times.
Controller Layer:
1 2 3 4 5 6
@PostMapping public Result add(@RequestBody Dept dept) { log.info("Adding department: {}", dept); deptService.add(dept); return Result.success(); }
Service Layer:
1 2 3 4 5 6 7
@Override publicvoidadd(Dept dept) { // Fill in basic attributes dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.insert(dept); }
The frontend only sends the name; we set createTime and updateTime in Service
Update Department Query for Display:
1 2 3 4 5 6
@GetMapping("/{id}") public Result getInfo(@PathVariable Integer id) { log.info("Querying department by ID: {}", id); Deptdept= deptService.getById(id); return Result.success(dept); }
// Service
1 2 3 4
@Override public Dept getById(Integer id) { return deptMapper.getById(id); }
// Mapper
1 2 3
@Select("select id, name, create_time, update_time from dept where id = #{id}") Dept getById(Integer id);
Update Data:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
@PutMapping public Result update(@RequestBody Dept dept) { log.info("Updating department: {}", dept); deptService.update(dept); return Result.success(); }
// Service @Override publicvoidupdate(Dept dept) { dept.setUpdateTime(LocalDateTime.now()); deptMapper.update(dept); }
// Mapper @Update("update dept set name = #{name}, update_time = #{updateTime} where id = #{id}") voidupdate(Dept dept);
Note: Only update name and update_time; create_time remains unchanged.
Employee Management: Pagination & Dynamic SQL
Employee management is more complex due to larger data volumes requiring pagination and conditional filtering.
Pagination Query (Basic Version)
Define a PageBean class:
1 2 3 4 5 6 7
@Data @NoArgsConstructor @AllArgsConstructor publicclassPageBean { private Long total; // Total record count private List<Emp> rows; // Current page data }
@Select("select * from emp order by update_time desc") List<Emp> list();
Key Points (PageHelper Understanding):
PageHelper.startPage(page, pageSize) uses a ThreadLocal to store pagination parameters. The next MyBatis query (the first one after this call) will automatically have LIMIT clauses appended.
It only affects the first SQL statement after the call. If you execute multiple queries, only the first one gets paginated.
The returned List is actually a Page object (a subclass of ArrayList) that contains pagination info like total count, page number, etc. You can cast it or directly use it as a list.
Important: Ensure that PageHelper.startPage() is called just before the mapper method that performs the actual data query. Do not put any other database operations in between.
For conditional queries, you still call PageHelper.startPage() first, then the dynamic SQL method; the plugin will automatically add LIMIT to that query.
<selectid="list"resultType="com.alex.pojo.Emp"> select e.*, d.name deptName from emp as e left join dept as d on e.dept_id = d.id <where> <iftest="name != null and name != ''"> e.name like concat('%',#{name},'%') -- use concat for safe string concatenation, prevent sql injecting!!! </if> <iftest="gender != null"> and e.gender = #{gender} </if> <iftest="begin != null and end != null"> and e.entry_date between #{begin} and #{end} </if> </where> order by update_time desc </select>
</mapper>
Important – Dynamic SQL Details:
tag: Automatically handles the removal of the first AND or OR if the condition is true. It ensures the SQL syntax is correct.
tag: The test attribute is an OGNL expression. Pay attention to:
String comparison: name != null and name != ‘’
Numeric comparison: gender != null
Date comparison: both begin and end must be non-null for range query.
#{} vs ${}: Always use #{} for parameter placeholders. It generates a prepared statement and prevents SQL injection. ${} is for literal substitution (e.g., table names) and should be avoided for user input.
Fuzzy matching: Use concat(‘%’, #{name}, ‘%’) instead of ‘%${name}%’ to avoid SQL injection. This is the safest way.
LocalDate comparison: In the SQL, between #{begin} and #{end} works because MyBatis automatically converts LocalDate to java.sql.Date when binding parameters. No special handling needed.
Handling empty strings: If name is an empty string, the condition name != ‘’ will be false, so it won’t be included. This is usually desired.
Batch Delete Employees – ForEach in XML
Controller:
1 2 3 4 5 6
@DeleteMapping("/{ids}") public Result delete(@PathVariable List<Integer> ids) { log.info("Batch deleting employees: {}", ids); empService.delete(ids); return Result.success(); }
Mapper XML:
1 2 3 4 5 6
<deleteid="delete"> delete from emp where id in <foreachcollection="ids"item="id"open="("separator=","close=")"> #{id} </foreach> </delete>
ForEach Details:
collection: name of the list/array parameter. Can be list, array, or a custom name if annotated with @Param. item: alias for each element during iteration. open, close: strings added before and after the whole collection. separator: string between each iteration. You can also use for batch inserts:
@DeleteMapping("/{id}") @Log("Delete department operation") public Result delete(@PathVariable Integer id) { ... }
Pitfall Diary: Bugs I Encountered
Pitfall 1: @RequestBody Misuse Problem: Using @RequestBody with GET requests. Reason: GET has no body. Solution: Remove @RequestBody; use @RequestParam or path variables.
Pitfall 2: SQL Injection Vulnerability Problem (unsafe):
1
@Select("select * from emp where name like '%" + name + "%'")
Solution (safe) prevent sql injection:
1
@Select("select * from emp where name like concat('%', #{name}, '%')")
Pitfall 3: Transaction Not Rolling Back Problem: Checked exceptions don’t trigger rollback. Solution: Add @Transactional(rollbackFor = Exception.class).
Pitfall 4: File Upload Path Issues Problem: Using relative paths leads to “File not found”. Solution: Use absolute paths or configure in application properties.
Pitfall 5: Forgetting @RequestParam Default Values Problem: When page or pageSize are not provided, you get null and cause errors. Solution: Use @RequestParam(defaultValue = “1”) to set defaults.
Pitfall 6: Mismatch Between XML Alias and Interface Return Type Problem: In MyBatis XML, if you use AS aliases but the result type doesn’t match the property names, you’ll get null values. Solution: Ensure aliases match the Java property names exactly, or use resultMap for explicit mapping. (See Advanced MyBatis Mapping)
Advanced: Statistics & Complex Queries
In real projects, you often need to generate statistical reports. This section covers how to design and implement statistics modules using MyBatis, including handling dynamic columns, using CASE WHEN, and returning List<Map<String, Object>>.
Understanding List<Map<String, Object>> in MyBatis
When the structure of the result is not fixed (e.g., dynamic columns), you cannot use a predefined POJO. MyBatis allows you to return a List<Map<String, Object>>, where each map represents a row with column names as keys and column values as objects.
<selectid="getEmployeeStatistics"resultType="java.util.Map"> select dept.name as departmentName, count(*) as employeeCount, avg(age) as averageAge from emp join dept on emp.dept_id = dept.id <where> <iftest="deptId != null"> dept.id = #{deptId} </if> </where> group by dept.id </select>
Important:
Use resultType=”java.util.Map” (or the fully qualified name).
MyBatis will automatically create a Map for each row, with column names (or aliases) as keys.
This is extremely flexible for reports and charts where the number and names of columns vary.
Using CASE WHEN in SQL for Conditional Aggregation
You can use CASE WHEN inside SQL to create conditional counts or sums.
Example: Count employees by gender per department.
1 2 3 4 5 6 7 8 9
<selectid="countByGenderPerDept"resultType="java.util.Map"> select dept.name as deptName, sum(case when emp.gender = 1 then 1 else 0 end) as maleCount, sum(case when emp.gender = 2 then 1 else 0 end) as femaleCount from emp join dept on emp.dept_id = dept.id group by dept.id </select>
Handling Date Formats in Query Results
When returning dates, ensure they are formatted properly for the frontend. You can either:
Format in SQL using DATE_FORMAT(create_time, ‘%Y-%m-%d’) as createDate
Or let MyBatis map to LocalDate and then format in the service/controller.
Strict Date Format Matching: If your frontend expects a specific format (e.g., “yyyy-MM-dd”), make sure to either format in SQL or use Jackson annotations (@JsonFormat) on your POJO.
MyBatis Alias Strictness
In XML, when you use AS aliases, they must exactly match the property names of the target Java type (if using resultType) or the column names defined in resultMap. For Map results, aliases become the map keys, so they should be consistent with what the frontend expects.
Example:
1 2 3 4 5 6
<selectid="getSummary"resultType="map"> select count(*) as totalEmployees, avg(age) as avgAge from emp </select>
The resulting map will have keys “totalEmployees” and “avgAge”.
Using for Dynamic IN Clauses in Statistics
You might need to filter by multiple department IDs:
1 2 3 4 5 6 7 8 9
<selectid="getStatsForDepts"resultType="map"> select dept_id, count(*) as cnt from emp where dept_id in <foreachcollection="deptIds"item="id"open="("separator=","close=")"> #{id} </foreach> group by dept_id </select>
LocalDate Comparison in Java (if needed)
Although we usually let SQL handle date comparisons, sometimes you need to compare LocalDate in Java:
1 2 3
if (entryDate.isAfter(LocalDate.now())) { // future date logic }
Or compare two LocalDate objects:
1 2
if (date1.isEqual(date2)) { ... } if (date1.isBefore(date2)) { ... }
<selectid="getEmployeeTrend"resultType="map"> select month(entrydate) as month, count(*) as newHires from emp where year(entrydate) = #{year} group by month(entrydate) order by month </select>
Service:
1 2 3
public List<Map<String, Object>> getEmployeeTrend(Integer year) { return statsMapper.getEmployeeTrend(year); }
Controller:
1 2 3 4 5
@GetMapping("/stats/trend") public Result trend(@RequestParam(required = false) Integer year) { if (year == null) year = LocalDate.now().getYear(); return Result.success(statsService.getEmployeeTrend(year)); }
This returns data that can be directly used by chart libraries.