it-swarm.com.de

Parametrisieren Sie den Tabellennamen in dynamischem SQL

Ich habe an einigen gespeicherten Prozeduren mit bedingten Parametern gearbeitet, aber eine davon gibt mir ein Problem, das ich nicht ganz herausfinden kann. Dies ist der Code für die Prozedur:

CREATE PROCEDURE dbo.GetTableData(
    @TblName   VARCHAR(50),
    @Condition VARCHAR(MAX) = NULL,
) AS
BEGIN
    IF(EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @TblName))
        BEGIN
            DECLARE @SQL NVARCHAR(MAX) = N'
            SELECT * FROM @TblName WHERE 1=1'
            + CASE WHERE @Condition IS NOT NULL THEN
            ' AND ' + @Condition ELSE N'' END

            DECLARE @params NVARCHAR(MAX) = N'
                @TblName   VARCHAR(50),
                @Condition VARCHAR(MAX)';

            PRINT @SQL

            EXEC sys.sp_executesql @SQL, @params,
                @TblName,
                @Condition
        END
    ELSE
        RETURN 1
END

Die Art und Weise, wie ich möchte, dass das Verfahren funktioniert, ist, dass ich schnell nach Tabellen suchen kann. Wenn ich also alles von meiner Teiletabelle aus sehen möchte, würde ich einfach rennen

EXEC GetTableData 'parts'

Oder wenn ich alles in der Teiletabelle mit einem bestimmten Lieferanten sehen wollte, den ich ausführen würde

EXEC GetTableData 'parts', 'supplier LIKE ''A2A Systems'''

Wenn ich es im obigen Beispiel ausführe, wird das PRINT @SQL Zeile druckt die Abfrage wie folgt aus:

SELECT * FROM @TblName WHERE 1 = 1 UND Lieferant WIE 'A2A Systems'

Also die Abfrage wird richtig zusammengestellt (es scheint).

Nach dem Drucken wird jedoch der folgende Fehler angezeigt:

Meldung 1087, Ebene 16, Status 1, Zeile 4

Muss die Tabellenvariable "@TblName" deklarieren

Ich erhalte immer noch diesen Fehler, wenn ich die Zeile EXEC in ändere:

EXEC GetTableData @TblName='parts', @Condition='supplier LIKE ''A2A Systems'''

Also, was mache ich hier falsch? Warum nimmt es nicht mein @TblName variabler Wert?

2
Skitzafreak

Sie müssen Ihr Verfahren folgendermaßen ändern:

CREATE PROCEDURE dbo.GetTableData(
@TblName   VARCHAR(50),
@Condition VARCHAR(MAX) = NULL
) AS
BEGIN
    IF(EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @TblName))
        BEGIN
            DECLARE @SQL NVARCHAR(MAX) = N'
            SELECT * FROM ' + @TblName + 'WHERE 1=1'
        + CASE WHEN @Condition IS NOT NULL THEN
        ' AND ' + @Condition ELSE N'' END

        DECLARE @params NVARCHAR(MAX) = N'
            @TblName   VARCHAR(50),
            @Condition VARCHAR(MAX)';

        PRINT @SQL

        EXEC sys.sp_executesql @SQL, @params,
            @TblName,
            @Condition
    END
ELSE
    RETURN 1
END

Ihre Variable @TblName darf sich nicht in der @ SQL-Zeichenfolge befinden

Sie können Entitätsnamen (Tabellen, Spalten, Ansichten usw.) nicht parametrisieren. Sie müssen es riskanter machen:

        DECLARE @SQL NVARCHAR(MAX) = N'
        SELECT * FROM ' + QUOTENAME(@TblName) + N' WHERE 1=1'
        + CASE WHERE @Condition IS NOT NULL THEN
        ' AND ' + @Condition ELSE N'' END

        DECLARE @params NVARCHAR(MAX) = N'
            @Condition VARCHAR(MAX)';

        PRINT @SQL

        EXEC sys.sp_executesql @SQL, @params,
            @Condition

QUOTENAME() reicht normalerweise aus, um vor gefährlicher Ausführung zu schützen (was möglicherweise zu einer SQL-Injection führt). Um die Sicherheit jedoch etwas zu erhöhen, sollten Sie (a) dem Tabellennamen das richtige Schema-Präfix voranstellen (z ...FROM dbo.' + QUOTENAME(@TblName) + ... und (b) zuerst auf Existenz prüfen:

IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = @TblName)
BEGIN
  RAISERROR(N'Nice try, robot.', 11, 1);
  RETURN;
