news 2026/10/17 20:24:20

JDBC调用MySQL存储过程与存储函数:从CallableStatement到工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JDBC调用MySQL存储过程与存储函数:从CallableStatement到工程实践

做后端这些年,我见过不少同学一碰到存储过程就打怵。明明JDBC里PreparedStatement已经能解决绝大多数查询,为什么项目里还留着成片的CALL xxx()?DAY04这课的内容,正好卡在这个问题点上:在Java里通过JDBC调用MySQL的存储过程和存储函数。这篇文章会从场景、MySQL侧建过程、JDBC调用代码、坑位排查,一路讲到工程化里的取舍,适合正在学JDBC的初学者,也适合被老系统里一堆存储过程折磨过的同学。

1. 先理解Java和MySQL存储过程打交道的场景

1.1 什么项目里会有一堆"只能用存储过程"的需求

我第一次在项目里大规模使用JDBC调用存储过程,是接手一个银行类老系统的报表模块。那套系统里,统计逻辑不是写在Java代码里,而是作为几十个存储过程放在MySQL中,由DBA统一维护。业务同学改报表口径时,连开发都不用找,直接让DBA改存储过程。这听起来有点反直觉,但在某些行业里确实是常态。

这类场景往往有几个共同点:

  • 报表统计逻辑极其复杂,涉及十几张表关联、动态拼接查询条件、多层子查询,放在Java里拼SQL会非常不可控。
  • 需要对大批量数据做循环、临时表、逐行刷数等操作,比如每日数据归档、历史数据处理,用存储过程跑更贴近数据的存放位置。
  • 存在强数据权限或多租户过滤逻辑,必须在一个事务里完成多次SELECT、UPDATE、INSERT,由数据库端统一封装更安全。
  • 旧系统已经沉淀了大量存储过程,你根本没有权限去重写它们,只能通过JDBC把它们调用起来。

所以在DAY04这个时间点,学会在Java里调存储过程,不是“学个冷门语法”,而是为接老系统、做数据迁移、配合DBA工作扫清障碍。

1.2 存储过程、存储函数、普通SQL的区别在哪

要搞清楚JDBC怎么调用,先得把三样东西放一起对比。

维度PreparedStatement执行SQL存储过程(CALL)存储函数(FUNCTION)
返回内容往往是一个结果集或更新计数可以有若干结果集、更新计数,也可以返回OUT参数必须返回一个单值
业务归属写在Java代码里,数据库不认知逻辑在数据库内,Java只传参数、取结果逻辑在数据库内,可嵌入SQL表达式
预编译效果SQL文本每次可变化,但可复用连接缓存首次执行后通常可复用执行计划同SQL一样可复用执行计划
典型使用简单查询、增删改、动态条件复杂统计、批量任务、多步骤事务金额计算、字符串处理等可复用逻辑
JDBC入口PreparedStatement / StatementCallableStatementCallableStatement或PreparedStatement

这里有个实际判断标准:简单增删改查,没必要非上存储过程,尤其是新项目,Java里用MyBatis或者JPA反而更直观。但当你遇到一条SQL超过几十行、需要循环更新临时表、必须在一个数据库事务里完成多步操作时,存储过程就变得非常合理。

我还想提醒一点:存储过程并不是“更快”的代名词。它减少了网络上的SQL文本传输,但真正的性能收益来自减少解析和固化执行计划的场景。很多存储过程的性能问题其实出在内部SQL没走索引,所以别迷信“调存储过程就一定快”。

2. MySQL侧先把过程造出来:语法、参数模式和函数

2.1 DELIMITER的产生原因和创建骨架

很多新手第一次在Navicat里建存储过程,直接复制网上的SQL跑,结果碰到语法错。原因往往不是SQL写错,而是少了DELIMITER。

DELIMITER是MySQL命令行客户端的一个指令,不是SQL语句。它的作用是告诉客户端:看到什么符号才算一条完整语句结束。因为存储过程内部有大量分号,默认的分号分隔符会让客户端以为每条内部语句都要单独执行,当然会报错。

所以创建一个存储过程前,先临时把分隔符改成$$或//,建完之后再改回来。

DROP PROCEDURE IF EXISTS sp_get_user_count; DELIMITER $$ CREATE PROCEDURE sp_get_user_count(OUT total INT) BEGIN SELECT COUNT(*) INTO total FROM user; END$$ DELIMITER ;

