1. 项目概述
存储过程作为数据库层面的重要功能组件,在企业级应用开发中扮演着关键角色。当我们需要在Spring Boot应用中调用存储过程时,MyBatis作为持久层框架提供了灵活的实现方案。不同于简单的SQL映射,存储过程调用涉及参数传递模式、结果集处理、事务边界控制等复杂场景。
我在金融支付系统开发中,曾处理过日均调用量超百万次的交易核对存储过程,深刻体会到正确调用方式对系统稳定性的影响。本文将基于实战经验,演示Spring Boot+MyBatis环境下存储过程调用的完整实现路径,并分享性能优化、异常处理等方面的最佳实践。
2. 核心设计解析
2.1 存储过程定义规范
在MySQL中创建规范的存储过程是成功调用的前提。建议采用以下模板:
DELIMITER // CREATE PROCEDURE proc_order_summary( IN merchant_id INT, OUT total_count INT, OUT total_amount DECIMAL(18,2) ) BEGIN SELECT COUNT(*), SUM(amount) INTO total_count, total_amount FROM orders WHERE merchant_id = merchant_id AND status = 'SUCCESS'; END // DELIMITER ;关键设计要点:
- 明确区分IN/OUT参数类型
- 金额等数值类型指定精度
- 添加完整的业务条件过滤
- 使用DELIMITER避免语法冲突
2.2 MyBatis映射配置
在Mapper XML中配置存储过程调用时,需要特别注意参数模式声明:
<select id="callOrderSummary" statementType="CALLABLE"> {call proc_order_summary( #{merchantId, mode=IN, jdbcType=INTEGER}, #{totalCount, mode=OUT, jdbcType=INTEGER}, #{totalAmount, mode=OUT, jdbcType=DECIMAL} )} </select>必须设置的属性:
- statementType="CALLABLE" 声明为存储过程调用
- 每个参数的mode属性明确指定IN/OUT
- jdbcType确保类型匹配数据库定义
3. 完整实现步骤
3.1 项目环境搭建
使用Spring Initializr创建项目时需包含:
- Spring Web (提供Controller层支持)
- MyBatis Framework (核心持久层框架)
- MySQL Driver (数据库连接)
pom.xml关键依赖版本建议:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>8.0.33</version> </dependency>3.2 存储过程调用实现
完整的服务层调用示例:
@Service @RequiredArgsConstructor public class OrderService { private final OrderMapper orderMapper; public OrderSummaryVO getOrderSummary(Integer merchantId) { Map<String, Object> params = new HashMap<>(); params.put("merchantId", merchantId); params.put("totalCount", null); params.put("totalAmount", null); orderMapper.callOrderSummary(params); return OrderSummaryVO.builder() .totalCount((Integer)params.get("totalCount")) .totalAmount(new BigDecimal(params.get("totalAmount").toString())) .build(); } }关键实现细节:
- 使用Map封装输入输出参数
- OUT参数初始化为null
- 结果转换时注意类型处理
- 建议添加@Transactional保证过程原子性
4. 高级实践技巧
4.1 结果集映射处理
当存储过程返回游标时,需特殊处理:
<resultMap id="orderResult" type="com.example.Order"> <id column="order_id" property="id"/> <result column="order_no" property="orderNo"/> </resultMap> <select id="callOrderQuery" statementType="CALLABLE"> {call proc_query_orders( #{status, mode=IN}, #{result, mode=OUT, jdbcType=CURSOR, resultMap=orderResult} )} </select>4.2 批量处理优化
对于需要批量处理的场景:
@Transactional public void batchUpdateOrders(List<Order> orders) { SqlSession session = sqlSessionTemplate.getSqlSessionFactory().openSession(ExecutorType.BATCH); try { OrderMapper mapper = session.getMapper(OrderMapper.class); orders.forEach(order -> { Map<String, Object> params = new HashMap<>(); params.put("orderId", order.getId()); params.put("newStatus", order.getStatus()); mapper.callUpdateStatus(params); }); session.commit(); } finally { session.close(); } }5. 生产环境注意事项
5.1 性能监控配置
在application.yml中添加MyBatis日志配置:
logging: level: com.example.mapper: debug配合P6Spy可获取完整执行SQL:
<dependency> <groupId>p6spy</groupId> <artifactId>p6spy</artifactId> <version>3.9.1</version> </dependency>5.2 常见问题排查
参数类型不匹配错误:
- 检查jdbcType与存储过程定义是否一致
- 特别注意DECIMAL类型的精度设置
存储过程不存在异常:
- 确认数据库连接指向正确的schema
- 检查存储过程名称大小写敏感性
事务不生效问题:
- 确保@Transactional注解生效
- 检查数据库引擎是否支持事务(如MyISAM不支持)
游标泄漏风险:
- 使用try-with-resources确保ResultSet关闭
- 设置defaultResultSetType=FORWARD_ONLY
6. 架构设计建议
在微服务架构下,建议采用以下模式:
- 将复杂业务逻辑下沉到存储过程
- 服务层仅负责参数组装和结果转换
- 通过数据库连接池控制并发调用量
- 对高频调用过程添加结果缓存
HikariCP连接池推荐配置:
spring: datasource: hikari: maximum-pool-size: 20 connection-timeout: 30000 leak-detection-threshold: 60000对于需要处理数万条记录的场景,建议采用分页调用模式:
CREATE PROCEDURE proc_large_query( IN page_num INT, IN page_size INT, OUT total_records INT ) BEGIN -- 计算总数 SELECT COUNT(*) INTO total_records FROM large_table; -- 分页查询 SELECT * FROM large_table LIMIT page_size OFFSET (page_num - 1) * page_size; END在Java调用侧实现分页迭代:
public <T> List<T> queryByPage(PageQuery<T> query) { Map<String, Object> params = new HashMap<>(); params.put("pageNum", query.getPageNum()); params.put("pageSize", query.getPageSize()); params.put("totalRecords", null); List<T> data = mapper.queryLargeData(params); query.setTotalRecords((Integer)params.get("totalRecords")); return data; }7. 安全防护措施
SQL注入防护:
- 永远不要拼接SQL参数
- 使用#{param}语法而非${param}
权限控制:
- 为应用账号配置最小必要权限
- 单独设置存储过程执行权限
敏感数据保护:
- 对金额等字段使用加密传输
- 日志中脱敏关键参数
审计日志示例配置:
@Around("execution(* com.example.mapper.*.*(..))") public Object logProcedureCall(ProceedingJoinPoint pjp) throws Throwable { String methodName = pjp.getSignature().getName(); Object[] args = pjp.getArgs(); auditLog.info("调用存储过程: {}, 参数: {}", methodName, maskSensitiveData(args)); return pjp.proceed(); }8. 性能优化方案
索引优化:
- 分析存储过程执行计划
- 确保WHERE条件字段有合适索引
连接池调优:
- 根据并发量调整maxPoolSize
- 设置合理的空闲超时时间
结果缓存:
- 对实时性要求不高的结果添加缓存
- 使用@Cacheable注解实现
批量操作:
- 合并多次调用为批量操作
- 使用UNION ALL替代多次查询
执行计划分析示例:
EXPLAIN ANALYZE CALL proc_complex_report('2023-01-01', '2023-12-31');9. 异常处理策略
建议定义统一的异常处理体系:
@ControllerAdvice public class ProcedureExceptionHandler { @ExceptionHandler(MyBatisSystemException.class) public ResponseEntity<ErrorResult> handleProcedureError(MyBatisSystemException e) { Throwable rootCause = NestedExceptionUtils.getRootCause(e); if (rootCause instanceof SQLException) { SQLException sqlEx = (SQLException)rootCause; return ResponseEntity.status(500) .body(ErrorResult.of("PROCEDURE_ERROR", "存储过程执行失败: " + sqlEx.getMessage())); } return ResponseEntity.status(500) .body(ErrorResult.of("SYSTEM_ERROR", "系统异常")); } }针对存储过程特别处理的错误码:
public enum ProcedureErrorCode { INVALID_PARAM(4001, "参数校验失败"), DATA_NOT_FOUND(4004, "数据不存在"), CONCURRENT_CONFLICT(4009, "并发操作冲突"); private final int code; private final String message; // constructor & getters }10. 现代架构演进
随着云原生架构的普及,存储过程的使用模式也在发生变化:
分布式事务场景:
- 使用Seata等分布式事务框架
- 将本地事务与存储过程结合
多数据源调用:
- 配置多个SqlSessionTemplate
- 使用@DS注解切换数据源
存储过程编排:
- 将复杂流程拆分为多个存储过程
- 使用状态表控制执行流程
多数据源配置示例:
@Configuration @MapperScan(basePackages = "com.example.db1.mapper", sqlSessionTemplateRef = "db1SqlSessionTemplate") public class Db1DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.db1") public DataSource db1DataSource() { return DataSourceBuilder.create().build(); } @Bean public SqlSessionTemplate db1SqlSessionTemplate( @Qualifier("db1DataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); return new SqlSessionTemplate(factory.getObject()); } }在需要处理超大规模数据的场景下,可以考虑以下优化策略:
分库分表支持:
- 使用ShardingSphere等中间件
- 在存储过程中处理分片逻辑
异步调用模式:
- 将存储过程调用放入消息队列
- 使用@Async实现异步执行
读写分离:
- 配置主从数据源
- 读操作路由到从库
异步调用示例:
@Async("dbTaskExecutor") @Transactional public CompletableFuture<ReportResult> generateReportAsync(ReportRequest request) { Map<String, Object> params = convertToParams(request); mapper.callReportProcedure(params); return CompletableFuture.completedFuture(extractResult(params)); }线程池配置建议:
spring: task: execution: pool: core-size: 5 max-size: 20 queue-capacity: 100 thread-name-prefix: db-task-