Cause: com.microsoft.sqlserver.jdbc.SQLServerException: “}”附近有语法错误。; uncategorized SQLException; SQL state [S0001]; error code [102]; “}”附近有语法错误。; nested exception is com.microsoft.sqlserver.jdbc.SQLServerException: “}”附近有语法错误。
在调用SQLServer的存储过程时使用分页会出现报错,核心原因是PageHelper (以及 MyBatis-Plus) 的自动分页机制无法处理 SQL Server 的存储过程 (CALLABLE),因为无法生成合法的 COUNT 子查询。
一、问题:当 PageHelper 遇上 SQL Server 存储过程
在跨数据库架构(如 Java 应用连接 SQL Server)中,我们有时需要调用第三方提供的存储过程。当引入 PageHelper 试图自动分页时,控制台突然抛出如下异常:
❌ 案例 1:无参存储过程报错
MyBatis XML 配置:
<select id="selectTBrandList" statementType="CALLABLE" parameterType="TBrand" resultMap="TBrandResult">
{call eg_BrandInfoGet()}
</select>
报错日志:
### SQL: select count(0) from ( {call eg_BrandInfoGet()} ) tmp_count
### Cause: com.microsoft.sqlserver.jdbc.SQLServerException: “}”附近有语法错误。
; uncategorized SQLException; SQL state [S0001]; error code [102];
“}”附近有语法错误。
❌ 案例 2:带参存储过程报错
MyBatis XML 配置:
<select id="selectTMaterialList" statementType="CALLABLE" resultMap="TMaterialResult">
{call eg_MaterielQry(
#{mCode, mode=IN, jdbcType=NVARCHAR},
#{mName, mode=IN, jdbcType=NVARCHAR},
#{mModel, mode=IN, jdbcType=NVARCHAR},
#{brand, mode=IN, jdbcType=NVARCHAR},
#{purchaser, mode=IN, jdbcType=NVARCHAR},
#{type, mode=IN, jdbcType=INTEGER}
)}
</select>
报错日志:
同样指向 select count(0) from ( {call ...} ) 语法错误。
二、根源剖析:为什么 PageHelper 会生成非法 SQL?
- PageHelper 的工作原理
PageHelper 是一个基于 MyBatis 拦截器机制的分页插件。它的核心逻辑是:
拦截用户执行的 SQL 语句。
自动在原始 SQL 外层包裹一个 SELECT COUNT(0) FROM (...) tmp_count 语句,用于获取总记录数。
根据数据库方言(Dialect),在原始 SQL 末尾添加分页限制(如 MySQL 的 LIMIT,SQL Server 的 OFFSET FETCH)。 - SQL Server 的语法铁律
对于普通的 SELECT 语句,PageHelper 生成的 SQL 是这样的:
-- 原始 SQL
SELECT * FROM BrandTable WHERE ...
-- PageHelper 生成的 Count SQL (合法)
SELECT COUNT(0) FROM (SELECT * FROM BrandTable WHERE ...) tmp_count
但是,当原始语句是存储过程调用 {call Proc_Name()} 时,PageHelper 依然机械地包裹:
-- PageHelper 生成的 Count SQL (非法!)
SELECT COUNT(0) FROM ( {call eg_BrandInfoGet()} ) tmp_count
❌ 致命错误点:
在 SQL Server (T-SQL) 中,存储过程调用(EXEC/CALL)不能直接作为子查询出现在 FROM 子句中。存储过程返回的是一个结果集流(Result Set Stream),而不是一个可以被外层 SELECT 直接引用的表对象。
这就是报错 “}”附近有语法错误 的根本原因。这不是 PageHelper 的 Bug,而是 SQL Server 的语法限制。 任何试图通过配置 PageHelper 来“自动”支持存储过程分页的尝试都会失败。
三、解决方案
针对上述问题,我们有三种经过生产环境验证的解决路径,按推荐程度排序。
✅ 方案一:重构为视图(View)—— 适用于无参/静态逻辑
适用场景:存储过程内部逻辑主要是 SELECT 查询,且不依赖输入参数动态改变表结构或核心逻辑(如案例 1)。
操作步骤:
数据库端:将存储过程改写为视图。
-- 原存储过程逻辑
-- CREATE PROC eg_BrandInfoGet AS SELECT BrandID, BrandName FROM Brands...
-- 改为视图
CREATE VIEW V_Eg_BrandInfo AS
SELECT BrandID, BrandName, CreateTime
FROM eg_Brands
WHERE Status = 1; -- 固定过滤条件
MyBatis 端:移除 statementType="CALLABLE",当作普通表查询。
<select id="selectTBrandList" parameterType="TBrand" resultMap="TBrandResult">
SELECT * FROM V_Eg_BrandInfo
<where>
<!-- 动态条件移到这里 -->
<if test="brandName != null and brandName != ''">
AND BrandName LIKE '%' + #{brandName} + '%'
</if>
</where>
</select>
优点:
PageHelper 完美支持,自动生成合法的 Count SQL。
性能最优,SQL Server 优化器可完全穿透视图。
代码最简洁,维护成本最低。
✅ 方案二:重构为表值函数(TVF)—— 适用于带参查询(强烈推荐)
适用场景:存储过程依赖输入参数进行过滤或动态逻辑(如案例 2)。这是解决带参存储过程分页的最佳方案。
关键知识点:
SQL Server 支持 表值函数 (Table-Valued Function),它可以接受参数,并返回一个表对象。这个返回的表可以在 FROM 子句中被查询,因此可以被 PageHelper 包裹。
优先选择:内联表值函数 (Inline TVF)
定义:RETURN (SELECT ...),本质是带参数的视图。
性能:极高,执行计划与直接写 SQL 无异。
操作步骤:
数据库端:将带参存储过程改为内联表值函数。
-- 原存储过程:eg_MaterielQry (@mCode, @mName...)
-- 改为内联表值函数
CREATE FUNCTION Func_Eg_MaterielQry (
@mCode NVARCHAR(50),
@mName NVARCHAR(50),
@mModel NVARCHAR(50),
@brand NVARCHAR(50),
@purchaser NVARCHAR(50),
@type INT
)
RETURNS TABLE
AS
RETURN (
-- 直接返回 SELECT 语句,不要 BEGIN...END
SELECT MaterialID, Code, Name, Model, BrandName, Purchaser, Type
FROM eg_Materials M
JOIN eg_Brands B ON M.BrandID = B.ID
WHERE
(@mCode IS NULL OR M.Code LIKE '%' + @mCode + '%')
AND (@mName IS NULL OR M.Name LIKE '%' + @mName + '%')
AND (@mModel IS NULL OR M.Model LIKE '%' + @mModel + '%')
AND (@brand IS NULL OR B.BrandName LIKE '%' + @brand + '%')
AND (@purchaser IS NULL OR M.Purchaser LIKE '%' + @purchaser + '%')
AND (@type IS NULL OR M.Type = @type)
);
注:如果逻辑极其复杂无法用单条 SELECT 表达,可使用多语句表值函数(Multi-Stmt TVF),但需注意性能影响。
MyBatis 端:像查表一样查函数。
<select id="selectTMaterialList" parameterType="MaterialQuery" resultMap="TMaterialResult">
SELECT *
FROM dbo.Func_Eg_MaterielQry(
#{mCode}, #{mName}, #{mModel}, #{brand}, #{purchaser}, #{type}
)
<where>
<!-- 这里还可以继续追加 MyBatis 动态条件,实现双重过滤 -->
<if test="extraCondition != null">
AND SomeColumn = #{extraCondition}
</if>
</where>
</select>
优点:
完美支持参数化查询。
PageHelper 可正常生成 SELECT COUNT(0) FROM (SELECT * FROM dbo.Func_...) 合法 SQL。
保留了业务逻辑在数据库层的优势,同时解决了分页问题。
⚠️ 方案三:存储过程内部手动分页 —— 适用于无法修改数据库对象
适用场景:第三方不配合修改,或存储过程包含 INSERT/UPDATE 等副作用操作,无法转为函数/视图。
操作步骤:
放弃 PageHelper 自动分页:在该接口上禁用分页插件,或不调用 PageHelper.startPage()。
修改存储过程:增加分页参数 @PageIndex, @PageSize 和输出参数 @TotalCount。
CREATE PROC eg_MaterielQry
@mCode NVARCHAR(50),
-- ... 其他参数
@PageIndex INT = 1,
@PageSize INT = 10,
@TotalCount INT OUTPUT
AS
BEGIN
-- 1. 计算总数
SELECT @TotalCount = COUNT(0) FROM ... WHERE ...;
-- 2. 分页查询 (SQL Server 2012+ 语法)
SELECT MaterialID, Code, Name ...
FROM ...
WHERE ...
ORDER BY CreateTime DESC -- 必须有 ORDER BY
OFFSET (@PageIndex - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;
END
MyBatis 端:手动接收输出参数。
<select id="selectTMaterialList" statementType="CALLABLE" parameterType="map" resultMap="TMaterialResult">
{call eg_MaterielQry(
#{mCode, mode=IN},
...,
#{pageIndex, mode=IN},
#{pageSize, mode=IN},
#{total, mode=OUT, jdbcType=INTEGER}
)}
</select>
Java 端:手动组装分页对象。
Map<String, Object> params = new HashMap<>();
params.put("mCode", code);
params.put("pageIndex", pageNum);
params.put("pageSize", pageSize);
List<Material> list = mapper.selectTMaterialList(params);
Integer total = (Integer) params.get("total"); // 从 Map 获取输出参数
PageInfo<Material> pageInfo = new PageInfo<>(list);
pageInfo.setTotal(total); // 手动设置总数
缺点:
侵入性强,需修改存储过程。
失去了 PageHelper 的便利性,需手动处理分页逻辑。
如果需要动态排序,存储过程会变得非常复杂。
四、方案对比与决策建议
| 维度 | 方案一:视图 (View) | 方案二:表值函数 (TVF) | 方案三:存过内部分页 |
|---|---|---|---|
| 适用场景 | 无参/静态逻辑 | 带参动态逻辑 (推荐) | 无法修改 DB 对象/含写操作 |
| PageHelper 支持 | ✅ 完美支持 | ✅ 完美支持 | ❌ 不支持 (需手动) |
| SQL 合法性 | ✅ 合法 | ✅ 合法 | ✅ 合法 |
| 性能 | ⭐⭐⭐⭐⭐ (最优) | ⭐⭐⭐⭐ (Inline TVF 极快) | ⭐⭐⭐ (取决于 SP 写法) |
| 改造成本 | 低 | 中 | 高 (需改 Java + SP) |
| 维护性 | 高 | 高 | 低 (逻辑分散) |
🚀 最终建议
首选策略:将SQL Server查询类存储过程重构为“视图”或“内联表值函数”。
无参 → 视图。
有参 → 内联表值函数 (Inline TVF)。
这是唯一能同时保留 PageHelper 便利性、保证 SQL 合法性且性能最优的方案。
备选策略:如果确实无法修改数据库对象,则必须放弃 PageHelper 自动分页,采用方案三,在存储过程内部实现分页逻辑,并在 Java 层手动组装分页结果。
避坑指南:
不要试图寻找能自动给 SQL Server 存储过程分页的 Java 插件,这是 SQL Server 语法的硬伤,认清 SQL Server 不允许在 FROM 子句中调用存储过程 这一事实,非插件之过。
不要在 Java 层查出全量数据再 subList 分页,这会导致严重的性能事故(OOM)。
MyBatis-Plus 的分页插件原理与 PageHelper 类似,同样无法解决此问题,不要盲目切换框架或升级驱动。




所有评论(0)