Flexible Parameterübergabe als Filterkriterien für dynamisches SQL in Stored Procedures

Während der Besprechung zu einer Projekterweiterung wurde unter anderem ein Problem besprochen, dass sehr häufig anzutreffen ist – Konkatenation eines SQL-String “am Client” und Versand und Ausführung am SQL Server, um die Daten zu ermitteln. Ich habe vorgeschlagen, die komplette Suchroutine in eine Stored Procedure auszulagern. Dieser Stored Procedure werden dann nur noch die Parameter übergeben und die Konkatenation findet dann in der Prozedur statt. Das komplette SQL-Statement wird dann innerhalb der Stored Procedure ausgeführt und die Daten an den Client zurück geliefert. Die Bedenken von SQL-Injection habe ich widerlegt, indem ich argumentiert habe, dass trotz Variabilität in der Parameterübergabe ausschließlich mit sp_executeSQL und expliziter Parameterübergabe gearbeitet wird. Die Herausforderung war nicht ganz einfach aber ich habe eine funktionierende Lösung entwickeln können, die mehrere Vorteile besitzt.

Problemstellung

Am Client wird mittels Konkatenation ein dynamischer SQL-String aufgebaut, der dann gegen den SQL Server ausgeführt wird. Neben der Gefahr von SQL Injection wurde unter anderem ausgeführt, dass Ausführungspläne nicht wiederverwendet werden können, wenn die SQL-Statements nicht parametrisiert gegen die Datenbank ausgeführt werden. Dadurch wird der Prozedurcache des SQL Servers nur unnötig belastet. Die nachfolgende Lösung basiert darauf, dass ein SQL-Statement nicht mehr auf Seiten des Clients “zusammengebaut” wird sondern in einer Prozedur hinterlegt ist. Die Variablen / Parameter für die Daten werden der Stored Procedure übergeben und in der Prozedur zu einem Querystring konkateniert. Anschließend wird dieser String mittels sp_executeSQL unter Ausnutzung der Übergabe von Parametern ausgeführt und die Daten an den Client zurück geliefert. Durch diese Technik werden gleich drei Probleme beseitigt:

  • SQL Injection wird unterbunden, da nur noch die Werte selbst übertragen werden und keine SQL-Strings mehr vom Client ausgeführt werden müssen
  • Abfragepläne können wiederverwendet werden, da die Abfrage parametrisiert wird und somit wiederverwendet werden kann
  • Änderungen in der Ergebnismenge können schnell implementiert werden und die Clients benötigen keine Updates

Datenstruktur

Um die nachfolgende Technik zu demonstrieren, soll das nachfolgende Datenmodell mit ein paar Datensätzen dienen. Hierbei handelt es sich um eine Relation mit einem einfachen Aufbau.

CREATE TABLE dbo.tbl_exec_demo
(
    id          
int           NOT NULL    IDENTITY (1, 1),
    Company     
varchar(256)  NOT NULL,
    Street      
varchar(128)  NOT NULL,
    ZIP         
char(10)      NOT NULL,
    City       
varchar(128)  NOT NULL,
    FirstName   
varchar(64)   NULL,
    LastName   
varchar(64)   NULL,
    CostCenter  
char(10)      NOT NULL,
    Phone       
char(20)      NULL,
    Fax         
char(20)      NULL,
    Email       
varchar(256)  NULL,

    CONSTRAINT pk_tbl_exec_demo_Id PRIMARY KEY CLUSTERED (Id)
);
GO

Um die Daten mittels dynamischem SQL in Verbindung mit Parametern abzufragen, wird eine Prozedur verwendet, die für vier abzufragende Attribute in der Relation Parameter bereitstellt, in denen der zu suchende Wert gespeichert wird. Per Definition haben diese Variablen einen Standardwert von NULL. Die Parameter haben identische Datentypen wie die Attribute in der Relation selbst.

CREATE PROC dbo.proc_app_SearchDemo
    @Company   
varchar(256)    =    NULL,
    @Street     
varchar(128)    =    NULL,
    @ZIP       
char(10)        =    NULL,
    @City       
