第一次接触MyBatis是在2013年的一个电商项目里,当时团队正从纯JDBC转向ORM框架。还记得那个被JDBC样板代码折磨的下午,我在调试一个简单的用户查询功能时,发现近80%的代码都在处理连接、语句和结果集。这种体验让我开始寻找更高效的解决方案,而MyBatis以其独特的"SQL友好"特性进入了我的视野。
MyBatis本质上是一个将JDBC操作标准化的持久层框架。与Hibernate等全自动ORM不同,它采用半自动化方式,让你既能享受自动映射的便利,又能完全掌控SQL语句。这种设计特别适合需要精细优化SQL性能的场景,也是它能在Java持久层生态中占据重要位置的原因。
让我们通过一个查询用户信息的典型场景,看看MyBatis内部如何运作:
xml复制<!-- 典型数据源配置 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="poolMaximumActiveConnections" value="10"/>
</dataSource>
</environment>
</environments>
SQL映射处理:Mapper XML文件(如UserMapper.xml)包含具体的SQL定义。MyBatis会将这些语句解析为MappedStatement对象,每个语句都有唯一的namespace+id标识。
会话生命周期:通过SqlSessionFactory创建SqlSession,这是最核心的交互接口。重要提示:SqlSession是非线程安全的,应该每次请求创建新会话,用完后立即关闭。
| 组件 | 职责 | 实践经验 |
|---|---|---|
| SqlSessionFactoryBuilder | 根据配置构建工厂实例 | 通常作为方法局部变量使用 |
| SqlSessionFactory | 生产SqlSession的工厂 | 应用生命周期内保持单例 |
| SqlSession | 执行CRUD操作的一线接口 | 每个线程独立实例 |
| Executor | SQL语句实际执行者 | 分为简单、重用和批处理三种类型 |
| MappedStatement | 封装SQL语句信息 | 对应mapper文件中的每个SQL节点 |
特别提醒:在Spring集成环境中,MyBatis-Spring模块会接管大部分组件管理,开发者通常只需关注Mapper接口定义。
回忆早期用纯JDBC查询用户表的代码:
java复制// 典型JDBC样板代码
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
ps = conn.prepareStatement("SELECT * FROM users WHERE id=?");
ps.setInt(1, userId);
rs = ps.executeQuery();
User user = new User();
while(rs.next()) {
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
// 更多字段设置...
}
return user;
} finally {
// 三级嵌套关闭资源
if(rs != null) try { rs.close(); } catch(SQLException e) { /* 日志记录 */ }
if(ps != null) try { ps.close(); } catch(SQLException e) { /* 日志记录 */ }
if(conn != null) try { conn.close(); } catch(SQLException e) { /* 日志记录 */ }
}
这种编码方式存在三大问题:
同样的功能用MyBatis实现:
xml复制<!-- UserMapper.xml -->
<select id="selectUser" resultType="com.example.User">
SELECT * FROM users WHERE id = #{userId}
</select>
java复制// Java调用代码
try(SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
return mapper.selectUser(userId);
}
优势对比:
虽然文档建议优先使用#{},但有些场景必须用${}:
xml复制<!-- 动态表名查询 -->
<select id="selectByTable" resultType="map">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>
安全警示:使用${}时必须严格验证输入,我曾见过因直接拼接用户输入导致SQL注入的案例。建议采用白名单校验,比如对于表名参数,应该检查是否在预期表名集合中。
除了简单的resultType,MyBatis支持更精细的resultMap:
xml复制<resultMap id="userResultMap" type="User">
<id property="id" column="user_id"/>
<result property="username" column="user_name"/>
<association property="department" javaType="Department">
<id property="id" column="dept_id"/>
</association>
</resultMap>
高级技巧:
<constructor>标签支持不可变对象<discriminator>实现继承映射@MapKey注解将结果集转为MapMyBatis提供了强大的动态SQL能力:
xml复制<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="roles != null and roles.size() > 0">
AND role IN
<foreach item="role" collection="roles"
open="(" separator="," close=")">
#{role}
</foreach>
</if>
</where>
ORDER BY
<choose>
<when test="orderBy == 'name'">name</when>
<otherwise>id</otherwise>
</choose>
</select>
MyBatis提供两级缓存:
一级缓存:SqlSession级别,默认开启
二级缓存:Mapper级别,需要显式配置
xml复制<cache eviction="LRU" flushInterval="60000" size="512"/>
配置要点:
对比三种批处理方式:
| 方式 | 实现 | 特点 | 适用场景 |
|---|---|---|---|
| 简单批处理 | 循环执行多条SQL | 实现简单 | 小批量数据 |
| BatchExecutor | openSession(ExecutorType.BATCH) | 预编译一次,多次执行 | 大批量相同操作 |
| Bulk Insert | 单条SQL执行 | MySQL等支持大批量插入的数据库 |
示例代码:
java复制try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for(int i=0; i<1000; i++) {
mapper.insert(new User("user"+i));
if(i % 200 == 0) {
session.flushStatements(); // 分段提交
}
}
session.commit();
}
BindingException:
TooManyResultsException:
ResultMap异常:
xml复制<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
在开发环境建议开启完整日志:
xml复制<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
或者结合Log4j配置:
properties复制log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
虽然注解方式更简洁:
java复制@Select("SELECT * FROM users WHERE id = #{id}")
User selectUser(int id);
但复杂查询仍推荐XML方式:
对于简单CRUD,可以考虑MyBatis-Plus:
java复制public interface UserMapper extends BaseMapper<User> {
// 自动获得各种基础方法
}
// 条件构造器示例
QueryWrapper<User> query = new QueryWrapper<>();
query.like("name", "张").between("age", 20, 30);
userMapper.selectList(query);
选择建议:
SQL编写规范:
Mapper接口设计原则:
事务管理要点:
java复制try(SqlSession session = sqlSessionFactory.openSession()) {
try {
mapper.insert(user);
mapper.updateLog(user.getId());
session.commit(); // 显式提交
} catch(Exception e) {
session.rollback(); // 异常回滚
throw e;
}
}
MyBatis的成功在于它把握住了两个平衡点:
这种设计使得它特别适合:
十年间,我见证了许多项目从其他ORM转向MyBatis,核心原因往往都是:"我们需要更透明的SQL控制"。这也提醒我们,技术选型没有银弹,理解框架背后的设计理念比单纯会用更重要。