END

DECLARE @SQL ...
7
Aaron Bertrand

Ich werde dies ergänzen, um meine Kommentare an den Fragesteller und die nachfolgenden Antwortenden zu vertuschen.

Wie ich im obigen Kommentar sage:

Dies ist eines der Anti-Patterns, die Codierer, die mit 'traditionellen' Programmiersprachen vertraut sind, die den Sprung zu SQL schaffen, aus Erfahrung herausgeschlagen haben müssen.

SQL ist nicht wie herkömmliche Sprachen und Sie müssen aufhören, daran zu denken, wie es ist.

Was Sie versuchen, wurde von vielen (einschließlich mir) versucht, und wir haben alle unter den Ergebnissen gelitten.

Hinweis : Das Überprüfen von @TblName Gegen INFORMATION_SCHEMA Ist gut - das Anhängen ist sicherer - aber @Condition Ist nie in sp_executesql parametrisieren und ein grelles eval - geformtes Loch in Ihrer App-Sicherheit hinterlassen.

Ein Kommentar ist hier zu klein, um vollständig zu erklären, warum ich die obige Haltung einnehme.

Bei Ihrem Prozedurentwurf nehmen Sie zwei Variablen, @TblName Und @Conditions, Und versuchen, sie in dynamisches SQL zu falten. Wie Sie festgestellt haben, akzeptieren nur bestimmte Teile der SQL-Syntax Variablen. Dies kann grob in Stellen in der Syntax unterteilt werden, an denen Werte erwartet werden (diese können normalerweise mit Variablen geliefert werden) und Stellen, an denen eine syntaktische Struktur erwartet wird (diese können nicht durch Variablen ersetzt werden).

Ich gebe zu, dass manchmal es Schön wäre, wenn bestimmte syntaktische Strukturen Variablen verstehen würden, aber im Fall der Anweisung a SELECT * FROM ... Das ... ist syntaktisch, kein Wert. Als solches muss es direkt in der SQL-Anweisung zusammengesetzt und nicht von einer Variablen bereitgestellt werden.

Sie können eine generische, bedingungslose SELECT * FROM ... Prozedur mit dynamischem SQL erstellen, aber die Regeln zum Generieren von dynamischem SQL lauten, dass Sie alle vom Benutzer eingegebenen Parameter sorgfältig validieren, bereinigen und entsprechend umgehen müssen bevor sie angehängt an eine zusammengesetzte SQL-Zeichenfolge angehängt werden, da es für einen böswilligen Endbenutzer zu einfach ist, Zeichenfolgen anzugeben, die enden würden Ihr zusammengesetzter SQL-Befehl und start ein weiterer Befehl, den der böswillige Endbenutzer steuert. Die Prozedur sp_executesql Kann den Unterschied zwischen Ihrem Befehl und ihrem Befehl nicht erkennen und führt beide in einem einzigen Stapel aus.

Der Name der Zieltabelle ist ein solcher vom Benutzer eingegebener Parameter, der jedoch leicht zu validieren, zu bereinigen und zu entkommen ist. Sie führen bereits den Teil Validierung Mit Ihrer Prüfung gegen INFORMATION_SCHEMA.TABLES Durch. Die Reinigungs-/Fluchtteile wurden in den anderen Antworten (z. B. QUOTENAME) ausführlich behandelt.

Zusätzlich zum Tabellennamen können Sie in Ihrem Prozedurdesign jedoch auch Bedingungen angeben, die die Ergebnisse aus der Zieltabelle einschränken. Aus Ihrer SQL-Komposition geht hervor, dass Sie SQL-ähnliche Klauseln wie foo = 1 AND bar = 'hello world' Übergeben und diese in einer WHERE -Klausel in Ihrer zusammengesetzten SQL-Zeichenfolge verwenden.

Leider sind die Spaltennamen und Operator und Konjunktion Ihrer Klauseln (ob sie AND oder sind OR) sind auch syntaktische Strukturelemente, die nicht von Variablen übergeben werden können. Dies bedeutet, dass Sie sie auch an Ihre zusammengesetzte SQL-Zeichenfolge anhängen müssen, anstatt einen Variablennamen zu verwenden.

Das Validieren, Bereinigen und Entkommen ist jedoch eine viel, viel schwierigere Aufgabe für vom Benutzer eingegebene WHERE -Klauseln, sodass Sie sich einem viel höheren Risiko für Injektionsangriffe aussetzen als für Ein einfacher Tabellenname-Parameter.

