本周,我发现自己需要一些动态查询。现在,动态查询和动态 where 子句已经不是什么新鲜事了,而且在整个网络上都有很好的记录。然而,我需要更多的东西。我需要一种流畅的方式将新的 where 字段拉到客户端,并允许用户根据需要制作尽可能多的过滤器。甚至在单个字段上有多个过滤器。更重要的是,我需要访问 SQL Server 中所有可能的运算符。以下代码是实现此目的的一种方法。我将尝试用底部的完整代码指出代码的亮点。
希望你喜欢代码。
要求
- 该解决方案永远不会允许 SQL 注入。(不能使用 exec(command))
- 存储过程的调用者可以是任何东西。
- 数据集必须来自存储过程。
- 任何字段都可以根据需要过滤多次,几乎可以进行任何操作。
- 应允许过滤器的任何组合。
- 存储过程应该允许强制参数
首先,让我们看一下参数。
CREATE PROCEDURE [dbo].[MyReport]
-- Add the parameters for the stored procedure here
@p_iDistributorUID INT, -- manditory
@p_xParameters XML = null --optional parameters (hostile)
第一个参数必须始终发送,在这个演示中,我们有一个必须发送的经销商 ID。第二个参数是一个 XML 文档。这些是“动态 Where 子句”,我们认为这些潜在的 sql 注入,或者我认为这个参数是敌对的。
<root>
<OrFilters>
<AndFilter Name="vcinvoicenumber" Operator="2" Value="inv12"/>
<AndFilter Name="vcID" Operator="1" Value="asdqwe"/>
</OrFilters>
<OrFilters>
<AndFilter Name="iSerialNumber" Operator="1" Value="123456"/>
</OrFilters>
NAME= 字段名(如果你想混淆,你可以只使用 object_id) OPERATOR = SQL 运算符,例如 <,>,=,like,ect。VALUE 是用户输入的内容。
这是最终代码的样子。
Select *
FROM someTable
Where (
vcinvoicenumber like ‘inv12%’
and vcID = ‘asdqwe’
)
Or
(
iSerialNumber = ‘123456’
)
首先是找出有多少“OrFilters”标签。
SELECT @l_OrFilters = COUNT(1)
FROM @p_xParameters.nodes('/root/OrFilters') Tab(Col)
接下来,我们需要一个临时表来保存 XML 文档中的值。
CREATE TABLE #temp
(keyid int IDENTITY(1,1) NOT NULL,value varchar(max))
我们现在为第一个“OrFilters”标签创建一个光标。
DECLARE OrFilter_cursor CURSOR LOCAL
FOR
SELECT Tab.Col.value('@Name','varchar(max)') AS Name
,Tab.Col.value('@Operator','Smallint') AS Operator
,Tab.Col.value('@Value','varchar(max)') AS Value
FROM @p_xParameters.nodes('/root/OrFilters[sql:variable("@l_OrFilters")]/AndFilter') Tab(Col)
为了确保我们有一个有效的字段,我们检查系统表。
SELECT @l_ParameterInName = [all_columns].Name
,@l_ParameterDataType= [systypes].Name
,@l_ParameterIsVariable= Variable
,@l_ParameterMax_length=max_length
,@l_ParameterpPrecision=precision
,@l_ParameterScale =[all_columns].scale
FROM [AprDesktop].[sys].[all_views]
INNER JOIN [AprDesktop].[sys].[all_columns]
ON [all_views].object_id = [all_columns].object_id
INNER JOIN [AprDesktop].[sys].[systypes]
ON [all_columns].system_type_id = [systypes].xtype
WHERE [all_views].name = 'vw_CreditMemo_Lists'
and [all_columns].Name= @l_Name
现在我们将参数保存到临时表
IF @@ROWCOUNT = 1
BEGIN
INSERT INTO #temp (value) SELECT @l_Value
SET @l_FilterKey = @@IDENTITY
.
.
.
我们调用一个将实际构建 where 子句的函数。
SET @l_TemporaryWhere +=
dbo.sfunc_FilterWhereBuilder2(@l_Operator
,@l_ParameterInName
,@l_TemporaryWhere
,CAST(@l_FilterKey AS VARCHAR(10))
,@l_ParameterDataType
,@l_ParameterVariable)
查看这个函数,您可以看到我们使用 case 语句来生成 where 子句字符串。
set @l_CastToType = ' CAST( VALUE as ' + @p_DataType + @p_PrecisionScale + ') '
set @l_CastToString = ' CAST( '+@p_Field+' as VARCHAR(MAX)) '
-- Add the T-SQL statements to compute the return value here
SELECT @l_Return =
CASE
--EQUAL
--ex: vcUID = (select value FROM #temp where keyid = 1)
WHEN @p_Command = 1
THEN @p_Field + ' = (select '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'
--BEGIN WITH
--ex:vcInvoiceNumber LIKE (select value+'%' FROM #temp where keyid = 2)
WHEN @p_Command = 2
THEN @l_CastToString +' LIKE (select value+'+ QUOTENAME('%','''') +' FROM #temp where keyid = ' + @p_KeyValue + ')'
.
.
.
最后调用 sp_execute。
EXECUTE sp_executesql @l_SqlCommand ,@l_Parameters, @p_iDistributorUID
呼叫代码
DECLARE @return_value int
DECLARE @myDoc xml
SET @myDoc =
'<root>
<OrFilters>
<AndFilter Name="vcinvoicenumber" Operator="1" Value="123"/>
</OrFilters>
</root>'
EXEC @return_value = [dbo].[spp_CreditMemo_Request_List_v2]
@p_siShowView = 1,
@p_iDistributorUID = 3667,
@p_xParameters = @myDoc
SELECT 'Return Value' = @return_value
主存储过程
ALTER PROCEDURE [dbo].[MyReport]
-- Add the parameters for the stored procedure here
@p_iDistributorUID INT , --manditory
@p_xParameters XML = null --optional parameters(hostile)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE @l_TemporaryWhere NVARCHAR(MAX)
-- declare variables
DECLARE @l_SqlCommand NVARCHAR(MAX)
DECLARE @l_Parameters NVARCHAR(MAX)
DECLARE @l_WhereClause NVARCHAR(MAX)
DECLARE @l_OrFilters INT
--cursor variables
DECLARE @l_Name VARCHAR(MAX)
DECLARE @l_Operator SMALLINT
DECLARE @l_Value VARCHAR(MAX)
--variables from the database views
DECLARE @l_ParameterInName NVARCHAR(128)
DECLARE @l_ParameterDataType NVARCHAR(128)
DECLARE @l_ParameterIsVariable BIT
DECLARE @l_ParameterMax_length SMALLINT
DECLARE @l_ParameterpPrecision TINYINT
DECLARE @l_ParameterScale TINYINT
--the variable that holds the latest @@identity
DECLARE @l_FilterKey INT
--init local variables
SET @l_SqlCommand =''
SET @l_Parameters =''
SET @l_WhereClause =''
BEGIN TRY
--verify manditory variables
if @p_iDistributorUID is null
raiserror('Null values not allowed for @p_iDistributorUID', 16, 1)
--Build the base query
-- only the fields needed in the tile should be selected
SET @l_SqlCommand =
' SELECT * ' +
' FROM vw_Lists '
--how many "OR" filters are there
SELECT @l_OrFilters = COUNT(1)
FROM @p_xParameters.nodes('/root/OrFilters') Tab(Col)
--create a temp table to
--hold the parameters to send into the sp
CREATE TABLE #temp
(
keyid int IDENTITY(1,1) NOT NULL,value varchar(max)
)
--Cycle through all the "OR" Filters
WHILE @l_OrFilters > 0
BEGIN
SET @l_TemporaryWhere = '';
--Create a cursor of the Next "OR" filter
DECLARE OrFilter_cursor CURSOR LOCAL
FOR
SELECT Tab.Col.value('@Name','varchar(max)') AS Name
,Tab.Col.value('@Operator','Smallint') AS Operator
,Tab.Col.value('@Value','varchar(max)') AS Value
FROM @p_xParameters.nodes('/root/OrFilters[sql:variable("@l_OrFilters")]/AndFilter') Tab(Col)
OPEN OrFilter_cursor
FETCH NEXT FROM OrFilter_cursor
INTO @l_Name, @l_Operator,@l_Value
WHILE @@FETCH_STATUS = 0
BEGIN
--verify the parameter actual exists
-- and get parameter details
SELECT @l_ParameterInName = [all_columns].Name
,@l_ParameterDataType= [systypes].Name
,@l_ParameterIsVariable= Variable
,@l_ParameterMax_length=max_length
,@l_ParameterpPrecision=precision
,@l_ParameterScale =[all_columns].scale
FROM [AprDesktop].[sys].[all_views]
INNER JOIN [sys].[all_columns]
ON [all_views].object_id = [all_columns].object_id
INNER JOIN [sys].[systypes]
ON [all_columns].system_type_id = [systypes].xtype
WHERE [all_views].name = 'vw_CreditMemo_Lists'
and [all_columns].Name= @l_Name
--if the paremeter exists, create a where clause
-- if the parameters does not exists, possible injection
IF @@ROWCOUNT = 1
BEGIN
--insert into the temp table the parameter value
--NOTE: we have turned in the @@identity as the key
INSERT INTO #temp (value) SELECT @l_Value
SET @l_FilterKey = @@IDENTITY
-- if the parameter is variable in length, add the length
DECLARE @l_ParameterVariable VARCHAR(1000)
IF @l_ParameterIsVariable = 1
BEGIN
SET @l_ParameterVariable ='(' + CAST(@l_ParameterMax_length as VARCHAR(MAX)) + ') '
END
ELSE
BEGIN
SET @l_ParameterVariable = ''
END
-- create the where clause for this filter
SET @l_TemporaryWhere +=
dbo.sfunc_FilterWhereBuilder2(@l_Operator
,@l_ParameterInName
,@l_TemporaryWhere
,CAST(@l_FilterKey AS VARCHAR(10))
,@l_ParameterDataType
,@l_ParameterVariable)
END
FETCH NEXT FROM OrFilter_cursor
INTO @l_Name, @l_Operator,@l_Value
END
-- clean up the cursor
CLOSE OrFilter_cursor
DEALLOCATE OrFilter_cursor
--add the and filers
IF @l_TemporaryWhere != ''
BEGIN
--if the where clause is not empty, we need to add an OR
IF @l_WhereClause != ''
BEGIN
SET @l_WhereClause += ' or ';
END
--add temp to where clause including the
SET @l_WhereClause += '(' + @l_TemporaryWhere + ')';
END
--get the next AND set
SET @l_OrFilters = @l_OrFilters - 1
END
--generate the where clause
IF @l_WhereClause != ''
BEGIN
SET @l_WhereClause ='('+ @l_WhereClause + ') AND '
END
--add in the first mandatory parameter
SET @l_WhereClause += ' vw_CreditMemo_Lists.iDistributorUID = @l_iDistributorUID '
SET @l_Parameters += '@l_iDistributorUID int'
--do we need to attach the where clause
if @l_WhereClause IS NOT NULL AND RTRIM(LTRIM(@l_WhereClause)) != ''
BEGIN
SET @l_SqlCommand += ' WHERE '+ @l_WhereClause;
END
print @l_SqlCommand
--query for the data
EXECUTE sp_executesql @l_SqlCommand ,@l_Parameters, @p_iDistributorUID
END TRY
BEGIN CATCH
DECLARE @ErrorUID int;
DECLARE @ErrorMessage NVARCHAR(4000);
DECLARE @ErrorSeverity INT;
DECLARE @ErrorState INT;
SELECT
@ErrorMessage = ERROR_MESSAGE(),
@ErrorSeverity = ERROR_SEVERITY(),
@ErrorState = ERROR_STATE();
--write the to stored procedure log
EXEC @ErrorUID = spp_Errors_CreateEntry @l_SqlCommand
-- Use RAISERROR inside the CATCH block to return error
-- information about the original error that caused
-- execution to jump to the CATCH block.
RAISERROR (@ErrorUID, -- Message text.
@ErrorSeverity, -- Severity.
@ErrorState -- State.
);
IF(CURSOR_STATUS('LOCAL','OrFilter_cursor') >= 0)
BEGIN
CLOSE OrFilter_cursor
END
IF(CURSOR_STATUS('LOCAL','OrFilter_cursor') = -1)
BEGIN
DEALLOCATE OrFilter_cursor
END
END CATCH
return
END
功能
ALTER FUNCTION [dbo].[sfunc_FilterWhereBuilder2]
(
@p_Command SMALLINT ,
@p_Field VARCHAR(1000) ,
@p_WhereClause VARCHAR(MAX) ,
@p_KeyValue VARCHAR(10) ,
@p_DataType VARCHAR(100) = NULL ,
@p_PrecisionScale VARCHAR(100) = NULL
)
RETURNS VARCHAR(MAX)
AS
BEGIN
-- Declare the return variable here
DECLARE @l_Return VARCHAR(MAX)
DECLARE @l_CastToType VARCHAR(4000)
DECLARE @l_CastToString VARCHAR(MAX)
set @l_CastToType = ' CAST( VALUE as ' + @p_DataType + @p_PrecisionScale + ') '
set @l_CastToString = ' CAST( '+@p_Field+' as VARCHAR(MAX)) '
-- Add the T-SQL statements to compute the return value here
SELECT @l_Return =
CASE
--EQUAL
--ex: vcBurnUID = (select value FROM #temp where keyid = 1)
WHEN @p_Command = 1
THEN @p_Field + ' = (select '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'
--BEGIN WITH
--ex:vcInvoiceNumber LIKE (select value+'%' FROM #temp where keyid = 2)
WHEN @p_Command = 2
THEN @l_CastToString +' LIKE (select value+'+ QUOTENAME('%','''') +' FROM #temp where keyid = ' + @p_KeyValue + ')'
--END WITH
--ex:vcInvoiceNumber LIKE (select '%'+value FROM #temp where keyid = 2)
WHEN @p_Command = 4
THEN @l_CastToString +' LIKE (select '+ QUOTENAME('%','''') +'+value FROM #temp where keyid = ' + @p_KeyValue + ')'
--END WITH
--ex:vcInvoiceNumber LIKE (select '%'+value+'%' FROM #temp where keyid = 2)
WHEN @p_Command = 8
THEN @l_CastToString +' LIKE (select '+ QUOTENAME('%','''') +'+value+'+ QUOTENAME('%','''') +' FROM #temp where keyid = ' + @p_KeyValue + ')'
--greater than
--ex: iSerialNumber > (select CAST(value as INT) FROM #temp where keyid = 1)
WHEN @p_Command = 16
THEN @p_Field +' > (select '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'
--greater than equal
--ex: iSerialNumber >= (select CAST(value as INT) FROM #temp where keyid = 1)
WHEN @p_Command = 32
THEN @p_Field +' >= (select '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'
--Less than
--ex: iSerialNumber < (select CAST(value as INT) FROM #temp where keyid = 1)
WHEN @p_Command = 64
THEN @p_Field +' < (select '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'
--less than equal
--ex: iSerialNumber <= (select CAST(value as INT) FROM #temp where keyid = 1)
WHEN @p_Command = 128
THEN @p_Field +' <= (select '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'
--less than equal
--ex: iSerialNumber != (select CAST(value as INT) FROM #temp where keyid = 1)
WHEN @p_Command = 256
THEN @p_Field +' != (select '+@l_CastToType+' FROM #temp where keyid = ' + @p_KeyValue + ')'
--default to an empty string
ELSE ''
END
if @l_Return != '' and LEN(@p_WhereClause) > 1
begin
set @l_Return = ' AND ' + @l_Return
end
-- Return the result of the function
RETURN @l_Return
END