动态查询和Dynamic Where子句解决方案

时间:2013-04-18 20:52:11

标签: sql dynamic where sp-executesql

本周,我发现自己需要一些动态查询。现在,动态查询和动态where子句在整个Web上都不是什么新鲜事物。然而,我还需要更多东西。我需要一种流畅的方式将新字段拉到客户端,并允许用户根据需要制作尽可能多的过滤器。甚至在单个字段上有多个过滤器。更重要的是,我需要访问SQL Server中所有可能的运算符。以下是代码是实现这一目标的一种方法。我将尝试使用底部的完整代码指出代码的亮点。

希望您喜欢这些代码。

要求

  1. 解决方案永远不会允许SQL注入。 (不能使用exec(命令))
  2. 存储过程的调用者可以是任何内容。
  3. 数据集必须来自存储过程。
  4. 任何字段都可以根据需要进行多次过滤,几乎可以进行任何操作。
  5. 应允许任何过滤器组合。
  6. 存储过程应允许强制参数
  7. 首先,让我们看一下参数。

    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运算符,例如&lt;,&gt;,=,like等。 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
    

0 个答案:

没有答案