这里顺带解释几个点:

  • DROP PROCEDURE IF EXISTS是习惯操作,方便重复执行脚本,避免每次都手动删。
  • 参数用括号跟在过程名后面,OUT total INT表示这个参数是输出参数。
  • SELECT COUNT(*) INTO total是把统计结果写进OUT参数。
  • 结束符$$只在客户端创建时有效,Java通过JDBC调用时完全不经MySQL命令行解析,所以不受DELIMITER影响。

2.2 IN、OUT、INOUT三兄弟,参数模式决定JDBC怎么写

存储过程的参数模式有三种:IN、OUT、INOUT。它们的对应关系,直接决定JDBC代码里要写setXxx还是registerOutParameter。

参数模式方向JDBC对应写法执行后能否取值
IN只进不出cs.setXxx(index, value)不用取
OUT只出不进cs.registerOutParameter(index, Types.xxx)cs.getXxx(index)
INOUT既进又出先setXxx,再registerOutParametercs.getXxx(index)

举个例子,班上有用户表,按角色统计人数:

DROP PROCEDURE IF EXISTS sp_get_user_by_role; DELIMITER $$ CREATE PROCEDURE sp_get_user_by_role(IN p_role INT, OUT total INT) BEGIN SELECT COUNT(*) INTO total FROM user WHERE role = p_role; END$$ DELIMITER ;

注意过程体里用p_role作为参数名,没有和字段名role混在一起。MySQL的变量命名解析存在“字段名优先”的情况,如果参数名和SELECT字段名相同,很容易出现取不到理想结果的问题,这是新手经常踩的暗坑。

再看一个带INOUT的:

DROP PROCEDURE IF EXISTS sp_update_salary; DELIMITER $$ CREATE PROCEDURE sp_update_salary(INOUT base DECIMAL(10, 2), IN rate DECIMAL(5, 2)) BEGIN SET base = base * (1 + rate); END$$ DELIMITER ;

这个过程的含义很直观:传入一个薪资基数,再传入涨薪比例,过程把计算结果写回base参数。因为参加计算,它得是INOUT,如果定义成OUT,传入时根本没有值,计算就成了空值。

2.3 存储函数:必须有返回值,调用位置完全不同

存储函数和存储过程最大的区别在于:函数一定有一个返回值,并且通常不能返回结果集。它更像一个数据库内置函数,比如NOW()、ABS(),只是这个函数是你自己写的。

DROP FUNCTION IF EXISTS fn_calc_tax; DELIMITER $$ CREATE FUNCTION fn_calc_tax(amount DECIMAL(10, 2), rate DECIMAL(5, 2)) RETURNS DECIMAL(10, 2) DETERMINISTIC BEGIN RETURN amount * rate; END$$ DELIMITER ;

几点说明:

  • RETURNS DECIMAL(10,2)声明返回类型,必须写,否则MySQL直接报错。
  • DETERMINISTIC意思是“同样的输入一定得到同样的输出”。不是每个函数都必须加,但如果你创建函数时开启了二进制日志(binlog),经常会要求声明,建议养成写的好习惯。
  • 调用位置不同是核心差异:过程用CALL调用,函数可以直接出现在SELECT表达式里。
SELECT fn_calc_tax(10000.00, 0.13) AS tax;

这条SQL能跑通,就说明函数已经建好。到JDBC侧,函数的调用方式会比过程更灵活,这一点我在后面第四章专门展开。

3. JDBC调用存储过程:从连接串到CallableStatement

3.1 连接串与驱动:MySQL 8 的认证与时区坑

调存储过程之前,先保证能连上MySQL。DAY04这个阶段很多同学卡住的不是存储过程本身,而是驱动版本和连接串参数。

MySQL 8默认的认证插件是caching_sha2_password,旧驱动com.mysql.jdbc.Driver已经不能用。现在要写:

