MyBatis是一款优秀的持久层框架,它通过XML或注解的方式将SQL与Java代码解耦,极大地简化了数据库操作。与传统的JDBC相比,MyBatis避免了大量重复的样板代码,让开发者能够更专注于业务逻辑的实现。
在实际项目中,我经常遇到需要频繁修改SQL的场景。使用原生JDBC时,每次修改SQL都需要重新编译Java代码,而MyBatis通过将SQL配置化,实现了热更新的效果。比如在性能优化时,可以随时调整SQL而无需重启应用,这对线上系统的维护特别重要。
这是MyBatis的核心工厂类,负责创建SqlSession实例。最佳实践是将其设计为单例,因为创建它的开销较大。在我的项目中,通常会这样初始化:
java复制String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
注意:SqlSessionFactoryBuilder用完即可丢弃,它的作用仅仅是构建SqlSessionFactory
这是执行CRUD操作的主要接口,每个线程都应该有自己的SqlSession实例。重要特性:
我习惯使用try-with-resources确保关闭:
java复制try(SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 业务操作...
}
Mapper是MyBatis的灵魂所在,它通过动态代理技术将接口方法与SQL映射关联。在实际开发中,我总结了几点经验:
selectActiveUsers比getUsers更明确List而非数组MyBatis的配置分为两个层次:
在团队协作中,我们会统一配置规范,比如:
com.example.entity包下XxxMapper命名规则经过多个项目的实践验证,我认为MyBatis最突出的优势在于:
对比其他ORM框架:
在Maven项目中,除了核心依赖外,我通常会添加这些实用组件:
xml复制<dependencies>
<!-- MyBatis核心 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.18</version>
</dependency>
<!-- 实用工具 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<optional>true</optional>
</dependency>
<!-- 测试支持 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.1.1</version>
</dependency>
</dependencies>
经验分享:Druid连接池相比HikariCP提供了更丰富的监控功能,适合需要详细监控SQL执行情况的场景
完整的mybatis-config.xml配置示例:
xml复制<?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="config/jdbc.properties"/>
<!-- 全局设置 -->
<settings>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 使用列别名替换列名 -->
<setting name="useColumnLabel" value="true"/>
<!-- 开启驼峰命名转换 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 日志实现 -->
<setting name="logImpl" value="SLF4J"/>
</settings>
<!-- 类型别名 -->
<typeAliases>
<package name="com.example.entity"/>
</typeAliases>
<!-- 类型处理器 -->
<typeHandlers>
<typeHandler handler="com.example.handler.JsonTypeHandler"/>
</typeHandlers>
<!-- 环境配置 -->
<environments default="dev">
<environment id="dev">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<!-- 连接池配置 -->
<property name="poolMaximumActiveConnections" value="20"/>
<property name="poolMaximumIdleConnections" value="10"/>
</dataSource>
</environment>
</environments>
<!-- 映射器配置 -->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
<package name="com.example.mapper"/>
</mappers>
</configuration>
关键配置说明:
settings中的mapUnderscoreToCamelCase可以自动将数据库的user_name映射到Java的userNamedefault属性切换现代项目大多基于SpringBoot,这是我的典型配置类:
java复制@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 配置实体类包路径
sessionFactory.setTypeAliasesPackage("com.example.entity");
// 配置mapper.xml文件位置
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/*.xml"));
// 自定义配置
org.apache.ibatis.session.Configuration configuration =
new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(true);
configuration.setDefaultFetchSize(100);
configuration.setDefaultStatementTimeout(30);
sessionFactory.setConfiguration(configuration);
return sessionFactory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
避坑指南:如果遇到"Invalid bound statement (not found)"错误,通常是因为Mapper接口与XML文件没有正确匹配,检查
MapperScan和mapperLocations配置
生产环境中的数据库连接池配置建议:
properties复制# Druid连接池配置
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
spring.datasource.druid.max-wait=60000
spring.datasource.druid.time-between-eviction-runs-millis=60000
spring.datasource.druid.min-evictable-idle-time-millis=300000
spring.datasource.druid.validation-query=SELECT 1
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
spring.datasource.druid.filters=stat,wall,log4j
监控配置(Druid特有):
java复制@Bean
public ServletRegistrationBean<StatViewServlet> druidServlet() {
ServletRegistrationBean<StatViewServlet> reg = new ServletRegistrationBean<>();
reg.setServlet(new StatViewServlet());
reg.addUrlMappings("/druid/*");
// 白名单
reg.addInitParameter("allow", "127.0.0.1");
// 登录查看信息的账号密码
reg.addInitParameter("loginUsername", "admin");
reg.addInitParameter("loginPassword", "admin");
return reg;
}
良好的实体类设计是MyBatis高效使用的基础。这是我的典型User实体类:
java复制@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
private Long id;
private String username;
private String password;
private Integer age;
private String email;
private Integer status;
private Date createTime;
private Date updateTime;
// 关联对象
private List<Role> roles;
private Department department;
}
设计要点:
UserMapper接口的完整示例:
java复制public interface UserMapper {
// 插入并返回主键
@Insert("INSERT INTO user(username,password,email) VALUES(#{username},#{password},#{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
// 批量插入
int batchInsert(List<User> users);
// 根据ID查询
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
// 条件查询
List<User> selectByCondition(UserQuery query);
// 更新
@Update("UPDATE user SET username=#{username}, email=#{email} WHERE id=#{id}")
int update(User user);
// 删除
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteById(Long id);
// 分页查询
List<User> selectPage(UserQuery query, RowBounds rowBounds);
}
接口设计原则:
完整的UserMapper.xml示例:
xml复制<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
<!-- 基础结果映射 -->
<resultMap id="BaseResultMap" type="User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="age" property="age"/>
<result column="email" property="email"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<!-- 包含角色的扩展映射 -->
<resultMap id="WithRolesResultMap" type="User" extends="BaseResultMap">
<collection property="roles" ofType="Role">
<id column="role_id" property="id"/>
<result column="role_name" property="name"/>
</collection>
</resultMap>
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="list" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(username, password, email, create_time)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.username}, #{item.password}, #{item.email}, NOW())
</foreach>
</insert>
<!-- 条件查询 -->
<select id="selectByCondition" parameterType="UserQuery" resultMap="BaseResultMap">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
<if test="statusList != null and statusList.size() > 0">
AND status IN
<foreach collection="statusList" item="status" open="(" separator="," close=")">
#{status}
</foreach>
</if>
</where>
ORDER BY create_time DESC
</select>
<!-- 分页查询 -->
<select id="selectPage" resultMap="WithRolesResultMap">
SELECT u.*, r.id as role_id, r.name as role_name
FROM user u
LEFT JOIN user_role ur ON u.id = ur.user_id
LEFT JOIN role r ON ur.role_id = r.id
<where>
<if test="query.keyword != null">
AND (u.username LIKE CONCAT('%', #{query.keyword}, '%')
OR u.email LIKE CONCAT('%', #{query.keyword}, '%'))
</if>
</where>
</select>
</mapper>
XML编写技巧:
<include>重用SQL片段LIMIT,Oracle使用ROWNUMJOIN而非多次查询实际项目中,我通常采用混合模式:
java复制public interface UserMapper {
// 简单查询使用注解
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
// 复杂查询使用XML
List<User> selectComplexQuery(UserQuery query);
// 动态SQL使用XML
int updateSelective(User user);
}
对应的XML配置:
xml复制<update id="updateSelective" parameterType="User">
UPDATE user
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
update_time=NOW()
</set>
WHERE id=#{id}
</update>
混合使用原则:
基础if条件判断:
xml复制<select id="searchUsers" resultType="User">
SELECT * FROM user
<where>
<if test="name != null and name != ''">
AND username LIKE CONCAT('%', #{name}, '%')
</if>
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
<if test="statusList != null and statusList.size() > 0">
AND status IN
<foreach collection="statusList" item="status" open="(" separator="," close=")">
#{status}
</foreach>
</if>
</where>
</select>
经验之谈:在字符串判断时,除了检查null还要检查空字符串,避免SQL语法错误
xml复制<select id="selectUsers" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="ids != null and ids.size() > 0">
id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</when>
<when test="name != null and name != ''">
username LIKE CONCAT('%', #{name}, '%')
</when>
<otherwise>
status = 1
</otherwise>
</choose>
</where>
</select>
适用场景:
批量插入的三种方式对比:
xml复制<insert id="batchInsert">
INSERT INTO user(username, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.email})
</foreach>
</insert>
xml复制<insert id="batchInsertWithId" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user(username, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.email})
</foreach>
</insert>
xml复制<update id="batchUpdate">
<foreach collection="list" item="user" separator=";">
UPDATE user
SET username = #{user.username},
email = #{user.email}
WHERE id = #{user.id}
</foreach>
</update>
性能提示:MySQL的批量插入建议每批500-1000条,过大可能导致包大小超限
处理Map类型的参数:
xml复制<select id="selectByUserMap" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="userMap.keys" item="key" open="(" separator="," close=")">
#{key}
</foreach>
AND status IN
<foreach collection="userMap.values" item="value" open="(" separator="," close=")">
#{value.status}
</foreach>
</select>
xml复制<sql id="Base_Column_List">
id, username, email, age, status
</sql>
<sql id="Where_Clause">
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</sql>
xml复制<select id="selectByExample" resultType="User">
SELECT
<include refid="Base_Column_List"/>
FROM user
<include refid="Where_Clause"/>
ORDER BY id DESC
</select>
xml复制<select id="selectOptimized" resultType="User">
SELECT * FROM user
<where>
<!-- 等值条件放前面 -->
<if test="status != null">
AND status = #{status}
</if>
<!-- 范围条件放后面 -->
<if test="createTimeStart != null">
AND create_time >= #{createTimeStart}
</if>
<!-- 模糊查询放最后 -->
<if test="keyword != null">
AND username LIKE CONCAT(#{keyword}, '%')
</if>
</where>
LIMIT 1000
</select>
xml复制<select id="countByExample" resultType="int">
SELECT COUNT(*) FROM user
<include refid="Where_Clause"/>
</select>
xml复制<select id="searchWithBind" resultType="User">
<bind name="pattern" value="'%' + keyword + '%'"/>
SELECT * FROM user
WHERE username LIKE #{pattern}
AND status = #{status}
</select>
一级缓存(SqlSession级别)的特点:
缓存验证代码示例:
java复制try(SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 第一次查询,访问数据库
User user1 = mapper.selectById(1L);
// 第二次查询,从缓存获取
User user2 = mapper.selectById(1L);
System.out.println(user1 == user2); // 输出true
// 执行更新操作
mapper.updateUsername(1L, "newname");
// 第三次查询,缓存已清空,访问数据库
User user3 = mapper.selectById(1L);
System.out.println(user1 == user3); // 输出false
}
启用二级缓存的步骤:
xml复制<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
xml复制<mapper namespace="com.example.mapper.UserMapper">
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
...
</mapper>
缓存参数说明:
eviction:缓存回收策略(LRU/FIFO/SOFT/WEAK)flushInterval:刷新间隔(毫秒)size:缓存对象数量readOnly:是否只读java复制public class User implements Serializable {
// ...
}
重要提示:二级缓存是跨SqlSession的,更新操作必须及时刷新缓存,否则会导致数据不一致
xml复制<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
xml复制<resultMap id="UserWithDetail" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
<!-- 延迟加载用户详情 -->
<association property="userDetail" column="id"
select="com.example.mapper.UserDetailMapper.selectByUserId"
fetchType="lazy"/>
</resultMap>
xml复制<insert id="batchInsert" parameterType="list">
INSERT INTO user(username, email) VALUES
<foreach collection="list" item="item" separator=",">
(#{item.username}, #{item.email})
</foreach>
</insert>
java复制try(SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for(int i=0; i<1000; i++) {
User user = new User("user"+i, "user"+i+"@example.com");
mapper.insert(user);
if(i % 200 == 0) {
session.flushStatements(); // 分批提交
}
}
session.commit();
}
性能对比:
xml复制<update id="batchUpdate">
UPDATE user
SET username = CASE id
<foreach collection="list" item="user">
WHEN #{user.id} THEN #{user.username}
</foreach>
END,
email = CASE id
<foreach collection="list" item="user">
WHEN #{user.id} THEN #{user.email}
</foreach>
END
WHERE id IN
<foreach collection="list" item="user" open="(" separator="," close=")">
#{user.id}
</foreach>
</update>
xml复制<insert id="batchInsertOrUpdate">
INSERT INTO user(id, username, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.id}, #{user.username}, #{user.email})
</foreach>
ON DUPLICATE KEY UPDATE
username = VALUES(username),
email = VALUES(email)
</insert>
处理JSON类型的示例:
java复制@MappedTypes({Map.class, List.class})
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonTypeHandler extends BaseTypeHandler<Object> {
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
Object parameter, JdbcType jdbcType) {
try {
ps.setString(i, mapper.writeValueAsString(parameter));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public Object getNullableResult(ResultSet rs, String columnName) {
try {
String json = rs.getString(columnName);
return json == null ? null : mapper.readValue(json, Object.class);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 其他重载方法...
}
xml复制<typeHandlers>
<typeHandler handler="com.example.handler.JsonTypeHandler"/>
</typeHandlers>
java复制public class User {
private Long id;
private String username;
private Map<String, Object> attributes; // 存储为JSON
}
java复制public class StatusEnumTypeHandler extends BaseTypeHandler<StatusEnum> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
StatusEnum parameter, JdbcType jdbcType) {
ps.setInt(i, parameter.getCode());
}
@Override
public StatusEnum getNullableResult(ResultSet rs, String columnName) {
int code = rs.getInt(columnName);
return StatusEnum.fromCode(code);
}
// 其他重载方法...
}
xml复制<typeHandlers>
<typeHandler handler="com.example.handler.StatusEnumTypeHandler"
javaType="com.example.enums.StatusEnum"/>
</typeHandlers>
xml复制<select id="selectEssentialFields" resultType="User">
SELECT id, username, email FROM user
WHERE status = 1
</select>
xml复制<select id="selectWithIndexHint" resultType="User">
SELECT /*+ INDEX(user idx_username) */ * FROM user
WHERE username = #{username}
</select>
xml复制<select id="selectPageOptimized" resultType="User">
SELECT u.* FROM user u
JOIN (
SELECT id FROM user
WHERE status = 1
ORDER BY create_time DESC
LIMIT #{offset}, #{limit}
) AS tmp ON u.id = tmp.id
</select>
通过MyBatis日志分析SQL执行:
xml复制<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
典型执行计划问题:
xml复制<settings>
<!-- 关闭不需要的自动映射 -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<!-- 设置默认执行器类型 -->
<setting name="defaultExecutorType" value="REUSE"/>
<!-- 设置JDBC超时时间 -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- 设置获取结果集时的批量大小 -->
<setting name="defaultFetchSize" value="100"/>
</settings>
Druid推荐配置:
properties复制# 初始连接数
spring.datasource.druid.initial-size=5
# 最小空闲连接数
spring.datasource.druid.min-idle=5
# 最大活跃连接数
spring.datasource.druid.max-active=20
# 获取连接等待超时时间(ms)
spring.datasource.druid.max-wait=60000
# 间隔多久检测空闲连接(ms)
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 连接最小生存时间(ms)
spring.datasource.druid.min-evictable-idle-time-millis=300000
# 测试连接有效性的SQL
spring.datasource.druid.validation-query=SELECT 1
# 空闲时测试连接
spring.datasource.druid.test-while-idle=true
java复制@Bean
public ServletRegistrationBean<StatViewServlet> druidServlet() {
ServletRegistrationBean<StatViewServlet> reg = new ServletRegistrationBean<>();
reg.setServlet(new StatViewServlet());
reg.addUrlMappings("/druid/*");
reg.addInitParameter("loginUsername", "admin");
reg.addInitParameter("loginPassword", "admin");
return reg;
}
xml复制<select id="searchProducts" resultType="ProductVO">
SELECT
p.id,
p.name,
p.price,
p.stock,
c.name AS category_name,
b.name AS brand_name,
(SELECT COUNT(*) FROM product_comment pc WHERE pc.product_id = p.id) AS comment_count
FROM product p
LEFT JOIN category c ON p.category_id = c.id
LEFT JOIN brand b ON p.brand_id = b.id
<where>
p.status = 1
<if test="keyword != null and keyword != ''">
AND (p.name LIKE CONCAT('%', #{keyword}, '%')
OR p.keywords LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test="categoryId != null">
AND (p.category_id = #{categoryId} OR c.parent_id = #{categoryId})
</if>
<if test="brandId != null">
AND p.brand_id = #{brandId}
</if>
<if test="minPrice != null">
AND p.price >= #{minPrice}
</if>
<if test="maxPrice != null">
AND p.price <= #{maxPrice}
</if>
<if test="hasStock != null and hasStock">
AND p.stock > 0
</if>
</where>
<choose>
<when test="sortBy == 'price_asc'">ORDER BY p.price ASC</when>
<when test="sortBy == 'price_desc'">ORDER BY p.price DESC</when>
<when test="sortBy == 'sales'">ORDER BY p.sales DESC</when>
<when test="sortBy == 'comment'">ORDER BY comment_count DESC</when>
<otherwise>ORDER BY p.create_time DESC</otherwise>
</choose>
LIMIT #{offset}, #{limit}
</select>
xml复制<!-- 使用二级缓存 -->
<cache eviction="LRU" flushInterval="3600000