varchar(128)    =    NULL
AS
    SET NOCOUNT ON

    DECLARE @base_stmt  nvarchar(1000) = N'SELECT * FROM dbo.tbl_exec_demo $(where)';
    DECLARE @parms      nvarchar(256)  = N'@Company varchar(256), @Street varchar(128), @ZIP char(10), @City varchar(128)';
    DECLARE @where_stmt nvarchar(1000) = N'';

    -- Konkatenation der WHERE-Klausel
    IF @Company IS NOT NULL AND LEN(@Company) != 0
        IF CHARINDEX('%', @Company) != 0
            SET @where_stmt = @where_stmt + 'Company LIKE @Company'
        ELSE
            SET @where_stmt = @where_stmt + 'Company = @Company'

    IF @Street IS NOT NULL AND LEN(@Street) != 0
        IF CHARINDEX('%', @Street) != 0
            SET @where_stmt = @where_stmt + CASE WHEN LEN(@where_stmt) != 0                                                  THEN ' AND '                                                  ELSE ''                                             END + 'Street LIKE @Street'
        ELSE
            SET @where_stmt = @where_stmt + CASE WHEN LEN(@where_stmt) != 0                                                  THEN ' AND '                                                  ELSE ''                                             END + 'Street = @Street'

    IF @ZIP IS NOT NULL AND LEN(@ZIP) != 0
        IF CHARINDEX('%', @ZIP) != 0
            SET
 @where_stmt = @where_stmt + CASE WHEN LEN(@where_stmt) != 0                                                  THEN ' AND '                                                  ELSE ''                                             END + 'ZIP LIKE @ZIP'
        ELSE
            SET @where_stmt = @where_stmt + CASE WHEN LEN(@where_stmt) != 0                                                  THEN ' AND '                                                  ELSE ''                                             END + 'ZIP = @ZIP'

    IF @City IS NOT NULL AND LEN(@City) != 0
        IF CHARINDEX('%', @City) != 0
            SET @where_stmt = @where_stmt + CASE WHEN LEN(@where_stmt) != 0                                                  THEN ' AND '                                                  ELSE ''                                             END + 'City LIKE @City'
        ELSE
            SET @where_stmt = @where_stmt + CASE WHEN LEN(@where_stmt) != 0                                                  THEN ' AND '                                                  ELSE ''                                             END + 'City = @City'

    -- Konkatenation von Basis-Statement und WHERE-Klausel
    SET @base_stmt = REPLACE(@base_stmt, '$(where)', CASE WHEN LEN(@where_stmt) != 0 THEN 'WHERE ' ELSE '' END + @where_stmt);

    -- Ausführung des Statements
    EXEC sp_executeSQL @base_stmt, @parms, @Company = @Company, @Street = @Street, @ZIP = @ZIP, @City = @City;
    SET NOCOUNT OFF GO

Funktionsweise

Die Prozedur besteht im Wesentlichen aus drei Bereichen. Zunächst werden die “Konstanten” in der Prozedur definiert. Hierbei genießen die nachfolgenden Variablen besondere Aufmerksamkeit:

Variable Beschreibung
@base_stmt Diese Variable beinhaltet den generellen Abfragestring, wie er später gegen die Datenbank ausgeführt wird. In dieser Variablen befindet sich ein Synonym $(Where), das zur Laufzeit durch den im zweiten Teil konkatenierten WHERE-Teil der Abfrage ersetzt wird.
@parms Eine Liste von Variablen, die in @base_stmt verwendet werden. Weitere Details zur Verwendung von Variablen in sp_executeSQL finden sich in den Links am Ende dieses Artikels.

Im zweiten Teil der Prozedur werden die übergebenen Variablen ausgewertet. Bei der Überprüfung wird nur dann ein Prädikat generiert, wenn auch tatsächlich ein Wert für diese Variable übergeben worden ist. Um die Prozedur etwas flexibler zu gestalten, kann der Anwender auch “” übergeben, dass wie ein NULL behandelt wird. Wenn einer Variablen ein Wert übergeben wurde, wird innerhalb der Routine überprüft, ob der Anwender ein Suchmuster (%) oder einen exakten Suchparameter übergeben hat. Abhängig davon wird die WHERE-Klausel entweder mit einem “=” oder einem “LIKE” definiert.

Der dritte Teil der Prozedur behandelt abschließend die Bereitstellung und Ausführung der Abfrage. Sind so alle Parameter ausgewertet worden, kann diese WHERE-Klausel in die Variable @base_stmt implementiert werden. Hierzu wird einfach das in @base_stmt enthaltene Synonym durch die generierte WHERE-Klausel ersetzt. Übergibt ein Anwender beispielsweise folgende Parameter:

@Company = “db%”
@ZIP     = “64390”

wird der reine Abfragetext in der Variablen @base_stmt zu

SELECT * FROM dbo.tbl_exec_demo WHERE Company LIKE @Company AND ZIP = @ZIP;

Die Ausführung de Abfrage erfolgt abschließend mittels sp_executeSQL. Da nun parametrisierte Abfragen verwendet werden, können bestehende Ausführungspläne wiederverwendet werden.

Herzlichen Dank fürs Lesen!

Referenzen Links
sp_executeSQL http://msdn.microsoft.com/de-de/library/ms188001.aspx
SQL-Injection http://www.sommarskog.se/dynamic_sql.html
sp_execute und Speicherung von Abfrageplänen http://db-berater.blogspot.de/2012/11/tucken-bei-der-verwendung-von.html