Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 612 Part IV Developing with SQL Server Because the result set of the stored procedure is returned via a function being used by a data source in the FROM clause of the SELECT statement, a WHERE clause can further reduce the output of the stored procedure. While this technique enables the use of stored procedures within a SELECT statement, it’s not as opti- mized as the technique of passing any row restrictions to the stored procedure for processing within the stored procedure. The only benefit of using openquery() is that it enables a complex stored procedure tobecalledfromwithinanadhocquery. For the purpose of the following code, assume that a linked server connection has been established to the local server with the name NOLI: SELECT * FROM OpenQuery( MAUINOLI, ‘EXEC OBXKites.dbo.pProductCategory_Fetch;’) WHERE ProductCategoryDescription Like ‘%stuff%’; Result: ProductCategoryName ProductCategoryDescription OBX OBX stuff Toy Kids stuff Best Practice I f you need to call complex code within a SELECT statement, using openquery() to call a stored procedure works, but the syntax is a bit bizarre. A better method is to use a CASE expression or create a user-defined function. Executing remote stored procedures Two methods exist for calling a stored procedure located on another server: a four-part name reference and a distributed query. Both methods require that the remote server be a linked server. Stored proce- dures may only be called remotely; they may not be created remotely. The remote stored procedure may be executed by means of the four-part name: server.database.schma.procedurename For example, the following code adds a new product category to the OBXKites database on Noli’s (my development server) second instance of SQL Server: EXEC [MAUINoli\SQL2COPENHAGEN].OBXKites.dbo.pProductCategory_AddNew ‘Food’, ‘Eatables’; 612 www.getcoolebook.com Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 613 Developing Stored Procedures 24 Alternately, the OpenQuery() function can be used to call a remote stored procedure: OpenQuery(linked server name, ‘exec ‘EXEC stored procedure;’) The next code sample executes the pCustomerType_Fetch stored procedure in the default database for the user login being used to connect to MAUI\COPENHAGENNoli\SQL2. If the default database is incorrect, a three-part name can be used to point to the correct database. SELECT CustomerTypeName, DiscountPercent, [Default] FROM OPENQUERY( [MAUI\COPENHAGENNoli\SQL2], ‘EXEC OBXKites.dbo.pCustomerType_Fetch;’); Result: CustomerTypeName DiscountPercent Default Preferred 10 0 Retail 00 1 Wholesale 15 0 As with any other distributed query, the Distributed Transaction Coordinator service must be running if the transaction updates data in more than one server. Passing Data to Stored Procedures A stored procedure is more useful when it can be manipulated by parameters. The CategoryList stored procedure created previously returns all the product categories, but a procedure that performs a task on an individual row requires a method for passing the row ID to the procedure. SQL Server stored procedures may have numerous input and output parameters (SQL Server 2005 increased the number of parameters from 1,024 to 2,100 to be exact). Input parameters You can add input parameters that pass data to the stored procedure by listing the parameters after the procedure name in the CREATE PROCEDURE command. Each parameter must begin with an @ sign, and becomes a local variable within the procedure. Like local variables, the parameters must be defined with valid data types. When the stored procedure is called, the parameter must be included (unless the parameter has a default value). The following code sample creates a stored procedure that returns a single product category. The @CategoryName parameter can accept Unicode character input up to 35 characters in length. The value passed by means of the parameter is available within the stored procedure as the variable @CategoryName in the WHERE clause: USE OBXKites; 613 www.getcoolebook.com Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 614 Part IV Developing with SQL Server go CREATE PROCEDURE dbo.CategoryGet ( @CategoryName NVARCHAR(35) ) AS SELECT ProductCategoryName, ProductCategoryDescription FROM dbo.ProductCategory WHERE ProductCategoryName = @CategoryName; go When the following code sample is executed, the Unicode string literal ‘Kite’ is passed to the stored procedure and substituted for the variable in the WHERE clause: EXEC dbo.CategoryGet N’Kite’; Result: ProductCategoryName ProductCategoryDescription Kite a variety of kites, from simple to stunt, to Chinese, to novelty kites If multiple parameters are involved, the parameter name can be specified in any order, or the parame- ter values listed in order. If the two methods are mixed, then as soon as the parameter is provided by name, all the following parameters must be as well. The next four examples each demonstrate calling a stored procedure and passing the parameters by original position and by name: EXEC Schema.StoredProcedure @Parameter1 = n, @Parameter2 =‘n’; EXEC Schema.StoredProcedure @Parameter2 =‘n’, @Parameter1 = n; EXEC Schema.StoredProcedure n,‘n’; EXEC Schema.StoredProcedure n, @Parameter2 =‘n’; Parameter defaults You must supply every parameter when calling a stored procedure, unless that parameter has been cre- ated with a default value. You establish the default by appending an equal sign and the default to the parameter, as follows: CREATE PROCEDURE Schema.StoredProcedure ( @Variable DataType = DefaultValue ) 614 www.getcoolebook.com Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 615 Developing Stored Procedures 24 The following code, extracted from the OBXKites sample database, demonstrates a stored procedure default. If a product category name is passed in this stored procedure, the stored procedure returns only the selected product category. However, if nothing is passed, the NULL default is used in the WHERE clause to return all the product categories: CREATE PROCEDURE dbo.pProductCategory_Fetch2 @Search NVARCHAR(50) = NULL If @Search = null then return all ProductCategories If @Search is value then try to find by Name AS SET NOCOUNT ON; SELECT ProductCategoryName, ProductCategoryDescription FROM dbo.ProductCategory WHERE ProductCategoryName = @Search OR @Search IS NULL; IF @@RowCount = 0 BEGIN; RAISERROR( ‘Product Category ‘’%s" Not Found.’,14,1,@Search); END; The first execution passes a product category: EXEC dbo.pProductCategory_Fetch2 ‘OBX’; Result: ProductCategoryName ProductCategoryDescription OBX OBX stuff When pProductCategory_Fetch executes without a parameter, the @Search parameter’s default of NULL allows the WHERE clause to evaluate to true for every row, as follows: EXEC dbo.pProductCategory_Fetch2; Result: ProductCategoryName ProductCategoryDescription Accessory kite flying accessories Book Outer Banks books Clothing OBX t-shirts, hats, jackets Kite a variety of kites, from simple to stunt, to Chinese, to novelty kites Material Kite construction material OBX OBX stuff Toy Kids stuff Video stunt kite contexts and lessons, and Outer Banks videos 615 www.getcoolebook.com Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 616 Part IV Developing with SQL Server Table-valued parameters New to SQL Server 2008 are table-valued parameters (TVPs). The basic idea is that a table can be created and populated in the client application or T-SQL and then passed, as a table variable, into a stored pro- cedure or user-defined function. This is no small thing. Chapter 1, ‘‘The World of SQL Server,’’ listed TVPs as my favorite new feature for developers and the number two top new feature overall. In every complex application I’ve worked on, there’s been a requirement that a complex transaction be passed to the database — orders and order details, flights and flight legs, or an object and a dynamic set of attributes. In each case, the complete transaction includes multiple types of items. Without considering TVPs, there are three traditional ways to solve the problem of how to pass multiple types of items in a transaction to SQL Server — none of them particularly elegant: ■ Pass each item in a separate stored procedure call — AddNewOrder, AddNewOrder Details — repeatedly for every item, and then CloseNewOrder. This method has two primary problems. First, it’s very chatty, with multiple trips to the server. Second, if the client does not complete the transaction, a process on the server must clean up the remains of the unfinished transaction. ■ Package the entire transaction at the client into XML, pass the XML to SQL Server, and shred the XML into the relational tables. This solution is significantly cleaner than the first method, but there’s still the extra work of shredding the data in the server. ■ Concatenate any data that includes multiple rows into a comma-delimited string and pass that as a varchar(max) parameter into the stored procedure. Like the XML solution, this method does achieve a single stored procedure call for the complex transaction, but it involves even more coding complexity than the XML method and will prove to be difficult to maintain. Table-valued parameters provide a fourth alternative. A table can be created in ADO.NET 3.5 or in T-SQL and passed into a SQL Server stored procedure or user-defined function. The following script creates a test order, order detail scenario: USE tempdb; Create Table dbo.Orders( OrderID INT NOT NULL IDENTITY Constraint OrdersPK Primary Key, OrderDate DateTime, CustomerID INT ); Create Table dbo.OrderDetails( OrderID INT NOT NULL Constraint OrderDetailsFKOrders References Orders, LineNumber SmallInt NOT NULL, ProductID INT ); The chapter file, which may be downloaded from www.sqlserverbible.com, includes both the XML solution (not printed here) and the TVP solution (listed here). 616 www.getcoolebook.com Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 617 Developing Stored Procedures 24 ThefirststepistodefineatabletypeinSQLServer for consistency when creating the TVP: CREATE TYPE OrderDetailsType AS Table ( LineNumber INT, ProductID INT, IsNew BIT, IsDirty BIT, IsDeleted BIT ); Once the table type is created, it can be seen in Object Explorer in the Database ➪ Programmability ➪ Types ➪ User-Defined Table Types node. With the table type established, the stored procedure can now be created that references the table type. The table must be defined as the table type name with the READONLY option. The TVP parameter will look like a table variable inside the stored procedure. Code can reference the data as a normal other data source except that it’s read-only. Some have criticized this as a major drawback. I don’t see it that way. If I want to modify the data and return it, I think the best way is to return the data as a selected result set. Here’s the stored procedure. It simply returns the data from the TVP so it’s easy to see the TVP in action: CREATE alter PROC OrderTransactionUpdateTVP ( @OrderID INT OUTPUT, @CustomerID INT, @OrderDate DateTime, @Details as OrderDetailsType READONLY ) AS SET NoCount ON ; Begin Try Begin Transaction; If @OrderID is NULL then it’s a new order, so Insert Order If @OrderID IS NULL BEGIN; Insert Orders(OrderDate, CustomerID) Values (@OrderDate, @CustomerID); Get OrderID value from insert SET @OrderID = Scope_Identity(); END; Test view of the data SELECT * FROM @Details ; Commit Transaction; End Try 617 www.getcoolebook.com Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 618 Part IV Developing with SQL Server Begin Catch; RollBack; End Catch RETURN; To test table-valued parameters, the following script creates a table variable as the previously defined table type, populates it using the new row constructers, and then calls the stored procedure: Declare @OrderID INT; DECLARE @DetailsTVP as OrderDetailsType; INSERT @DetailsTVP (LineNumber,ProductID,IsNew,IsDirty,IsDeleted) VALUES (5, 101, -1, -1, 0), (2, 999, 0, -1, 0), (3, null, 0, 0, 0); exec OrderTransactionUpdateTVP @OrderID = @OrderID Output , @CustomerID = ‘78’, @OrderDate = ‘2008/07/24’, @Details = @DetailsTVP; Result: LineNumber ProductID IsNew IsDirty IsDeleted 5 101 1 1 0 2 999 0 1 0 3 NULL 0 0 0 I’ve already converted some of my software to TVP and have found that not only does the TVP syntax make sense and simplify a nasty problem with an elegant solution, but TVPs are fast. Under the cov- ers, TVPs leverage SQL Server’s bulk insert technology, which is by far the fastest way to move data into SQL Server. Returning Data from Stored Procedures SQL Server provides five means of returning data from a stored procedure. A batch can return data via a SELECT statement or a RAISERROR command. Stored procedures inherit these from batches and add output variables and the RETURN command. And, the calling stored procedure can create a table that the called stored procedure populates. This section walks through the methods added by stored procedures and clarifies their scope and purpose. 618 www.getcoolebook.com Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 619 Developing Stored Procedures 24 Output parameters Output parameters enable a stored procedure to return data to the calling client procedure. The key- word OUTPUT is required both when the procedure is created and when it is called. Within the stored procedure, the output parameter appears as a local variable. In the calling procedure or batch, a vari- able must have been created to receive the output parameter. When the stored procedure concludes, its current value is passed to the calling procedure’s local variable. Although output parameters are typically used solely for output, they are actually two-way parameters. Best Practice O utput parameters are useful for returning single units of data when a whole record set is not required. For returning a single row of information, using output parameters is blazingly faster than preparing a record set. The next code sample uses an output parameter to return the product name for a given product code from the Product table in the OBXKites sample database. To set up for the output parameter: 1. The batch declares the local variable @ProdName to receive the output parameter. 2. The batch calls the stored procedure, using @ProdName in the EXEC call to the stored procedure. 3. Within the stored procedure, the @ProductName output parameter/local variable is created in the header of the stored procedure. The initial value is NULL until it is initialized by code. 4. Inside the stored procedure, the SELECT statement sets @ProductName to ‘Basic Box Kite 21 inch’ , the product name for the product code ‘1001’. 5. The stored procedure finishes and execution is passed back to the calling batch. The value is transferred to the batch’s local variable, @ProdName. 6. The calling batch uses the PRINT command to send @ProdName to the user. This is the stored procedure: USE OBXKites; go CREATE PROC dbo.GetProductName ( @ProductCode CHAR(10), @ProductName VARCHAR(25) OUTPUT ) AS SELECT @ProductName = ProductName FROM dbo.Product WHERE Code = @ProductCode; RETURN; 619 www.getcoolebook.com Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 620 Part IV Developing with SQL Server This is the calling batch: USE OBXKites; DECLARE @ProdName VARCHAR(25); EXEC dbo.GetProductName ‘1001’, @ProdName OUTPUT; PRINT @ProdName; Result: Basic Box Kite 21 inch Unit Testing I combine agile development with unit testing when designing and developing a database, using three scripts: ■ Create: The first script includes all the DDL code and creates the database, tables, a nd all stored procedures and functions. ■ Sample: The second script includes the sample da ta in the form of INSERT VALUES statements and is used to load the data for unit testing. ■ ProcTest: The last script executes every procedure, checking the output against what is expected from the sample data. Sequentially executing the three scripts unit tests every procedure. Using the Return Command A RETURN command unconditionally terminates the procedure and returns an integer value to the call- ing batch or client. Technically, a return can be used with any batch, but it can only return a value from a stored procedure or a function. When calling a stored procedure, the EXEC command must use a local integer variable if the returned status value is to be captured: EXEC @LocalVariable = StoredProcedureName; I’ve seen stored procedures in production at different companies that use the return code for everything from success/failure, to the number of rows processed, to the @@error code. Personally, I prefer to use the return code for success/failure, and pass back any other data in parameters, or RAISERROR.The most important consideration is that the database is 100% consistent in the use of RETURN. The following basic stored procedure returns a success or failure status, depending on the parameter: 620 www.getcoolebook.com Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 621 Developing Stored Procedures 24 CREATE PROC dbo.IsItOK ( @OK VARCHAR(10) ) AS IF @OK = ‘OK’ BEGIN; RETURN 0; END; ELSE BEGIN; RETURN -100; END; The calling batch: DECLARE @ReturnCode INT; EXEC @ReturnCode = dbo.IsItOK ‘OK’; PRINT @ReturnCode; EXEC @ReturnCode = dbo.IsItOK ‘NotOK’; PRINT @ReturnCode; Result: 0 -100 Path and scope of returning data Any stored procedure has five possible methods of returning data (SELECT, RAISERROR,external table, OUTPUT parameters, and RETURN). Deciding which method is right for a given stored procedure depends on the quantity and purpose of the data to be returned, and the scope of the method used to return the data. The return scope for the five methods is as follows: ■ Selected record sets are passed to the calling stored procedure. If the calling stored procedure consumes the result set (e.g., INSERT EXEC) then the result set ends there. If the calling stored procedure does not consume the result set, then it is passed up to the next calling stored procedure or client. ■ RETURN values, and OUTPUT parameters are all passed to local variables in the immediate calling procedure or batch within SQL Server. ■ RAISERROR is passed to the calling stored procedure and will continue to bubble up until it is trapped by a TRY CATCH or it reaches the client application. If SQL Server Management Studio (the client application) executes a batch that calls stored procedure A, which then calls stored procedure B, there are multiple ways procedure B can pass data back to proce- dure A or to the client application, as illustrated in Figure 24-2. 621 www.getcoolebook.com . videos 615 www.getcoolebook.com Nielsen c24.tex V4 - 07/23/2009 4:53pm Page 616 Part IV Developing with SQL Server Table-valued parameters New to SQL Server 2008 are table-valued parameters (TVPs). The basic idea is. the cov- ers, TVPs leverage SQL Server s bulk insert technology, which is by far the fastest way to move data into SQL Server. Returning Data from Stored Procedures SQL Server provides five means. four -part name: server. database.schma.procedurename For example, the following code adds a new product category to the OBXKites database on Noli’s (my development server) second instance of SQL Server: EXEC