1. MyBatis 入门:ORM 思想解析
第一次接触 MyBatis 时,我完全被它的 SQL 映射文件搞懵了——这玩意儿跟 JDBC 有什么区别?直到真正理解了 ORM 思想,才发现 MyBatis 的设计哲学就藏在这三个字母里。ORM(Object-Relational Mapping)的本质是解决面向对象编程与关系型数据库之间的阻抗失配问题。
举个实际开发中的例子:当我们需要从用户表查询数据时,传统 JDBC 需要手动处理 ResultSet:
java复制while(rs.next()){
User user = new User();
user.setId(rs.getInt("id"));
user.setName(rs.getString("name"));
// 更多字段...
}
而 MyBatis 通过 XML 或注解的映射配置,自动完成这种对象关系转换。但要注意的是,MyBatis 属于"半自动化"ORM,与 Hibernate 的全自动映射不同,它把 SQL 的控制权完全交给了开发者。
关键理解:MyBatis 的核心价值不在于完全隐藏 SQL,而是提供优雅的 SQL 与 Java 对象的映射机制。这种设计特别适合需要精细控制 SQL 的场景。
2. 核心配置全解
2.1 配置文件层级结构
一个完整的 MyBatis 配置通常包含三个层级:
- 主配置文件(mybatis-config.xml)
- Mapper 接口(UserMapper.java)
- SQL 映射文件(UserMapper.xml)
我习惯用以下目录结构组织:
code复制src/main/resources
├── mybatis-config.xml
└── com/example/mapper
└── UserMapper.xml
2.2 主配置关键项
主配置文件中这些配置项最容易被忽视但至关重要:
xml复制<settings>
<!-- 下划线转驼峰 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 查询超时时间(秒) -->
<setting name="defaultStatementTimeout" value="30"/>
</settings>
2.3 数据源配置陷阱
连接池配置直接影响系统性能。生产环境推荐这样配置 Druid:
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/test"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<!-- 关键参数 -->
<property name="poolMaximumActiveConnections" value="20"/>
<property name="poolMaximumIdleConnections" value="10"/>
<property name="poolMaximumCheckoutTime" value="20000"/>
</dataSource>
</environment>
</environments>
3. SQL 映射实战技巧
3.1 动态 SQL 的妙用
MyBatis 的动态 SQL 是我最喜欢的功能之一。比如这个根据条件查询用户的例子:
xml复制<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name LIKE CONCAT('%',#{name},'%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<choose>
<when test="orderBy == 'name'">
ORDER BY name
</when>
<otherwise>
ORDER BY id
</otherwise>
</choose>
</where>
</select>
3.2 结果映射的高级用法
处理复杂结果集时,resultMap 比 resultType 更强大:
xml复制<resultMap id="userWithOrders" type="User">
<id property="id" column="user_id"/>
<result property="name" column="user_name"/>
<collection property="orders" ofType="Order">
<id property="id" column="order_id"/>
<result property="orderNo" column="order_no"/>
</collection>
</resultMap>
3.3 批处理性能优化
大批量插入数据时,这样配置可以提升10倍性能:
java复制try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for(int i=0; i<10000; i++){
mapper.insert(new User("user"+i));
if(i % 1000 == 0){
session.flushStatements();
}
}
session.commit();
}
4. 踩坑实录与解决方案
4.1 缓存导致的脏读
曾经遇到过一个诡异的问题:更新数据后查询结果不变。原因是 MyBatis 一级缓存(SqlSession级别)在作祟。解决方案:
- 在查询方法上添加
@Options(flushCache=true) - 或者直接
sqlSession.clearCache()
4.2 模糊查询的陷阱
这样的模糊查询在MySQL中会导致全表扫描:
xml复制WHERE name LIKE '%${name}%'
应该改用:
xml复制WHERE name LIKE CONCAT('%',#{name},'%')
4.3 分页查询的正确姿势
物理分页推荐使用 PageHelper:
java复制PageHelper.startPage(1, 10);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
注意:PageHelper 的 startPage 必须紧跟在查询方法前调用,中间不能有其他数据库操作。
5. 扩展应用场景
5.1 多数据源配置
Spring Boot 中这样配置多数据源:
java复制@Configuration
@MapperScan(basePackages = "com.primary.mapper",
sqlSessionTemplateRef = "primarySqlSessionTemplate")
public class PrimaryDataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.primary")
public DataSource primaryDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public SqlSessionFactory primarySqlSessionFactory(
@Qualifier("primaryDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/primary/*.xml"));
return bean.getObject();
}
}
5.2 类型处理器妙用
自定义类型处理器处理枚举:
java复制public class StatusEnumTypeHandler extends BaseTypeHandler<StatusEnum> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
StatusEnum parameter, JdbcType jdbcType) {
ps.setInt(i, parameter.getCode());
}
// 其他方法实现...
}
在配置中注册:
xml复制<typeHandlers>
<typeHandler handler="com.example.handler.StatusEnumTypeHandler"/>
</typeHandlers>
5.3 插件开发实战
实现一个SQL执行时间统计插件:
java复制@Intercepts(@Signature(type= StatementHandler.class,
method="query",
args={Statement.class, ResultHandler.class}))
public class SqlCostInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
return invocation.proceed();
} finally {
long cost = System.currentTimeMillis() - start;
StatementHandler handler = (StatementHandler)invocation.getTarget();
System.out.println("SQL执行耗时["+cost+"ms]:"
+ handler.getBoundSql().getSql());
}
}
}
6. 性能调优指南
6.1 连接池配置黄金法则
根据我们的压测经验,Druid 连接池建议这样配置:
properties复制# 初始连接数
spring.datasource.initialSize=5
# 最小空闲连接
spring.datasource.minIdle=5
# 最大活跃连接
spring.datasource.maxActive=20
# 获取连接超时时间(毫秒)
spring.datasource.maxWait=60000
# 连接有效性检测
spring.datasource.validationQuery=SELECT 1
spring.datasource.testWhileIdle=true
spring.datasource.timeBetweenEvictionRunsMillis=60000
6.2 二级缓存优化策略
MyBatis 二级缓存(namespace级别)使用时要注意:
- 在映射文件中开启缓存:
xml复制<cache eviction="LRU" flushInterval="60000" size="1024"/>
- 实体类必须实现 Serializable
- 事务提交后才会更新缓存
6.3 SQL 编写规范
经过多个项目总结出的SQL编写规范:
- 禁止使用
SELECT *,必须明确列出字段 - 多表关联不超过3个表
- 批量操作使用
<foreach>标签 - 结果集超过1000条必须分页
7. 最佳实践总结
经过多个项目的实战,我总结出这些 MyBatis 最佳实践:
-
XML vs 注解:简单SQL用注解,复杂SQL用XML。我习惯把超过2个条件的查询都放到XML中。
-
结果映射:优先使用resultMap而不是resultType,虽然要多写几行配置,但后期维护方便很多。
-
动态SQL:
<where>标签会自动处理前缀AND,比手动写WHERE 1=1更优雅。 -
批处理:插入超过100条数据时,一定要用Batch模式,性能差异可以达到数量级。
-
插件开发:自定义插件是扩展MyBatis的利器,可以用来实现数据脱敏、SQL审计等功能。
最后分享一个真实案例:在某电商项目中,通过优化MyBatis配置和SQL写法,将订单查询接口的响应时间从800ms降低到了120ms。关键点在于:
- 使用了正确的resultMap映射
- 启用了二级缓存
- 优化了动态SQL的写法
- 添加了合适的数据库索引