Es scheint mir, dass die Absicht der Prozedur in der Frage darin besteht, zu vermeiden SQL zu schreiben. Wenn Sie dies tun möchten - sei es, weil Sie entweder kein Vertrauen in SQL haben oder weil Sie glauben, dass dies das Schreiben Ihrer Anwendungsschicht erleichtert -, sollten Sie in Betracht ziehen, das für die jeweilige Sprache gut unterstützte ORM-Framework zu implementieren Sie tun möchten schreiben.

Wenn es nicht Ihre Absicht ist, das Schreiben von SQL zu vermeiden, sollten Sie tatsächlich SQL schreiben und nicht versuchen, es mit einem Anti-Pattern zu umgehen.

Nach diesem Geschwätz werde ich zeigen, wie schwierig es tatsächlich ist, eine ordnungsgemäß validierte generische SELECT -Prozedur mit beliebigen Klauseln zu schreiben. Beachten Sie, dass ich kategorischnicht empfehle, diesen Code zu verwenden - ich habe ihn nur geschrieben, weil es Freitag ist. Ich garantiere, dass ich nicht jeden möglichen Edge-Fall abgedeckt habe. Wenn Sie es benutzen würden, würde es eines Tages wahrscheinlich versuchen, Sie im Schlaf zu töten.

Wie auch immer, hier geht's:

IF OBJECT_ID('dbo.get_any', 'P') IS NOT NULL DROP PROCEDURE dbo.get_any;
GO
IF TYPE_ID('dbo.GenericCondition') IS NOT NULL DROP TYPE dbo.GenericCondition;
GO

CREATE TYPE dbo.GenericCondition AS TABLE (
     ordinal        INTEGER         IDENTITY(1, 1)
    ,conjunction    VARCHAR(3)      NULL
    ,colname        SYSNAME         NOT NULL
    ,operator       VARCHAR(2)      NOT NULL
    ,value          SQL_VARIANT     NULL
)
GO

