七、Spring 集成
7.1 配置集成
Maven 依赖:
xml
<dependencies>
<!-- MyBatis Spring 集成 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<!-- Spring 相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.30</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.30</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
</dependencies>
Spring XML 配置:
xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
xsi:schemaLocation="...">
<!-- 配置数据源 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置 SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:mapper/**/*.xml"/>
<property name="typeAliasesPackage" value="com.example.entity"/>
</bean>
<!-- 配置 Mapper 扫描 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.example.mapper"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启注解事务 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
Spring Boot 配置:
yaml
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mybatis_demo
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
initial-size: 5
max-active: 20
min-idle: 5
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
lazy-loading-enabled: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
Spring Boot 配置类:
java
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return DruidDataSourceBuilder.create().build();
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
// 配置 MyBatis 设置
org.apache.ibatis.session.Configuration configuration =
new org.apache.ibatis.session.Configuration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(true);
configuration.setLazyLoadingEnabled(true);
sessionFactory.setConfiguration(configuration);
// 设置类型别名包
sessionFactory.setTypeAliasesPackage("com.example.entity");
// 设置 mapper 位置
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver()
.getResources("classpath:mapper/**/*.xml"));
return sessionFactory.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
7.2 事务管理
java
@Service
@Transactional
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private LogMapper logMapper;
// 事务传播行为
@Transactional(propagation = Propagation.REQUIRED)
public void createUser(User user) {
userMapper.insert(user);
logMapper.insertLog("创建用户: " + user.getUsername());
// 手动回滚
if (user.getAge() < 0) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
// 只读事务
@Transactional(readOnly = true)
public User getUser(Long id) {
return userMapper.selectById(id);
}
// 指定回滚异常
@Transactional(rollbackFor = Exception.class, noRollbackFor = IllegalArgumentException.class)
public void updateUser(User user) {
userMapper.update(user);
if (user.getAge() > 150) {
throw new IllegalArgumentException("年龄无效");
}
}
// 事务超时
@Transactional(timeout = 30)
public void batchInsert(List<User> users) {
for (User user : users) {
userMapper.insert(user);
}
}
// 事务隔离级别
@Transactional(isolation = Isolation.REPEATABLE_READ)
public User getUserWithLock(Long id) {
return userMapper.selectForUpdate(id);
}
}
7.3 使用 SqlSessionTemplate
java
@Repository
public class UserDao {
@Autowired
private SqlSessionTemplate sqlSessionTemplate;
public User selectById(Long id) {
return sqlSessionTemplate.selectOne("com.example.mapper.UserMapper.selectById", id);
}
public List<User> selectAll() {
return sqlSessionTemplate.selectList("com.example.mapper.UserMapper.selectAll");
}
public void insert(User user) {
sqlSessionTemplate.insert("com.example.mapper.UserMapper.insert", user);
}
public void update(User user) {
sqlSessionTemplate.update("com.example.mapper.UserMapper.update", user);
}
public void delete(Long id) {
sqlSessionTemplate.delete("com.example.mapper.UserMapper.deleteById", id);
}
// 批量操作
public void batchInsert(List<User> users) {
sqlSessionTemplate.execute(new SqlSessionCallback<Void>() {
@Override
public Void doInSqlSession(SqlSession session) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user);
}
return null;
}
});
}
}
八、分页插件
8.1 PageHelper 使用
Maven 依赖:
xml
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
配置 PageHelper:
yaml
# application.yml
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
使用示例:
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 基础分页
public PageInfo<User> getUsersByPage(int pageNum, int pageSize) {
// 在查询前设置分页参数
PageHelper.startPage(pageNum, pageSize);
// 执行查询
List<User> users = userMapper.selectAll();
// 包装为 PageInfo
return new PageInfo<>(users);
}
// 带条件的分页
public PageInfo<User> searchUsers(String username, Integer age,
int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<User> users = userMapper.selectByCondition(username, age);
return new PageInfo<>(users);
}
// 排序分页
public PageInfo<User> getUsersOrderByAge(int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize, "age desc, id asc");
List<User> users = userMapper.selectAll();
return new PageInfo<>(users);
}
// 获取分页信息
public void testPageInfo() {
PageInfo<User> pageInfo = getUsersByPage(1, 10);
System.out.println("当前页: " + pageInfo.getPageNum());
System.out.println("每页大小: " + pageInfo.getPageSize());
System.out.println("总记录数: " + pageInfo.getTotal());
System.out.println("总页数: " + pageInfo.getPages());
System.out.println("第一页: " + pageInfo.getFirstPage());
System.out.println("最后一页: " + pageInfo.getLastPage());
System.out.println("是否有上一页: " + pageInfo.isHasPreviousPage());
System.out.println("是否有下一页: " + pageInfo.isHasNextPage());
// 获取数据
List<User> users = pageInfo.getList();
}
// 自定义分页参数
public PageInfo<User> getUsersWithCustomPage(PageParam pageParam) {
PageHelper.startPage(pageParam.getPage(), pageParam.getSize());
List<User> users = userMapper.selectByCondition(
pageParam.getUsername(), pageParam.getAge());
return new PageInfo<>(users);
}
// 使用 RowBounds 分页
public List<User> getUsersWithRowBounds(int offset, int limit) {
RowBounds rowBounds = new RowBounds(offset, limit);
return userMapper.selectByRowBounds(rowBounds);
}
}
8.2 自定义分页插件
java
@Intercepts({
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class CustomPageInterceptor implements Interceptor {
private String dialect = "mysql";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds) args[2];
// 判断是否需要分页
if (parameter instanceof Page) {
Page page = (Page) parameter;
// 获取原始 SQL
BoundSql boundSql = ms.getBoundSql(parameter);
String sql = boundSql.getSql();
// 查询总记录数
String countSql = getCountSql(sql);
Long total = getTotal(ms, parameter, countSql);
page.setTotal(total);
// 添加分页 SQL
String pageSql = getPageSql(sql, page);
// 创建新的 BoundSql
BoundSql newBoundSql = new BoundSql(
ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
// 创建新的 MappedStatement
MappedStatement newMs = copyMappedStatement(ms, newBoundSql);
args[0] = newMs;
}
return invocation.proceed();
}
private String getCountSql(String sql) {
return "SELECT COUNT(*) FROM (" + sql + ") temp_count";
}
private Long getTotal(MappedStatement ms, Object parameter, String countSql) {
// 执行 count 查询
// 实现省略
return 0L;
}
private String getPageSql(String sql, Page page) {
if ("mysql".equals(dialect)) {
return sql + " LIMIT " + page.getOffset() + "," + page.getLimit();
} else if ("oracle".equals(dialect)) {
return "SELECT * FROM (SELECT t.*, ROWNUM rn FROM (" + sql +
") t WHERE ROWNUM <= " + (page.getOffset() + page.getLimit()) +
") WHERE rn > " + page.getOffset();
}
return sql;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.dialect = properties.getProperty("dialect", "mysql");
}
}
九、代码生成器
9.1 MyBatis Generator 使用
Maven 插件配置:
xml
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version>
<configuration>
<configurationFile>src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
generatorConfig.xml:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="MySqlContext" targetRuntime="MyBatis3" defaultModelType="flat">
<!-- 注释生成器 -->
<commentGenerator>
<property name="suppressDate" value="true"/>
<property name="suppressAllComments" value="false"/>
<property name="addRemarkComments" value="true"/>
</commentGenerator>
<!-- 数据库连接 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
connectionURL="jdbc:mysql://localhost:3306/mybatis_demo"
userId="root"
password="123456">
<property name="useSSL" value="false"/>
</jdbcConnection>
<!-- Java类型解析器 -->
<javaTypeResolver>
<property name="forceBigDecimals" value="false"/>
<property name="useJSR310Types" value="true"/>
</javaTypeResolver>
<!-- 实体类配置 -->
<javaModelGenerator targetPackage="com.example.entity"
targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
<property name="immutable" value="false"/>
</javaModelGenerator>
<!-- Mapper XML 配置 -->
<sqlMapGenerator targetPackage="mapper"
targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!-- Mapper 接口配置 -->
<javaClientGenerator type="XMLMAPPER"
targetPackage="com.example.mapper"
targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!-- 表配置 -->
<table tableName="user" domainObjectName="User"
enableCountByExample="true"
enableDeleteByExample="true"
enableSelectByExample="true"
enableUpdateByExample="true">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
<columnOverride column="create_time" javaType="java.time.LocalDateTime"/>
<columnOverride column="update_time" javaType="java.time.LocalDateTime"/>
</table>
<table tableName="department" domainObjectName="Department">
<generatedKey column="id" sqlStatement="MySql" identity="true"/>
</table>
</context>
</generatorConfiguration>
9.2 自定义代码生成器
java
public class CustomCodeGenerator {
public static void main(String[] args) throws Exception {
// 配置信息
String jdbcUrl = "jdbc:mysql://localhost:3306/mybatis_demo";
String username = "root";
String password = "123456";
String packageName = "com.example";
String baseDir = "src/main/java";
// 创建连接
Connection conn = DriverManager.getConnection(jdbcUrl, username, password);
DatabaseMetaData metaData = conn.getMetaData();
// 获取所有表
ResultSet tables = metaData.getTables(null, null, "%", new String[]{"TABLE"});
while (tables.next()) {
String tableName = tables.getString("TABLE_NAME");
String className = toCamelCase(tableName, true);
// 生成实体类
generateEntity(conn, tableName, className, packageName, baseDir);
// 生成 Mapper 接口
generateMapper(className, packageName, baseDir);
// 生成 XML 映射文件
generateXml(conn, tableName, className, packageName, baseDir);
}
conn.close();
}
private static void generateEntity(Connection conn, String tableName,
String className, String packageName,
String baseDir) throws Exception {
// 获取列信息
ResultSet columns = conn.getMetaData().getColumns(null, null, tableName, null);
StringBuilder sb = new StringBuilder();
sb.append("package ").append(packageName).append(".entity;\n\n");
sb.append("import java.io.Serializable;\n");
sb.append("import java.time.LocalDateTime;\n\n");
sb.append("public class ").append(className).append(" implements Serializable {\n\n");
sb.append(" private static final long serialVersionUID = 1L;\n\n");
// 生成字段
List<ColumnInfo> columnInfos = new ArrayList<>();
while (columns.next()) {
String columnName = columns.getString("COLUMN_NAME");
String columnType = columns.getString("TYPE_NAME");
String propertyName = toCamelCase(columnName, false);
String javaType = toJavaType(columnType);
columnInfos.add(new ColumnInfo(columnName, propertyName, javaType));
sb.append(" private ").append(javaType).append(" ").append(propertyName).append(";\n");
}
// 生成 getter/setter
for (ColumnInfo col : columnInfos) {
sb.append("\n public ").append(col.javaType).append(" get")
.append(toCamelCase(col.propertyName, true)).append("() {\n");
sb.append(" return ").append(col.propertyName).append(";\n");
sb.append(" }\n\n");
sb.append(" public void set").append(toCamelCase(col.propertyName, true))
.append("(").append(col.javaType).append(" ").append(col.propertyName).append(") {\n");
sb.append(" this.").append(col.propertyName).append(" = ").append(col.propertyName).append(";\n");
sb.append(" }\n");
}
sb.append("}");
// 写入文件
String filePath = baseDir + "/" + packageName.replace(".", "/") +
"/entity/" + className + ".java";
Files.write(Paths.get(filePath), sb.toString().getBytes());
}
private static void generateMapper(String className, String packageName,
String baseDir) throws Exception {
StringBuilder sb = new StringBuilder();
sb.append("package ").append(packageName).append(".mapper;\n\n");
sb.append("import ").append(packageName).append(".entity.").append(className).append(";\n");
sb.append("import org.apache.ibatis.annotations.Mapper;\n");
sb.append("import org.apache.ibatis.annotations.Param;\n");
sb.append("import java.util.List;\n\n");
sb.append("@Mapper\n");
sb.append("public interface ").append(className).append("Mapper {\n\n");
sb.append(" ").append(className).append(" selectById(@Param(\"id\") Long id);\n\n");
sb.append(" List<").append(className).append("> selectAll();\n\n");
sb.append(" int insert(").append(className).append(" ").append(toCamelCase(className, false)).append(");\n\n");
sb.append(" int update(").append(className).append(" ").append(toCamelCase(className, false)).append(");\n\n");
sb.append(" int deleteById(@Param(\"id\") Long id);\n");
sb.append("}");
String filePath = baseDir + "/" + packageName.replace(".", "/") +
"/mapper/" + className + "Mapper.java";
Files.write(Paths.get(filePath), sb.toString().getBytes());
}
private static String toCamelCase(String name, boolean capitalize) {
StringBuilder result = new StringBuilder();
boolean upper = capitalize;
for (char c : name.toLowerCase().toCharArray()) {
if (c == '_') {
upper = true;
} else if (upper) {
result.append(Character.toUpperCase(c));
upper = false;
} else {
result.append(c);
}
}
return result.toString();
}
private static String toJavaType(String dbType) {
switch (dbType.toUpperCase()) {
case "INT":
case "INTEGER":
case "SMALLINT":
case "TINYINT":
return "Integer";
case "BIGINT":
return "Long";
case "VARCHAR":
case "CHAR":
case "TEXT":
return "String";
case "DATE":
case "TIMESTAMP":
case "DATETIME":
return "LocalDateTime";
case "DECIMAL":
case "NUMERIC":
return "java.math.BigDecimal";
default:
return "Object";
}
}
static class ColumnInfo {
String columnName;
String propertyName;
String javaType;
ColumnInfo(String columnName, String propertyName, String javaType) {
this.columnName = columnName;
this.propertyName = propertyName;
this.javaType = javaType;
}
}
}
十、最佳实践与优化
10.1 配置优化
xml
<configuration>
<settings>
<!-- 开启驼峰命名自动映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
<!-- 设置默认执行器类型 -->
<setting name="defaultExecutorType" value="REUSE"/>
<!-- 设置超时时间 -->
<setting name="defaultStatementTimeout" value="30"/>
<!-- 设置获取数据库自增主键值 -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 开启日志 -->
<setting name="logImpl" value="SLF4J"/>
<!-- 开启自动映射未知列 -->
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<!-- 开启局部缓存 -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 开启延迟加载触发方法 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>
</configuration>
10.2 SQL 性能优化
xml
<!-- 使用 ResultMap 代替 resultType -->
<resultMap id="optimizedMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username"/>
</resultMap>
<!-- 只查询需要的字段 -->
<select id="selectBasicInfo" resultMap="optimizedMap">
SELECT id, username FROM user WHERE id = #{id}
</select>
<!-- 使用 bind 避免 SQL 注入 -->
<select id="selectByUsername" resultType="User">
<bind name="pattern" value="'%' + username + '%'"/>
SELECT * FROM user WHERE username LIKE #{pattern}
</select>
<!-- 批量操作使用 foreach -->
<insert id="batchInsertOptimized">
INSERT INTO user (username, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.username}, #{user.age})
</foreach>
</insert>
<!-- 使用动态 SQL 避免无效查询 -->
<select id="selectDynamic" resultType="User">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
username = #{username}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
<if test="orderBy != null">
ORDER BY ${orderBy}
</if>
</select>
10.3 常见问题与解决方案
java
// 1. N+1 问题解决
@Select("SELECT * FROM user")
@Results({
@Result(property = "orders", column = "id",
many = @Many(select = "com.example.mapper.OrderMapper.selectByUserId",
fetchType = FetchType.LAZY))
})
List<User> selectAllWithOrders();
// 使用嵌套结果代替嵌套查询
<resultMap id="userWithOrdersResult" type="User">
<id property="id" column="id"/>
<collection property="orders" ofType="Order" resultMap="orderResultMap"/>
</resultMap>
// 2. 批量操作性能优化
public void batchInsertOptimized(List<User> users) {
try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH, false)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : users) {
mapper.insert(user);
}
session.commit();
}
}
// 3. 防止 SQL 注入
// 使用 #{} 而不是 ${}
// 使用 bind 标签
// 使用参数校验
// 4. 事务管理
@Transactional(rollbackFor = Exception.class)
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
User from = userMapper.selectForUpdate(fromId);
User to = userMapper.selectForUpdate(toId);
if (from.getBalance().compareTo(amount) < 0) {
throw new BusinessException("余额不足");
}
from.setBalance(from.getBalance().subtract(amount));
to.setBalance(to.getBalance().add(amount));
userMapper.update(from);
userMapper.update(to);
}
MyBatis 的世界简洁而强大,愿本文成为你持久层开发之路上的重要指南。持续学习,深入实践,你一定能成为 MyBatis 专家!
来源:
https://app-abggx9rbr6dd.appmiaoda.com