对于开发人员来说,数据分页查询是常见的需求。网上也有一大堆的分页查询存储过程的例子,有的还冠以“通用”之名。但实际情况是,在特定项目需求下,又显得不那么通用了。最近在工作中就碰到连接多个表查询记录的情况,由于项目是基于Entity Framework的,最初的做法是先写好相关的视图,然后映射到EF,采用Linq To Entity查询数据。这样的做法运行效率低不说,每次有字段改动还得重新映射一遍,很郁闷。由于之前存储过程写的很少,不知道采用存储过程来实现分页查询。在网上参考了几个实例,再结合自己的项目,利用周末的时间写了个分页查询存储过程的模板。基本原理是存储过程的参数采用默认值,根据传进来的参数情况动态创建SQL语句,实现按条件查询。至于分页,有两种方法:SELECT TOP 方式和ROW_NUMBER()方式。前者是比较常见的处理方法,后者是SQL SERVER 2005才开始有的特性。
Create PROCEDURE [dbo].[SP_QueryAttandance] (
@recordTotal INT OUTPUT, --输出记录总数
@status int=0,
@empID int =0,
@dateFrom nvarchar(20)='',
@dateTo nvarchar(20)='',
@deptCode NVARCHAR(800)='', --部门Code
@sName NVARCHAR(800) = '', --员工名字
@sWorkNO NVARCHAR(200) = '', --工号
@pageSize INT = 20, --每页记录数
@page INT =1, --当前页
@sortName NVARCHAR(50)='PKID', --排序字段
@sortOrder NVARCHAR(20) = 'asc' --排序顺序
)
AS
BEGIN
declare @sql nvarchar(MAX)
declare @where nvarchar(MAX)=' where 1=1 ' --查询条件拼接字符串
declare @sort nvarchar(50) --排序信息
declare @cols nvarchar(MAX) --选择列
--表连接语句
set @sql = ' from TB_HR_Attendance as att left join [TB_Admin_User] as u on att.FK_EmpID = u.PK_ID ' +
'left join TB_Admin_Department dept on u.FK_DepartID = dept.PK_ID '+
' left join TB_HR_DutyPeriod as duty on att.FK_DutyPeriodID = duty.PK_ID '
set @sort = ' order by '+ @sortName + ' ' + @sortOrder
set @cols = ' att.PK_ID as PKID,u.PK_ID as EmployeeID,u.sWorkNo as EmployeeNO, u.sName as EmployeeName, dept.sName as DepartName,dept.sCode as DeptCode, duty.sName as DutyPeriodName,duty.PK_ID as DutyPeriod,'+
'att.dtDate as Date,att.dtOnTime as OnTimeString, att.dtOffTime as OffTimeString,att.iStatus as Status '
IF @sName <> ''
BEGIN
--如果字符串中包含单引号,则替换成转义后的符号,否则拼接的SQL会有语法错误
if(CHARINDEX('''',@sName)>0)
begin
set @sName = REPLACE(@sName,'''','''''')
end
set @where = @where + ' and EmployeeName like ''%'+@sName + '%'''
END
IF @empID <>0
BEGIN
set @where = @where + ' and EmployeeID = '+ STR(@empID)
END
IF @status = -1
BEGIN
set @where = @where + ' and Status > 1'
END
ELSE IF @status >0
BEGIN
set @where = @where + ' and Status = '+ STR(@status)
END
IF @sWorkNO <> ''
--如果字符串中包含单引号,则替换成转义后的符号,否则拼接的SQL会有语法错误
if(CHARINDEX('''',@sWorkNO)>0)
begin
set @sWorkNO = REPLACE(@sWorkNO,'''','''''')
end
BEGIN
set @where = @where + ' and EmployeeNO = '''+@sWorkNO +''''
END
IF @deptCode <> ''
--如果字符串中包含单引号,则替换成转义后的符号,否则拼接的SQL会有语法错误
if(CHARINDEX('''',@deptCode)>0)
begin
set @deptCode = REPLACE(@deptCode,'''','''''')
end
BEGIN
set @where= @where+' and left(DeptCode,' + STR(LEN(@deptCode)) + ') =''' + @deptCode +''''
END
IF @dateFrom <> ''
BEGIN
set @where = @where + ' and Date >= ''' + @dateFrom + ''''
END
IF @dateTo <> ''
BEGIN
set @where = @where + ' and Date <= '''+ @dateTo + ''''
END
--set @where = @where + ' and tmpid between' +str((@page - 1)*@pageSize + 1 )+' and '+str( @page * @pageSize)
declare @strSQL nvarchar(MAX)
--采用SELECT TOP方式的分页
set @strSQL = 'select top ' + STR(@pageSize) + ' * from (select' + @cols + @sql + ')as tmpTable3' + @where + ' and PKID not in (select top '+ STR((@page-1)*@pageSize) +
' PKID from (select * from (select ' + @cols + @sql +')as tmpTable2' + @where +')as tmpTable1' + @sort +')'
+' ' + @sort
--采用ROW_NUMBER()方法的分页
set @strSQL = N'select * from (select ROW_NUMBER() over(' + @sort + ') as tmpid, * from (select * from (select ' + @cols + @sql+ ')as tmpTable1' + @where +')as tmpTable2) as tmpTable3 ' +'where tmpid between' +str((@page - 1)*@pageSize + 1 )+' and '+str( @page * @pageSize)
print(@strSQL)
exec(@strSQL)
set @strSQL = 'select @total= count(*) from (select' + @cols + @sql + ')as tmpTable' + @where
print @strSQL
exec sp_executesql @strSQL,N'@total int output',@total = @recordTotal output
END
总结下在写脚本过程中碰到的问题,我相信开发朋友们可能也碰到过。由于SQL语句用得不熟,很多语法都不太清楚,经过N次尝试和网上查询,总算能跑起来了。
1. 列别名问题。
SQL中为选择的列指定的别名在Order by子句中可以使用,但在Where子句中不能引用。一种解决办法是采用子查询,嵌套一层,在外层就可以引用别名,组织Where子句了。这对我写这个模板很关键,因为字段名是展现层表格的列名,排序用的也是这些列名。另外,需要给子查询结果表指定表别名,否则也会出错。这个让我当时纠结了半天,汗颜。
2.ROW_NUMBER()的用法
初次接触它,各种不熟悉。对于OVER(order by 列名 asc),列别名同样不识别,可采用上面的方法,嵌套一层子查询来解决。
3.拼接SQL语句问题。
要实现按条件动态生成SQL,我采用的是拼接SQL。这样带来的问题跟SQL注入攻击的问题差不多。输入的参数如果带有一个单引号,拼接生成的SQL语句在执行时会报错。所以要将单个单引号替换成两个单引号。代码注释有说明。
大家如果有需要的的话,可以将参数和表连接查询的语句修改下以适应自己的应用场景。