CREATE PROCEDURE dbo.get_any (
     @tablename     NVARCHAR(515)
    ,@conditions    dbo.GenericCondition READONLY
)
WITH EXECUTE AS CALLER
AS
    DECLARE @server SYSNAME
           ,@dbname SYSNAME
           ,@schema SYSNAME
           ,@object SYSNAME;

    -- extract component names from the passed table indicator
    SELECT @server = PARSENAME(@tablename, 4)
          ,@dbname = COALESCE(PARSENAME(@tablename, 3), DB_NAME())
          ,@schema = COALESCE(PARSENAME(@tablename, 2), N'dbo')
          ,@object = PARSENAME(@tablename, 1);

    -- check that the server and database exists
    IF (@server IS NULL OR EXISTS (SELECT 1 FROM sys.servers WHERE name = @server))
       AND EXISTS (SELECT 1 FROM sys.databases WHERE name = @dbname)
    BEGIN
        DECLARE @sql NVARCHAR(MAX);
        DECLARE @params NVARCHAR(MAX);
        DECLARE @target NVARCHAR(2000);
        DECLARE @cols TABLE (cname SYSNAME, tname SYSNAME, tsize NVARCHAR(32));

        -- escape the server and database name for use in dynamic queries
        SET @target = CASE WHEN @server IS NOT NULL
                           THEN N'[' + REPLACE(@server, N']', N']]') + N'].'
                           ELSE N''
                           END
                    + N'[' + REPLACE(@dbname, N']', N']]') + N']'

        -- get column information from the target database's system tables
        SET @sql = N'
            SELECT
                 c.name
                ,t.name
                ,CASE WHEN t.name IN (''char'', ''nchar'', ''binary'', ''varchar'', ''nvarchar'', ''varbinary'')
                      THEN N''('' + COALESCE(CONVERT(NVARCHAR(32), NULLIF(c.max_length, -1)), N''max'') + N'')''
                      WHEN c.max_length = t.max_length
                       AND c.precision = t.precision
                       AND c.scale = t.scale
                      THEN N''''
                      ELSE N''('' + CONVERT(NVARCHAR(32), c.precision) + N'','' + CONVERT(NVARCHAR(32), c.scale) + N'')''
                      END
            FROM ' + @target + N'.sys.objects o
            INNER JOIN ' + @target + N'.sys.schemas s ON s.schema_id = o.schema_id
            INNER JOIN ' + @target + N'.sys.columns c ON c.object_id = o.object_id
            INNER JOIN ' + @target + N'.sys.types t ON c.user_type_id = t.user_type_id
            WHERE s.name = @schema
              AND o.name = @object
              AND o.type_desc IN (''SYSTEM_TABLE'', ''USER_TABLE'', ''VIEW'');
        ';
        SET @params = N'@schema SYSNAME, @object SYSNAME';

        /* debug */-- PRINT ('/* getting types */' + @sql);
        INSERT INTO @cols(cname, tname, tsize)
        EXEC sp_executesql @command = @sql
                          ,@params  = @params
                          ,@schema  = @schema
                          ,@object  = @object;

        /* debug */-- SELECT * FROM @cols;

        -- if we have no columns, then the schema or table does not exist
        IF EXISTS(SELECT 1 FROM @cols) BEGIN
            SET @target = @target
                        + N'.[' + REPLACE(@schema, N']', N']]') + N'].['
                        + REPLACE(@object, N']', N']]') + N']';

            /* debug */-- RAISERROR('/* target = %s /*', 10, 1, @target) WITH NOWAIT;

            -- now we check the columns supplied in any conditions, to make sure they exist
            DECLARE @badlist NVARCHAR(MAX);

            SELECT @badlist = STUFF((SELECT N', "' + colname + N'"'
                                     FROM @conditions
                                     WHERE colname NOT IN (SELECT cname FROM @cols)
                                     FOR XML PATH(N''), TYPE).value(N'.', 'NVARCHAR(MAX)'),
                                     1, 2, N'');

            /* debug */-- RAISERROR('/* badcols = %s /*', 10, 1, @badlist) WITH NOWAIT;
            IF @badlist IS NOT NULL
                RAISERROR('Cannot find column(s) %s in object %s.%s in database "%s" on server "%s"',
                          16, 1, @badlist, @schema, @object, @dbname, @server)
                          WITH NOWAIT;
            ELSE BEGIN
                -- we check the operators in the conditionals now, for valid syntax we support
                SELECT @badlist = STUFF((SELECT N', "' + operator + N'"'
                                         FROM @conditions
                                         WHERE operator NOT IN ('=', '<', '<=', '>', '>=', '<>')
                                         FOR XML PATH(N''), TYPE).value(N'.', 'NVARCHAR(MAX)'),
                                         1, 2, N'');

                /* debug */-- RAISERROR('/* badops = %s /*', 10, 1, @badlist) WITH NOWAIT;
                IF @badlist IS NOT NULL
                    RAISERROR('Invalid operator(s) %s in conditions', 16, 1, @badlist)
                              WITH NOWAIT;
                ELSE BEGIN
                    -- we check the conjunctions, for valid syntax we support
                    SELECT @badlist = STUFF((SELECT N', "' + conjunction + N'"'
                                             FROM @conditions
                                             WHERE (ordinal = 1 AND conjunction IS NOT NULL)
                                                OR (ordinal > 1 AND conjunction NOT IN ('AND', 'OR'))
                                             FOR XML PATH(N''), TYPE).value(N'.', 'NVARCHAR(MAX)'),
                                             1, 2, N'');

                    /* debug */-- RAISERROR('/* badconjs = %s /*', 10, 1, @badlist) WITH NOWAIT;
                    IF @badlist IS NOT NULL
                        RAISERROR('Invalid conjunction(s) %s in conditions', 16, 1, @badlist)
                                  WITH NOWAIT;
                    ELSE BEGIN
                        -- we have done the validations, and can now build our SQL, where
                        -- we use our properly-escaped target and fold in the conditions,
                        -- which we also escape heavily, using a horrid binary/Base64
                        -- conversion below, to cover arbitrary comaprison of as many of
                        -- the standard types as possible...
                        WITH b64 AS (
                            SELECT *,
                                b64 = (SELECT CONVERT(VARBINARY(MAX), value)
                                       FOR XML PATH(''), TYPE, BINARY BASE64)
                                       .value('.', 'VARCHAR(MAX)')
                            FROM @conditions
                        )
                        SELECT @sql = N'SELECT * FROM ' + @target
                                    + COALESCE(
                                      (SELECT NCHAR(13) + NCHAR(10)
                                            + CASE ordinal WHEN 1 THEN N'WHERE' ELSE conjunction END
                                            + N' '
                                            + CASE WHEN x.value IS NULL AND x.operator = '='
                                                   THEN N'[' + REPLACE(colname, N']', N']]') + N'] IS NULL'
                                                   WHEN x.value IS NULL AND x.operator = '<>'
                                                   THEN N'[' + REPLACE(colname, N']', N']]') + N'] IS NOT NULL'
                                                   ELSE N'[' + REPLACE(colname, N']', N']]') + N'] '
                                                      + operator
                                                      + N' CONVERT([' + REPLACE(c.tname, N']', N']]') + N']' + c.tsize
                                                      + N', CONVERT(XML, ''' + b64 + ''').value(''xs:base64Binary(.)'', ''VARBINARY(MAX)''))'
                                                   END
                                        FROM b64 x
                                        INNER JOIN @cols c ON x.colname = c.cname
                                        ORDER BY ordinal
                                        FOR XML PATH(N''), TYPE, BINARY BASE64).value(N'.', N'NVARCHAR(MAX)'),
                                        N'');

                        /* debug */-- PRINT ('/* actual sql */ ' + @sql);

                        EXEC sp_executesql @command = @sql;

                        /* debug */-- RAISERROR('done...', 10, 1) WITH NOWAIT;
                    END
                END
            END
        END
        ELSE RAISERROR(N'Cannot find object "%s.%s" in database "%s" on server "%s"',
                       16, 1, @schema, @object, @dbname, @server)
                       WITH NOWAIT;
    END
    ELSE RAISERROR(N'Cannot find one of server "%s" or database "%s"',
                   16, 1, @server, @dbname)
                   WITH NOWAIT;
GO

Wenn Sie keine Bedingungen wollten, würden Sie es ganz einfach nennen:

EXEC dbo.get_any @tablename = 'dbo.my_target_table'

... und es würde die einfache Anweisung generieren und ausführen:

SELECT * FROM [dbo].[my_target_table]

Wenn Sie haben Bedingungen wollen, würden Sie es mit der folgenden etwas schrecklichen Konvention aufrufen:

DECLARE @c AS GenericCondition;
INSERT INTO @c (conjunction, colname, operator, value)
VALUES (NULL,  'foo', '=', CONVERT(SQL_VARIANT, 1))
      ,('AND', 'bar', '>', 4)
      ,('AND', 'baz', '<', CONVERT(DATETIME, '2008-03-19T00:00:00'))
      ,('OR',  'qux', '<>', 'arrrrghhh!');

EXEC dbo.get_any @tablename = 'dbo.my_target_table'
                ,@conditions = @c;

... und es würde dynamisch generieren und ausführen:

SELECT * FROM [dbo].[my_target_table]
WHERE [foo] = CONVERT([int], CONVERT(XML, 'AAAAAQ==').value('xs:base64Binary(.)', 'VARBINARY(MAX)'))
AND [bar] > CONVERT([int], CONVERT(XML, 'AAAABA==').value('xs:base64Binary(.)', 'VARBINARY(MAX)'))
AND [baz] < CONVERT([datetime], CONVERT(XML, 'AACaZAAAAAA=').value('xs:base64Binary(.)', 'VARBINARY(MAX)'))
OR [qux] <> CONVERT([varchar](100), CONVERT(XML, 'YXJycnJnaGhoIQ==').value('xs:base64Binary(.)', 'VARBINARY(MAX)'))

Das CONVERT-geladene base64/binäre Zeug ist ein gewaltiger Overkill, der es ermöglicht, so viele verschiedene Arten von Datentypen wie möglich bereitzustellen. Sie könnten wahrscheinlich damit durchkommen, implizite Casts von Zeichenfolgentypen für 70% Ihrer Anwendungsfälle zu verwenden. Ich gehe davon aus, dass selbst das schreckliche Base64/Binary-Zeug in mindestens 10% der Anwendungsfälle schwer versagen würde.

Aber ehrlich gesagt weiß ich auf jeden Fall, dass ich lieber schreiben und rennen würde:

SELECT * 
FROM dbo.my_target_table
WHERE foo = 1
  AND bar > 4
  AND baz < '2008-03-19T00:00:00'
  AND qux <> 'arrrrghhh!'

... als der Gräuel, den ich oben zusammengestellt habe. Ich hätte auch mehr Kontrolle über komplexe WHERE Klauseln.

Jetzt geh und bleich deine Augen und betrachte den obigen Ansatz nie wieder!

3
jimbobmcgee