Class.forName("com.mysql.cj.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/test_db" + "?useSSL=false&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true&characterEncoding=utf8"; Connection conn = DriverManager.getConnection(url, "root", "your_password");

这三个参数碰到会报什么错,我建议你亲手试一次:

  • serverTimezone不配:MySQL Connector/J 6.0以上会报The server time zone value 'xxx' is unrecognized,因为驱动想校验服务器的时区映射。
  • allowPublicKeyRetrieval=true不配:用caching_sha2_password认证且非SSL连接时,常见报错Public Key Retrieval is not allowed。
  • useSSL=false:本地开发环境一般不强制走SSL,测试阶段先关掉,遇到SSL connection error会少很多干扰。

一个更推荐的写法是用连接池而不是裸的DriverManager,比如HikariCP。但DAY04先把裸连接跑通,理解底层原理再上连接池也不迟。

这里我补充一个认知:JDBC连接串本身和存储过程调用没有直接关系,但它决定了你能不能顺利走到prepareCall这一步。踩连接异常时,优先检查驱动版本、时区、SSL、公钥获取这几个参数,再去看网络和端口。

3.2 无参过程和IN参数过程的调用模板

CallableStatement是专门用来调用存储过程的接口,它继承了PreparedStatement,所以之前学过的setInt、setString、executeQuery等操作它也都有。但调用过程时,更推荐用execute()而不是executeQuery(),因为存储过程可能返回结果集、更新计数、OUT参数的任意组合,executeQuery()只适合清晰返回单个结果集的情况。

先看OUT参数的调用模板:

String sql = "{CALL sp_get_user_count(?)}"; try (CallableStatement cs = conn.prepareCall(sql)) { cs.registerOutParameter(1, Types.INTEGER); cs.execute(); int total = cs.getInt(1); System.out.println("用户总数:" + total); } catch (SQLException e) { e.printStackTrace(); }

这段代码的骨架是:prepareCall-> 用占位符的存储过程调用串 ->registerOutParameter注册OUT参数 ->execute()执行 ->getXxx(index)取值。

注意一点:JDBC里的参数索引从1开始。很多从别的语言转过来的同学会习惯从0开始,结果取出来的永远是错位数据,这是个高频问题。

再来看带IN参数的版本:

String sql = "{CALL sp_get_user_by_role(?, ?)}"; try (CallableStatement cs = conn.prepareCall(sql)) { cs.setInt(1, 3); cs.registerOutParameter(2, Types.INTEGER); cs.execute(); System.out.println("角色3的用户数:" + cs.getInt(2)); }

这里{CALL sp_get_user_by_role(?, ?)}整体是JDBC的转义语法,花括号不是装饰。有些老资料会写成prepareCall("CALL sp_get_user_by_role(?, ?)"),某些驱动能容忍,但标准写法是带花括号的。项目里如果碰到“换了一个驱动就报错”的诡异问题,先检查转义语法是否规范。

3.3 OUT、INOUT参数的注册与取值

前面那个涨薪的INOUT过程,对应JDBC代码是这样:

String sql = "{CALL sp_update_salary(?, ?)}"; try (CallableStatement cs = conn.prepareCall(sql)) { BigDecimal base = new BigDecimal("8000.00"); cs.setBigDecimal(1, base); cs.setBigDecimal(2, new BigDecimal("0.12")); // INOUT参数必须同时注册为OUT,否则拿不到计算结果 cs.registerOutParameter(1, Types.DECIMAL); cs.execute(); System.out.println("调整后薪资:" + cs.getBigDecimal(1)); }

这段代码的关键细节:

  • base先进去参与计算,最后又出来被重新赋值,所以第1个参数既要setBigDecimal又要registerOutParameter。
  • registerOutParameter里的Types.DECIMAL要和MySQL字段类型对应。你全填Types.INTEGER,在getBigDecimal时会因为类型映射对不上而报错,或者在某些驱动里返回null。
  • execute()执行完,OUT参数才有值,必须等执行后再getXxx。

我见过有人用cs.getInt(1)去取DECIMAL,正常情况下会抛SQLException,因为类型转换失败。解决方法是拿不准类型时先getObject(1)看实际类型,再决定用getBigDecimal还是getInt。这招调试阶段很好用。

3.4 过程返回多个ResultSet时怎么按序取

存储过程用BEGIN...END包起来的语句可能有多个SELECT,这意味着JDBC侧可能收到多个ResultSet。很多初学存储过程的人只处理第一个结果集,后面的数据白白丢掉。

假设有这样一个过程,先查用户信息,再查用户明细地址:

DELIMITER $$ CREATE PROCEDURE sp_get_user_and_address(IN p_user_id INT) BEGIN SELECT id, username, email FROM user WHERE id = p_user_id; SELECT address, city FROM user_address WHERE user_id = p_user_id; END$$ DELIMITER ;

JDBC侧的标准遍历方式:

String sql = "{CALL sp_get_user_and_address(?)}"; try (CallableStatement cs = conn.prepareCall(sql)) { cs.setInt(1, 1001); boolean hasResult = cs.execute(); // execute()返回true,说明第一个结果是ResultSet if (hasResult) { try (ResultSet rs = cs.getResultSet()) { while (rs.next()) { System.out.println("用户:" + rs.getString("username")); } } // 继续取后面的结果集 if (cs.getMoreResults()) { try (ResultSet rs2 = cs.getResultSet()) { while (rs2.next()) { System.out.println("地址:" + rs2.getString("city")); } } } } }

这里有几个必须记住的点:

  • 用execute()判断第一个结果是不是ResultSet;返回true就getResultSet(),返回false说明它是更新计数。
  • 处理完第一个结果集之后想取下一个,必须调用getMoreResults(),它会推动驱动向后移动。
  • 不能一次性拿到所有ResultSet的引用再逐个读。JDBC驱动一般只会保留当前活动结果集,前一个结果集往往在获取下一个后被关闭,所以必须“读一个,取下一个”。
  • 更完整的循环还要判断getUpdateCount(),当getMoreResults()返回false且getUpdateCount() == -1时才表示所有结果处理完毕。Day04阶段,能把两个结果集按顺序读完已经及格,但理解-1这个终止条件能帮你避开很多边界问题。

4. 存储函数在JDBC里的两种调法

4.1 方式一:CallableStatement + {? = CALL ...}

JDBC对存储函数同样提供了转义语法,和过程最大的不同是前面多了一个? =,这个问号代表函数返回值。

看一个计算税额的完整例子:

String sql = "{? = CALL fn_calc_tax(?, ?)}"; try (CallableStatement cs = conn.prepareCall(sql)) { cs.registerOutParameter(1, Types.DECIMAL); cs.setBigDecimal(2, new BigDecimal("100000.00")); cs.setBigDecimal(3, new BigDecimal("0.13")); cs.execute(); BigDecimal tax = cs.getBigDecimal(1); System.out.println("税额:" + tax); }

注意参数顺序,和函数定义完全一致:第一个占位符给返回值,第二个给amount,第三个给rate。写错顺序的报错很有迷惑性,经常是Parameter index out of range或者拿到一个明显离谱的值。

如果你对一个函数是不是有返回值拿不准,可以在MySQL里先SELECT fn_calc_tax(...) AS tax试一下,返回正常再写JDBC。用SQL侧验证是成本最低的手段。

4.2 方式二:SELECT 函数(?, ?) 直接查询

存储函数本质上是一个可复用的表达式,所以它不像过程那样必须用CallableStatement调,完全可以直接放进SELECT语句:

String sql = "SELECT fn_calc_tax(?, ?) AS tax"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setBigDecimal(1, new BigDecimal("100000.00")); ps.setBigDecimal(2, new BigDecimal("0.13")); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { BigDecimal tax = rs.getBigDecimal("tax"); System.out.println("税额:" + tax); } } }

我在项目里第一次看到同事这么写的时候还挺意外,后来发现这是调函数的常规思路。它比CallableStatement少一个registerOutParameter步骤,代码更短,也更符合函数作为表达式的语义。

这种方式还方便混进复杂SQL里,比如:

String sql = "SELECT username, fn_calc_tax(order_amount, 0.13) AS tax FROM user_order WHERE id = ?";

函数直接在查询列表里参与计算,JDBC端完全不需要知道函数调用的存在,把函数当成普通数据库函数即可。

4.3 二选一的判断标准

判断依据CallableStatement方式SELECT方式
调用语义更接近“调用函数”更接近“使用表达式”
参数绑定需要用registerOutParameter只需要普通setXxx
嵌套进复杂SQL基本不可能很方便
代码简洁度略啰嗦更短
典型场景单独执行一次函数且需要OUT包一层把函数当普通数据库内置函数使用

个人经验:如果函数只是纯粹的单值计算,又没有和复杂SQL混用的需求,我默认用SELECT fn(...),简洁、不容易写错;如果项目里其他人习惯统一走CallableStatement,那就跟着项目风格走。关键不是“哪个更高级”,而是保持代码风格一致,后人维护起来不费劲。

5. 实战排错:把我在项目里踩过的坑完整还原一遍

5.1 OUT参数总是null:先看索引还是先看类型?

有一次我维护一个人员统计接口,DBA给了个过程,签名里有3个IN参数和2个OUT参数。我在Java侧写完,执行程序,两个OUT参数返回全都是null。当时第一反应是SQL写错了,查了半天没结果。后来一步步排查才发现问题出在参数索引和类型上。

排查链路是这样的:

  • 第一步,把prepareCall里的SQL原样打印出来:{CALL sp_person_stat(?, ?, ?, ?, ?)},确认占位符数量和过程签名一致。
  • 第二步,直接在MySQL客户端手工执行CALL sp_person_stat(...),确认过程本身能正常输出。这一步很关键,它把“数据库侧问题”和“JDBC侧问题”一刀切开。
  • 第三步,检查registerOutParameter的索引。我当时的错误是把OUT参数当成从0开始,但JDBC的参数索引从1开始,最后一个参数写成了4和5,实际对应的是DBA签名里的第4、5个位置,表面看好像对,其实第4个是IN参数,往里注册OUT就会失效。
  • 第四步,检查参数类型。如果DBA把OUT定义成DECIMAL,我用Types.INTEGER注册,用getInt()读取,某些驱动会转换失败或返回null。

这里分享一个高效的调试SQL,可以直接查过程参数的原义:

SELECT PARAMETER_NAME, PARAMETER_MODE, DATA_TYPE FROM information_schema.parameters WHERE SPECIFIC_NAME = 'sp_person_stat' ORDER BY ORDINAL_POSITION;

通过这个查询,能直接看到哪个参数是IN,哪个是OUT,数据类型是什么。比对着文档猜索引靠谱太多。

5.2 多个结果集互相覆盖:getMoreResults的正确使用姿势

有一次我从一个存储过程里取数据,过程第一段SELECT的是用户主表,第二段SELECT的是订单明细,但我拿到的结果集始终只有第一段的数据。

排查后发现,不是过程返回了错误数据,而是我的Java代码在处理ResultSet时,存储了两个ResultSet的引用,想等都执行完再统一读取。结果第二个结果集覆盖了第一个的内容,前一个句柄变成了无效状态。

正确的处理逻辑前面已经写过:execute()之后,每取到一个ResultSet就立即消费完,再调用getMoreResults()进入下一个。整个过程是串行的,不是把多个ResultSet对象都保存下来。

实际项目里,我一般会封装一个统一遍历方法:

List<Map<String, Object>> allResults = new ArrayList<>(); CallableStatement cs = conn.prepareCall(sql); boolean hasResult = cs.execute(); while (true) { if (hasResult) { try (ResultSet rs = cs.getResultSet()) { while (rs.next()) { // 收集当前行的数据 } } } int updateCount = cs.getUpdateCount(); if (updateCount == -1) { break; } hasResult = cs.getMoreResults(); }

这个循环的终止条件是getUpdateCount() == -1,含义是“所有结果集和更新计数都处理完了”。如果你只关心结果集,那么也要至少区分出“后面还有没有结果集”,避免漏数据。

5.3 存储过程内部报错,Java端却"没反应":事务与异常检查链

另一个让我印象深刻的坑,是过程内部明明有重复键冲突,程序却“感觉没报错”地跑完了。后来定位到两个原因。

第一个原因是存储过程内部用了DECLARE CONTINUE HANDLER或者异常吞掉的逻辑。MySQL允许在过程里定义异常处理器,比如遇到重复键就忽略继续执行。这种情况下,数据库侧就没有把错误抛出来,Java端自然拿不到SQLException。解决方式是先看过程体里有没有HANDLER相关代码,有的话要么让DBA改成DECLARE EXIT HANDLER,要么在Java侧做明确的结果校验。

第二个原因是事务没有提交。如果Connection的autoCommit被设置成false,执行完CALL后没调用commit(),过程里做的修改在外面看起来就是“没生效”。这不是过程执行失败,而是事务还挂着。排查方式是在存储过程执行后查一次本会话内的事务状态,或者干脆在测试阶段先不要关自动提交。

我现在的习惯是:涉及存储过程的事务,先和DBA确认过程内部有没有COMMIT。如果过程自己提交事务,那应用层的rollback实际上是无效的;如果过程不提交,应用层要负责整个事务边界,代码里每个CALL之后必须明确commit或rollback。把这事理清楚,就不会出现“报错没反应”和“成功但数据没落库”的妖蛾子。

5.4 连不上MySQL:ERROR 2003、Public Key Retrieval和连接池熔断

DAY04前后手写JDBC连接,常碰到连不上的报错,我按频率排一下:

  • ERROR 2003 (HY000): Can't connect to MySQL server on 'localhost:3306' (10061)。Windows下最常见,MySQL服务没启动。命令行执行net start mysql,或在服务管理器里启动MySQL服务,先确认3306端口有进程在监听。其次检查bind-address是不是限制了只允许本机连接。
  • Public Key Retrieval is not allowed。前面说过,MySQL 8的caching_sha2_password要求客户端获取服务端公钥,连接串加allowPublicKeyRetrieval=true即可。如果公司安全策略不允许,就要考虑把用户改成mysql_native_password认证方式,或者走SSL。
  • Connection is not available, request timed out。这类别是在连接池场景下,不是连不上MySQL,而是连接池里的连接被耗尽了。很多慢存储过程会长时间占用连接,如果maximumPoolSize配得小,高并发下一会儿就耗尽。

关于热搜里那个“flink的jdbc连接器异常”,本质上也跑不出上面这几类。框架底层用的还是JDBC,连接器报错时先绕过框架,用最原始的DriverManager写个十几行Java测试连接,能连上就说明问题在框架侧资源管理,连不上就回到驱动参数、服务状态、连接池这老三样排查。

6. 工程化建议:调用存储过程时的性能、事务与取舍

6.1 批量调用与网络往返

存储过程把多条SQL放进数据库内执行,确实能减少SQL文本在网络上传输的次数。但是,如果你在Java侧写一个for循环,每次循环都prepareCall然后execute,那仍然是“一次调用一次网络往返”,存储过程带来的网络优势就被消解了。

我的经验是,如果只需要传不同参数执行同一个过程,先考虑过程内部是否支持一次处理多组参数,比如传JSON字符串或临时表。实在要多次调用,尽量减少循环中的额外操作,比如把参数绑定和ResultSet处理剥离出主循环。

另一个容易误解的点:rewriteBatchedStatements=true这个参数对普通PreparedStatement批量SQL有效,但对CallableStatement的帮助有限。调用存储过程的批量执行更多是靠过程内部写循环,而不是靠Java侧批量提交。

所以结论是:想发挥存储过程的性能价值,就把“循环”放在数据库侧,让过程内部去处理临时表和迭代;Java侧尽量只做一次CALL,然后把结果集拉回来展示。

6.2 事务边界、连接池与存储过程内部commit的冲突

在项目里,事务控制至少会碰到两类问题。

第一类是过程内部写了显式的COMMIT。假设Java侧配了autoCommit=false,打算在业务失败时整体回滚,结果过程内部已经COMMIT了,那之前的更新就永久落库,Java侧的rollback()根本撤不回来。这个隐患很难在测试初期暴露,往往要等到线上数据异常才被发现。所以我会在代码评审和数据库评审阶段重点检查“交易类存储过程是否含有事务控制语句”。

第二类是连接池连接被长事务占满。MySQL的事务隔离下,一个连接上如果开了事务长时间不提交,即使没有大查询,也会影响连接池周转。调存储过程时尤其容易出现,因为过程内部可能有多条UPDATE、SELECT,执行时间被放大。排查时看连接池的active连接数和数据库SHOW PROCESSLIST,能快速定位是哪一条CALL在占连接。

6.3 生产上什么时候该用存储过程,什么时候该绕开

单说“存储过程老土不可用”是片面的,单说“什么逻辑都往数据库塞”也不对。我给一个比较务实的取舍标准。

适合用存储过程的场景:

  • 报表汇总、日切、计费等需要大量数据库往返的批处理逻辑。
  • 一套系统里已有数百个存储过程,业务语义已经固化在数据库层,贸然重写风险很大。
  • 需要数据库内循环、临时表配合的复杂数据处理,且数据库迁移可能性小。

应该绕开的场景:

  • 简单CRUD。MyBatis、JPA这类ORM已经足够好,非要在上面包一层存储过程,反而损失类型安全和可测试性。
  • 业务逻辑频繁变化、版本迭代快的模块。Java代码可以很方便地做单元测试、灰度发布,存储过程的版本管理天然弱于代码。
  • 分库分表、跨数据库异构环境。存储过程一旦绑定某个库,迁移和横向扩展都会变成灾难。

我自己的工作习惯是:新项目默认不主动写存储过程,复杂校验和聚合计算优先考虑Java里能不能实现;老项目里已经存在的存储过程,则老老实实把CallableStatement调用写稳,把参数文档列清楚,起码让人能接手维护。

最后分享一个数据库排查的小技巧:连着业务库的时候,随时查一下information_schema.routines和information_schema.parameters这两个表,一个看过程/函数的定义,一个看它们有哪些参数。比反复问DBA要准,也比你翻半天建表脚本要快。这些细节,就是Day04之后真正开始被项目需要的东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 12:12:33

微信小程序+SSM社区团购系统毕业设计实战指南

简介&#xff1a;这是一套面向计算机专业本科生的高分毕业设计实战案例&#xff0c;聚焦微信小程序SSM架构的社区团购系统开发&#xff0c;专为毕设、课程设计及项目实训打造。资源完整覆盖从前端小程序到后端SpringSpringMVCMyBatis的全栈实现&#xff0c;含可直接运行的源码、…

作者头像 李华
网站建设 2026/10/16 11:42:31

问卷系统测试实战:从功能用例到并发、安全与数据一致性验证

1. 问卷系统测试的起点&#xff1a;为什么这会是一个"高难度"项目接到问卷系统的测试任务时&#xff0c;说实话我一开始是有点轻敌的。问卷嘛&#xff0c;无非就是出题、答题、看结果&#xff0c;CRUD 四个字母基本能概括。但是真正扎进去之后&#xff0c;我才发现问…

作者头像 李华
网站建设 2026/10/13 12:29:24

操作系统与进程:从底层原理到故障排查实战

你每天开机关机、点开软件、遇到卡顿&#xff0c;第一反应多半是打开任务管理器一顿乱杀。但你有没有想过&#xff0c;那个让你“杀”的进程到底是什么&#xff1f;它和操作系统究竟是怎样的关系&#xff1f;为什么有的进程你杀了它又自己回来&#xff1f;为什么 F 盘会提示“被…

作者头像 李华
网站建设 2026/10/15 15:11:13

Flutter CustomPainter 在 OpenHarmony 上的 2D 游戏渲染实战方案

CustomPainter 这个名字&#xff0c;玩过 Flutter 的人多少都听过&#xff0c;但真正把它用在游戏画面渲染上、还跑在 OpenHarmony 设备上的人&#xff0c;可能没那么多。这篇文章想跟你分享的就是这么一件事&#xff1a;用 Flutter 的 CustomPainter 在 OpenHarmony 上做一套轻…

作者头像 李华
网站建设 2026/10/16 7:28:44

PostgreSQL逻辑复制解析WAL同步Kafka实战

简介&#xff1a;本资源面向数据库开发与数据集成工程师&#xff0c;提供一套基于PostgreSQL逻辑复制功能的实时数据变更捕获与同步系统源码。系统通过解析WAL日志捕获数据变更&#xff0c;将其转换为可执行的SQL语句&#xff0c;并借助Kafka消息队列实现PostgreSQL到异构数据源…

作者头像 李华
网站建设 2026/10/16 1:41:25

ARM64节点部署Flannel v0.11.0:从tar.gz到Pod网络全攻略

简介&#xff1a;针对ARM64架构Linux服务器上部署Kubernetes集群的运维与开发人员&#xff0c;这份flannel网络插件安装包提供了v0.11.0版本的离线二进制。flannel是K8s中广泛使用的Overlay网络方案&#xff0c;可解决多节点容器间通信与Pod网段划分问题&#xff0c;适合树莓派…

作者头像 李华