第一次接触MyBatis时,我被XML配置搞得晕头转向。记得有次为了改一个简单的查询条件,我在XML文件和Java代码之间来回切换了十几次。这种开发体验让我开始寻找更优雅的解决方案,直到发现了@*Provider系列注解。
传统XML配置的主要痛点在于:SQL语句分散在外部文件中,与业务代码分离;修改时需要频繁切换文件;复杂的动态SQL需要写大量
我做过一个对比实验:同样实现多条件用户查询功能,XML方式需要38行配置,而@SelectProvider仅需15行Java代码。更重要的是,当业务需求变更时,注解方式能让我在同一个类中完成所有修改,再也不用担心"改漏了XML文件"的情况。
让我们从一个真实案例开始。假设我们需要根据动态条件查询用户列表,包括用户名模糊搜索、博客地址精确匹配和注册时间范围查询。使用@SelectProvider的实现如下:
java复制@SelectProvider(type = UserQueryProvider.class, method = "buildQuerySql")
List<User> queryUsers(@Param("name") String name,
@Param("blogUrl") String blogUrl,
@Param("startTime") Date startTime,
@Param("endTime") Date endTime);
对应的SQL构建类应该这样写:
java复制public class UserQueryProvider {
public String buildQuerySql(Map<String, Object> params) {
return new SQL() {{
SELECT("*");
FROM("tb_user");
if (params.get("name") != null) {
WHERE("user_name LIKE CONCAT('%', #{name}, '%')");
}
if (params.get("blogUrl") != null) {
WHERE("blog_url = #{blogUrl}");
}
if (params.get("startTime") != null && params.get("endTime") != null) {
WHERE("create_time BETWEEN #{startTime} AND #{endTime}");
}
ORDER_BY("user_id DESC");
}}.toString();
}
}
这里有几个实用技巧:
对于@InsertProvider,我推荐这种写法来处理自增主键:
java复制@InsertProvider(type = UserSqlBuilder.class, method = "buildInsertSql")
@Options(useGeneratedKeys = true, keyProperty = "userId")
int insertUser(User user);
对应的SQL构建方法:
java复制public String buildInsertSql(User user) {
return new SQL() {{
INSERT_INTO("tb_user");
INTO_COLUMNS("user_name", "blog_url");
INTO_VALUES("#{userName}", "#{blogUrl}");
if (user.getRemark() != null) {
INTO_COLUMNS("blog_remark");
INTO_VALUES("#{blogRemark}");
}
}}.toString();
}
这种写法的优势在于:
当遇到非常复杂的查询条件时,我总结出两种处理方案:
第一种是分步构建法:
java复制public String buildComplexQuery(Map<String, Object> params) {
SQL sql = new SQL().SELECT("*").FROM("tb_user");
if (params.get("type") != null) {
sql.WHERE("user_type = #{type}");
}
// 添加更多条件...
// 最后处理排序
String orderBy = (String) params.get("orderBy");
if ("name".equals(orderBy)) {
sql.ORDER_BY("user_name");
} else {
sql.ORDER_BY("create_time DESC");
}
return sql.toString();
}
第二种是使用Criteria模式:
java复制public class UserQueryBuilder {
private final List<String> conditions = new ArrayList<>();
public UserQueryBuilder withNameLike(String name) {
if (name != null) {
conditions.add("user_name LIKE CONCAT('%', #{name}, '%')");
}
return this;
}
public String build() {
return new SQL() {{
SELECT("*");
FROM("tb_user");
if (!conditions.isEmpty()) {
WHERE(conditions.toArray(new String[0]));
}
}}.toString();
}
}
在使用动态SQL时,安全问题是重中之重。我踩过的坑包括:
绝对不要用字符串拼接方式构造SQL:
java复制// 错误示范!存在SQL注入风险
String sql = "SELECT * FROM users WHERE id = " + userId;
正确做法是始终使用预编译参数:
java复制// 正确做法
WHERE("user_id = #{userId}");
对于LIKE查询,应该这样处理:
java复制WHERE("user_name LIKE CONCAT('%', #{name}, '%')");
对于IN查询,可以使用MyBatis的
java复制public String buildQueryWithIds(@Param("ids") List<Integer> ids) {
SQL sql = new SQL().SELECT("*").FROM("tb_user");
if (ids != null && !ids.isEmpty()) {
String inClause = ids.stream()
.map(id -> "#{ids[" + (ids.indexOf(id)) + "]}")
.collect(Collectors.joining(","));
sql.WHERE("user_id IN (" + inClause + ")");
}
return sql.toString();
}
在Spring Boot项目中使用@*Provider注解时,我推荐这种配置方式:
首先在pom.xml中添加依赖:
xml复制<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
配置application.yml:
yaml复制mybatis:
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true
建议的Mapper接口结构:
java复制@Mapper
public interface UserMapper {
@SelectProvider(type = UserSqlBuilder.class, method = "findById")
User findById(@Param("id") Long id);
// SQL构建器作为内部类
class UserSqlBuilder {
public String findById() {
return new SQL() {{
SELECT("*");
FROM("users");
WHERE("id = #{id}");
}}.toString();
}
}
}
在实际项目中,我遇到过这些问题和解决方案:
参数绑定失败:
动态条件失效:
性能问题:
分页查询优化:
java复制public String buildPageQuery(@Param("condition") UserQuery condition,
@Param("pageable") Pageable pageable) {
return new SQL() {{
SELECT("*");
FROM("tb_user");
// 动态条件...
ORDER_BY(pageable.getSort().toString().replace(":", " "));
}}.toString() + " LIMIT #{pageable.pageSize} OFFSET #{pageable.offset}";
}
多表关联查询处理:
java复制public String buildUserWithPostsQuery(@Param("userId") Long userId) {
return new SQL() {{
SELECT("u.*, p.title as post_title");
FROM("users u");
LEFT_OUTER_JOIN("posts p ON u.id = p.user_id");
WHERE("u.id = #{userId}");
}}.toString();
}
经过多个项目的实践验证,我发现注解方式特别适合中等复杂度的动态SQL场景。当SQL极其复杂时,可以考虑混合使用XML和注解,但这种情况在实际开发中并不多见。最重要的是建立团队统一的编码规范,确保所有成员都能理解和维护这种风格的代码。