{"id":1908,"date":"2014-11-14T00:00:00","date_gmt":"2014-11-14T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/using-stored-procedures-to-provide-an-applications-business-logic-layer\/"},"modified":"2021-09-29T16:21:32","modified_gmt":"2021-09-29T16:21:32","slug":"using-stored-procedures-to-provide-an-applications-business-logic-layer","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/using-stored-procedures-to-provide-an-applications-business-logic-layer\/","title":{"rendered":"Using Stored Procedures to Provide an Application&#8217;s Business-Logic Layer"},"content":{"rendered":"<p>Using Stored Procedures as an Ap<\/p>\n<div id=\"pretty\">\n<p>It is not a trivial matter to build the sort of multi-tier, web application that every corporation seems to want. It is too easy to end up with a morass of extremely complicated code that is difficult to get working correctly even with extensive testing, and it can furthermore be quite challenging to maintain it in production.<\/p>\n<p>T-SQL Stored Procedures (SPs), along with some views and functions, are a useful way to encapsulate and implement most of an application&#8217;s business logic, especially that which retrieves the underlying data from the tables (master or transaction), and\/or updates it. &#160;This is useful because: <\/p>\n<ul>\n<li>&#160;It allows the database developer to work fairly independently of the application developers.<\/li>\n<li>It means that the application developers could concentrate their efforts on getting the best possible user-interface without needing to understand the relational design of the base tables within the database.<\/li>\n<li>It provides a logical abstraction of the database that could be &#8216;mocked up&#8217; to assist with testing.<\/li>\n<li>It allows a much more robust security regime for the database that reduces the likelihood that SQL Injection can occur.<\/li>\n<\/ul>\n<p>I will describe two basic templates that can be used for many commonly-seen forms and web pages, which can then be elaborated as required.&#160; <\/p>\n<p>Occasionally in this article I will refer to an application that my company recently and successfully developed using this approach, along with the reasons why it was successful. &#160;Without going too far into the details, I can say that in my 7+ years of experience with this company, this application ranks within the top three or four in complexity among all of those I&#8217;ve been intimately involved with.&#160; It was the culmination of refining the approach I will describe, and it was successful because it was delivered with minimum staffing, on-time and within budget.&#160; While it has not been in production long enough yet to fully assess customer satisfaction levels, initial reception has been promising and that is no mean feat considering it was designed to replace a legacy system that had been in use for over twenty years.<\/p>\n<p>Now that I&#8217;ve established those rather lofty expectations, let&#8217;s start by explaining the basic idea within a common business requirement.<\/p>\n<h2>The Header\/Details Form Pattern<\/h2>\n<p>I&#8217;ll start by describing, as an example, a relatively simple but common business document, an invoice, after which I&#8217;ll flesh out the T=SQL that would implement its&#8217; functionality. <\/p>\n<p>There are two parts to an invoice: header and details.<\/p>\n<p>In the invoice&#8217;s header, you&#8217;ll normally find at least the following information <\/p>\n<ul>\n<li>&#160;An invoice number, which is often the primary key in the underlying table and is usually system-generated.<\/li>\n<li>&#160;The invoice date.<\/li>\n<li>&#160;The customer to which the invoice will be sent.<\/li>\n<li>That customer&#8217;s billing details.<\/li>\n<\/ul>\n<p>There may also be other sundry information that you may think of as being part of an invoice&#8217;s footer, because they appear at the end of the physical document once printed. In reality, these are stored with the header.&#160; This does not include the invoice total, which is derived by summing the details.<\/p>\n<p>In the invoice&#8217;s details, there will be:<\/p>\n<ul>\n<li>A line number, or some other way of sequencing the detail lines of the invoice and which uniquely identifies each detail line as unique.<\/li>\n<li>&#160;An item code (the product description would normally be retrieved by joining the item code back to the item table).<\/li>\n<li>&#160;Quantity of the item.<\/li>\n<li>&#160;Price of the item (unit price).<\/li>\n<li>&#160;Extended price, or the product of quantity and price which, being derived from them, need not be actually included in the table.<\/li>\n<\/ul>\n<p>Here is an example of a typical User-interface form that could be designed to create\/maintain an invoice.<\/p>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"372\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image002.jpg\" width=\"623\" alt=\"2097-clip_image002.jpg\" \/><\/p>\n<p>There are several buttons on this form, each of which represent an action that the user can take.&#160; This form would initially be in &#8220;query mode,&#8221; meaning that the user enters the filtering criteria that are required to retrieve the unique invoice record and then presses &#8216;<i>Execute Query&#8217;<\/i>.&#160; The &#8216;<i>Query<\/i>&#8216; button returns the form to the &#8216;query mode&#8217;.&#160; Alternatively, the &#8216;<i>Insert<\/i>&#8216; button may be used to create a new invoice, with the invoice number being auto-generated upon clicking &#8216;<i>Save<\/i>&#8216;.&#160; We&#8217;ve also offered our user the means to &#8216;<i>Delete<\/i>&#8216; an invoice<\/p>\n<p>In the &#8216;Invoice Details&#8217; section, users have the options to update or delete an existing row (U and D buttons), or insert a new row, all through the action buttons provided.&#160; Most modern web applications will replace the buttons with cute icons, with context-sensitive help text that appears as you hover the mouse over them, but the concept remains the same.<\/p>\n<p>The shaded patterns on the form represent text-panels where error messages may be displayed, so you see one panel below the action buttons, for errors that occur when processing the header, and also one above the grid for errors on actions on the details.<\/p>\n<p>Let&#8217;s also mention the basic business rules for using the form.<\/p>\n<ul>\n<li>In order to query (retrieve) an invoice, that invoice&#8217;s number must be supplied.<\/li>\n<li>To create a new invoice, the customer ID must be entered before pressing the &#8216;Insert&#8217; button, so that the existing customer details can be used to populate the relevant default values on the form.<\/li>\n<li>The invoice number for a newly-created invoice is generated when it is first saved.&#160; The format is &#8220;IN&#8221; plus the year of the invoice, plus a running number that must be unique.<\/li>\n<li>The user may change the billing information (e.g., the customer name, address, etc.) with those changes to be applied to the single invoice only. <\/li>\n<\/ul>\n<h2>Implementing Form Actions through SPs<\/h2>\n<p>We can develop two Stored Procedures (SPs) that will together support the functionality of this form.&#160; The first is implemented to perform all of the allowable form actions (buttons) for the header record, including a cascade delete of the details\/header if that is required.&#160; The second, somewhat simpler, SP is developed to support maintenance of the details.&#160; Even though the &#8216;details&#8217; SP is simpler, it will follow the same template or pattern as for the header SP.&#160; We will discuss the differences later.<\/p>\n<p>The design of the SPs is intended to insulate those who are developing the forms from needing to know about the details of the physical implementation of the data in the relational tables.<\/p>\n<p>This will allow DBAs to set access rights to allow only execute privileges on SPs, giving access to the data indirectly via ownership chaining, without having to assign rights to insert, update, delete, etc. directly against the tables (or views). &#160;This effectively prevents almost all possibility of SQL Injection attacks.<\/p>\n<p>So far, other than my rather vague statement that the SPs &#8220;implement each of the form&#8217;s actions,&#8221; I haven&#8217;t mentioned precisely what the SPs will actually do.&#160; This should become clear as we review the template in the next section, but one thing that I&#8217;ll mention right now is that the SPs must return something.<\/p>\n<p>For our invoices form, the header SP should return three tables:<\/p>\n<ul>\n<li>What I will call the &#8220;ReturnErrors&#8221; table, which delivers results from audits performed in the SP.<\/li>\n<li>A table consisting of zero or one row with columns of data that represent the header information, and any additional derived data based on that.<\/li>\n<li>&#160;A third table that consists of the invoice details, with columns that are consistent with the data appearing in the details grid.<\/li>\n<\/ul>\n<p>If you are using .Net as your development environment, you can make that front-end code retrieve data from all three of the tables mentioned above (or more if needed).&#160; I will not go into the details on how this is done, because I&#8217;m not a .Net developer. Suffice it to say that a <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/system.data.dataset(v=vs.110).aspx\">dataset<\/a> or<a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/system.data.odbc.odbcdatareader(v=vs.110).aspx\">  OdbcDataReader<\/a> will store all tables <a href=\"https:\/\/www.simple-talk.com\/dotnet\/.net-framework\/calling-stored-procedures-from-.net-applications\/\">returned  by a stored procedure<\/a> in one action.<\/p>\n<h2>An Invoice Header SP Template<\/h2>\n<p>Below we have a template that can be used generically for any &#8220;header&#8221; type set of actions such as defined for our form, created specifically to maintain our invoice header.&#160; There&#8217;s a few comments interspersed with the code to help you follow along and a more detailed description at the end.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">CREATE PROCEDURE [dbo].[Maintain_Invoice_Header]\r\n-- =============================================================================\r\n-- Author:&#160;&#160;&#160; Dwain.C\r\n-- Create date: 15-Nov-2014\r\n-- Description: This is a template for an SP that maintains the header of an \r\n--&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; invoice document.&#160; \/* Comment blocks *\/ are explanations of the \r\n--&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; sections of the template and should be removed in a customized \r\n--&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; version of the template.\r\n-- =============================================================================\r\n(\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; ** Parameters Section **\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Which includes:\r\n&#160;&#160;&#160; - An @action variable that corresponds to form actions (buttons), along with\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; any other parameters such as the user that is performing the action, \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; which could be used to maintain a record of the person performing the \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; change.\r\n&#160; &#160;&#160;- The filtering criteria (in this case @InvoiceNo), which could be more than\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; one form field, in which case either parameter must return a unique \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; result.\r\n&#160;&#160;&#160; - The data values (form fields) which will be updated on that action.&#160; Note \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; that this includes @InvoiceNo, which is required on save (if the invoice \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; exists) and delete.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; After the @action variable, default values for all parameters are defaulted \r\n&#160;&#160;&#160; to NULL, because the SP will validate (audit) that appropriate values are \r\n&#160;&#160;&#160; provided on each specific action.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; It is a best practice to call this SP using specified parameter names rather \r\n&#160;&#160;&#160; than assuming that the positional placement won't change.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Note that data parameters (including @InvoiceNo) should be identical to the \r\n&#160;&#160;&#160; data types for the underlying table's columns, so that when you publish the \r\n&#160;&#160;&#160; SP's call signature the developers know how that data is stored in the \r\n&#160;&#160;&#160; front-end code.\r\n*******************************************************************************\/\r\n&#160;&#160;&#160; -- @action=0 Query, =1 Insert, =2 Update\/save, =3 Delete\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; TINYINT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@InvoiceDate&#160;&#160;&#160;&#160;&#160;&#160; DATE&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@CustomerName&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@CustomerAddr1&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@CustomerAddr2&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@CustomerZipcode&#160;&#160; VARCHAR(10)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n)\r\nAS BEGIN;\r\n&#160;&#160;&#160; SET NOCOUNT ON;\r\n\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Declarations Section *** \r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; This includes some local control variables (@Error_Code and @Error_Message\r\n&#160;&#160;&#160; will always be among them) and a table variable that is identical in format\r\n&#160;&#160;&#160; to the underlying table we are maintaining, but without the PRIMARY KEY, \r\n&#160;&#160;&#160; FOREIGN KEYS (or other constraints), which allows NULL values in any column.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Note that for a table that has lots of columns, it is a good practice to use \r\n&#160;&#160;&#160; SQL Server Management Studio (SSMS) to script the CREATE TABLE statement and\r\n&#160;&#160;&#160; then edit that result down for the table variable to ensure that column data\r\n&#160;&#160;&#160; types are consistent.\r\n*******************************************************************************\/\r\n&#160;&#160;&#160; DECLARE @Error_Code&#160;&#160;&#160;&#160; INT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 0&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@Error_Message&#160;&#160;&#160;&#160; VARCHAR(MAX)\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@NextInvoiceNo&#160;&#160;&#160;&#160; BIGINT\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@InvoicePrefix&#160;&#160;&#160;&#160; VARCHAR(5);\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; DECLARE @Invoice&#160;&#160;&#160;&#160;&#160;&#160;&#160; TABLE\r\n&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160; NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,InvoiceDate&#160;&#160;&#160;&#160;&#160;&#160;&#160; DATE&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160; NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerName&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160; NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr1&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160; NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr2&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160; NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerZipcode&#160;&#160;&#160; VARCHAR(10)&#160;&#160;&#160;&#160; NULL\r\n&#160;&#160;&#160; );\r\n&#160;&#160;&#160; \r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Parameters Cleansing Section ***\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Convert blank values to NULL, transform any other values to something \r\n&#160;&#160;&#160; appropriate for the @action, etc. \r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- Cleanse\/transform the input parameters\r\n&#160;&#160;&#160; SELECT @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = CASE \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHEN @action IN (0, 1, 2, 3) THEN @action \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ELSE -1 \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; END\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Convert blanks passed to parameters to NULL \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- in case the developers didn't\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULLIF(@InvoiceNo, '')\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULLIF(@CustomerID, '')\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@CustomerName&#160;&#160;&#160;&#160;&#160; = NULLIF(@CustomerName, '')\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@CustomerAddr1&#160;&#160;&#160;&#160; = NULLIF(@CustomerAddr1, '')\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@CustomerAddr2&#160;&#160;&#160;&#160; = NULLIF(@CustomerAddr2, '')\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@CustomerZipcode&#160;&#160; = NULLIF(@CustomerZipcode, '');\r\n&#160;&#160;&#160; \r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Parameters Auditing Section ***\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Parameters auditing generally occur in these three parts in this sequence:\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; 1. Control audits: These audits are performed to make sure that appropriate\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; parameters are passed to the SP on the specific actions.&#160; Generally in\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; a production application you would not see these messages returned to\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; the form, because they represent cases where the developer did not \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; call the SP correctly (so should be corrected during forms testing).\r\n&#160;&#160;&#160; 2. Simple data audits: Includes things like making sure required values\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; (NOT NULL in the database table) are provided, consistency checks\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; between data elements, i.e., anything that does not require a \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; validation against the database.&#160; Another example is auditing a data\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; element against the CHECK constraint for the underlying column.\r\n&#160;&#160;&#160; 3. Complex data audits: These are any audits requiring validating\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; that a specified parameter is valid based on existing rows in other \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; table.&#160; For example, making sure FOREIGN KEY constraints are met.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; It may be necessary in some cases to perform audits of typs 3 mixed in \r\n&#160;&#160;&#160; with audits of type 2 when it is necessary to do several validations in a \r\n&#160;&#160;&#160; specific sequence.&#160; That is permissible, but generally when possible the \r\n&#160;&#160;&#160; sequence should be adhered to.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Always check that @Error_Code still equals zero before you perform the\r\n&#160;&#160;&#160; next audit.&#160; This is especially important for complex audits to avoid\r\n&#160;&#160;&#160; multiple retrievals from the database (keeps the performance up).\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; When commenting any particular audit, specify what the data should be, i.e.,\r\n&#160;&#160;&#160; the opposite information from what the code audit performs (what the data\r\n&#160;&#160;&#160; should not be).&#160; This adds clarity.\r\n*******************************************************************************\/\r\n\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Perform Type 1 (Control) audits\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- After cleansing, @action should be 0,1,2,3\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action NOT IN (0, 1, 2, 3) \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 1;\r\n\r\n&#160;&#160;&#160; -- @InvoiceNo required on query or delete\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action IN (0, 3) AND @InvoiceNo IS NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 2;\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; -- @InvoiceNo not allowed on insert (generated on save)\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action = 1 AND @InvoiceNo IS NOT NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 3;\r\n\r\n&#160;&#160;&#160; -- @CustomerID is required on insert\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action = 1 AND @CustomerID IS NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 4;\r\n&#160;&#160;&#160; \r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Perform Type 2 (Simple Data) audits\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- Validate required (NOT NULL) fields on update\/save&#160;&#160;&#160; \r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action = 2 AND @InvoiceDate IS NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 5;\r\n&#160;&#160;&#160; ELSE IF @Error_Code = 0 AND @action = 2 AND @CustomerID IS NULL&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 6;\r\n&#160;&#160;&#160; ELSE IF @Error_Code = 0 AND @action = 2 AND @CustomerName IS NULL&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 7;\r\n&#160;&#160;&#160; ELSE IF @Error_Code = 0 AND @action = 2 AND @CustomerAddr1 IS NULL&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 8;\r\n&#160;&#160;&#160; ELSE IF @Error_Code = 0 AND @action = 2 AND @CustomerZipcode IS NULL \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 9;\r\n\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Perform Type 3 (Complex Data) audits\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- Check that the invoice exists\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @InvoiceNo IS NOT NULL AND NOT EXISTS\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM dbo.Invoices\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE InvoiceNo = @InvoiceNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ) SELECT @Error_Code = 10;\r\n\r\n&#160;&#160;&#160; -- Confirm foreign key constraints before saving&#160;&#160;&#160; \r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @CustomerID IS NOT NULL AND NOT EXISTS\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM dbo.Customers\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE CustomerID = @CustomerID\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ) SELECT @Error_Code = 11;\r\n\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Actions Section ***\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; This section processes each possible action the user can specify by a \r\n&#160;&#160;&#160; button push on the form.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Only process actions if @Error_Code = 0.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Note that the Insert @action does not really put any data into the \r\n&#160;&#160;&#160; underlying table.&#160; All it does is populate default values for each column\r\n&#160;&#160;&#160; for display on the form.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; The invoice number is not generated until a save action occurs.\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- Perform the Query @action (=0)\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action = 0\r\n&#160;&#160;&#160; -- Store query results in the table variable&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; INSERT INTO @Invoice\r\n&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; InvoiceNo, InvoiceDate, CustomerID, CustomerName\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr1, CustomerAddr2, CustomerZipcode\r\n&#160;&#160;&#160; )\r\n&#160;&#160;&#160; SELECT InvoiceNo, InvoiceDate, CustomerID, CustomerName\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr1, CustomerAddr2, CustomerZipcode\r\n&#160;&#160;&#160; FROM dbo.Invoices\r\n&#160;&#160;&#160; WHERE InvoiceNo = @InvoiceNo;\r\n\r\n&#160;&#160;&#160; -- Perform the Insert @action (=1)\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action = 1&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; -- Store default values in the table variable&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; INSERT INTO @Invoice\r\n&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; InvoiceNo, InvoiceDate, CustomerID, CustomerName\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr1, CustomerAddr2, CustomerZipcode\r\n&#160;&#160;&#160; )\r\n&#160;&#160;&#160; SELECT NULL, GETDATE(), CustomerID, CustomerName\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr1, CustomerAddr2, CustomerZipcode\r\n&#160;&#160;&#160; FROM dbo.Customers\r\n&#160;&#160;&#160; WHERE CustomerID = @CustomerID; \r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; -- Perform the update (and actual Insert) and delete @actions (=2 and 3)\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action IN (2, 3)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; BEGIN\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Additional Explanation for the Actions Section ***\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Below is the code that actually updates the underlying tables.&#160; Always wrap\r\n&#160;&#160;&#160; this in a TRANSACTION with appropriate error handling using TRY\/CATCH.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; MERGE (available in SQL 2008 onwards), is quite useful to minimize the \r\n&#160;&#160;&#160; number of actual SQL statements you need to write to maintain the \r\n&#160;&#160;&#160; underlying table.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; The Counters table (or a SEQUENCE if you're using SQL 2012) generates the\r\n&#160;&#160;&#160; next available invoice number when @InvoiceNo is NULL on save (basically\r\n&#160;&#160;&#160; on an INSERT to the underlying table).&#160; The UPDLOCK hint locks that row of\r\n&#160;&#160;&#160; the table until the TRANSACTION completes, to avoid duplicate invoice\r\n&#160;&#160;&#160; numbers.\r\n*******************************************************************************\/\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; BEGIN TRANSACTION T1;\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; BEGIN TRY;\r\n\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; IF @InvoiceNo IS NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Generate the next invoice number here\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; UPDATE dbo.Counters WITH(UPDLOCK)\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SET @NextInvoiceNo&#160; = CurrentValue = CurrentValue + 1\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@InvoicePrefix = IDPrefix\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE CounterID = 'Invoices';\r\n\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Cascade delete of the details\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; IF @action = 3\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; DELETE FROM dbo.Invoice_Details\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE InvoiceNo = @InvoiceNo;\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- And now perform the save\/delete (t=target table, s=source table)\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; MERGE dbo.Invoices t\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; USING\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160; = ISNULL(@InvoiceNo, @InvoicePrefix + \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; CAST(YEAR(@InvoiceDate) AS CHAR(4))+ \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; CAST(@NextInvoiceNo AS VARCHAR(20)))\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,InvoiceDate&#160;&#160;&#160;&#160;&#160;&#160;&#160; = @InvoiceDate\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = @CustomerID\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerName&#160;&#160;&#160;&#160;&#160;&#160; = @CustomerName\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr1&#160;&#160;&#160;&#160;&#160; = @CustomerAddr1\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr2&#160;&#160;&#160;&#160;&#160; = @CustomerAddr2\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerZipcode&#160;&#160;&#160; = @CustomerZipcode\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ) s\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ON s.InvoiceNo = t.InvoiceNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHEN MATCHED AND @action = 3\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; THEN DELETE\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHEN MATCHED AND @action = 2\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; THEN UPDATE\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SET InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = s.InvoiceNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,InvoiceDate&#160;&#160;&#160;&#160;&#160;&#160;&#160; = s.InvoiceDate\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = s.CustomerID\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerName&#160;&#160;&#160;&#160;&#160;&#160; = s.CustomerName\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr1&#160;&#160;&#160;&#160;&#160; = s.CustomerAddr1\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr2&#160;&#160;&#160;&#160;&#160; = s.CustomerAddr2\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerZipcode&#160;&#160;&#160; = s.CustomerZipcode\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHEN NOT MATCHED\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; THEN INSERT\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; InvoiceNo, InvoiceDate, CustomerID, CustomerName\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr1, CustomerAddr2, CustomerZipcode\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; )\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VALUES\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; s.InvoiceNo, s.InvoiceDate, s.CustomerID, s.CustomerName\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,s.CustomerAddr1, s.CustomerAddr2, s.CustomerZipcode\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; )\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Use OUTPUT to capture the updated results\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Note that on DELETE, all of these values will be NULL\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; OUTPUT INSERTED.InvoiceNo, INSERTED.InvoiceDate, INSERTED.CustomerID\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,INSERTED.CustomerName, INSERTED.CustomerAddr1\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,INSERTED.CustomerAddr2, INSERTED.CustomerZipcode\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; INTO @Invoice;\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; END TRY\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; BEGIN CATCH;\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Some basic error handling\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code&#160; = ERROR_NUMBER()\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@Error_Message = ERROR_MESSAGE();\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'Merge to Invoices table';\r\n\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Useful information for debugging&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_NUMBER() '+&#160;&#160; CAST(ERROR_NUMBER()&#160;&#160;&#160; AS VARCHAR(10));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_SEVERITY() '+ CAST(ERROR_SEVERITY()&#160; AS VARCHAR(10));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_STATE() '+&#160;&#160;&#160; CAST(ERROR_STATE()&#160;&#160;&#160;&#160; &#160; AS VARCHAR(10));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_PROCEDURE() '+CAST(ERROR_PROCEDURE() AS VARCHAR(8000));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_LINE() '+&#160;&#160;&#160;&#160; CAST(ERROR_LINE()&#160;&#160;&#160;&#160;&#160; &#160; AS VARCHAR(100));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_MESSAGE() '+&#160; CAST(ERROR_MESSAGE()&#160;&#160; AS VARCHAR(8000));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'XACT_STATE() '+&#160;&#160;&#160;&#160; CAST(XACT_STATE()&#160;&#160;&#160;&#160;&#160; AS VARCHAR(5));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; END CATCH;\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; IF @Error_Code &lt;&gt; 0 OR XACT_STATE() = -1 ROLLBACK TRANSACTION T1;\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ELSE IF @Error_Code = 0 AND XACT_STATE() = 1 COMMIT TRANSACTION T1;\r\n&#160;&#160;&#160; END;\r\n\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Returned Tables Section ***\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; The table names you can reference in the front-end code.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; In this case:\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; 1. ReturnErrors - an error message suitable for display on the form\r\n&#160;&#160;&#160; 2. InvoiceHeader - All columns needed to refresh all form fields in the\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; invoice header.\r\n&#160;&#160;&#160; 3. InvoiceDetails - All columns needed to refresh all fields in the grid\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; showing the invoice details.\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- This SP returns 3 tables to the front end:\r\n&#160;&#160;&#160; -- ReturnErrors\r\n&#160;&#160;&#160; -- InvoiceHeader\r\n&#160;&#160;&#160; -- InvoiceHeader\r\n\r\n&#160;&#160;&#160; -- ReturnErrors: whether the action was successful\r\n&#160;&#160;&#160; WITH ReturnErrors (ErrorCode, ErrorMessage) AS\r\n&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 0,&#160; 'Requested Operation Successful'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Audit failures&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 1,&#160; 'Invalid action specified'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 2,&#160; 'Invoice number is required for this action'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 3,&#160; 'Invoice number is not allowed for this action'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 4,&#160; 'Customer ID is required for this action'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 5,&#160; 'Not saved - Invoice date is required'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 6,&#160; 'Not saved - Customer ID is required'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 7,&#160; 'Not saved - Customer name is required'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 8,&#160; 'Not saved - Customer address is required'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 9,&#160; 'Not saved - Customer zipcode is required'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 10, 'Specified invoice number does not exist'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 11, 'Specified customer ID does not exist'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- For this case @Error_Code contains a SQL error (e.g., deadlock)\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code, @Error_Message\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE @Error_Code NOT BETWEEN 0 AND 11&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- End of the BETWEEN range must correspond to the number of audit \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- failure error messages above&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; )\r\n&#160;&#160;&#160; SELECT ErrorCode, RequestedAction=@action, ErrorMessage\r\n&#160;&#160;&#160; FROM ReturnErrors ReturnErrors\r\n&#160;&#160;&#160; WHERE @Error_Code = ErrorCode;\r\n\r\n&#160;&#160;&#160; -- InvoiceHeader: Return the invoice header or\r\n&#160;&#160;&#160; -- nothing if there's an error or when action is to delete\r\n&#160;&#160;&#160; SELECT InvoiceNo, InvoiceDate, CustomerID, CustomerName\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,CustomerAddr1, CustomerAddr2, CustomerZipcode\r\n&#160;&#160;&#160; FROM @Invoice InvoiceHeader\r\n&#160;&#160;&#160; WHERE @Error_Code = 0 AND @action &lt;&gt; 3;\r\n\r\n&#160;&#160;&#160; -- InvoiceDetails Return the invoice details \r\n&#160;&#160;&#160; -- InvoiceLineNo is for display in the grid\r\n&#160;&#160;&#160; SELECT InvoiceNo, LineItem, InvoiceLineNo, ItemNo, ItemDescription\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,Quantity, Price, ExtendedPrice\r\n&#160;&#160;&#160; FROM\r\n&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT InvoiceNo, LineItem\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,InvoiceLineNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = ROW_NUMBER() OVER (ORDER BY LineItem)\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,a.ItemNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ItemDescription&#160;&#160;&#160;&#160;&#160;&#160;&#160; = b.ItemDesc\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,Quantity, Price\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ExtendedPrice\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM dbo.Invoice_Details a\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; JOIN dbo.Items b\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ON a.ItemNo = b.ItemNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE InvoiceNo = ISNULL(@InvoiceNo, @InvoicePrefix + \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; CAST(YEAR(@InvoiceDate) AS CHAR(4)) + \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; CAST(@NextInvoiceNo AS VARCHAR(20))) \r\n&#160;&#160;&#160; ) InvoiceDetails\r\n&#160;&#160;&#160; WHERE @Error_Code = 0; \r\n\r\nEND;\r\n<\/pre>\n<p>Let&#8217;s now describe the relevant components of our SP template:<\/p>\n<p class=\"MsoListParagraphCxSpFirst\"> 1.&#160;&#160;&#160;&#160;&#160;&#160; The parameters passed to the SP consist of an @action variable (required) and &#8220;optional&#8221; parameters for each column in the header table.&#160; Some will not really be optional, but required based on the action that is being performed (validated in the SP).<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\"> 2.&#160;&#160;&#160;&#160;&#160;&#160; There&#8217;s a section to define some local variables and also a table variable that has the same columns as our header table, but no primary key.&#160; The attribute on the primary key column (all columns in fact) must allow NULL values.<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\"> 3.&#160;&#160;&#160;&#160;&#160;&#160; There&#8217;s a section to cleanse the input parameters.&#160; Mainly this consists of setting blank values to NULL, in case the forms developers weren&#8217;t particularly thorough in passing NULLs when it makes sense to do so.<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\"> 4.&#160;&#160;&#160;&#160;&#160;&#160; The auditing section, which consists of three parts:<\/p>\n<div class=\"indent\">\n<p class=\"MsoListParagraphCxSpMiddle\"> a.&#160;&#160;&#160;&#160;&#160;&#160; A section that makes sure the SP was called correctly.&#160; These errors should never appear when testing the form (where @Error_Code IN (1, 2, 3, 4)).<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\"> b.&#160;&#160;&#160;&#160;&#160; A section that performs any audits on the data passed in, which should be done in the form instead.&#160; These are audits that do not require a check against a database table.&#160; For example, making sure that required data is supplied, data range checks (such as what might be done by a CHECK constraint on a table column), etc.&#160; These are where <span class=\"monospaced\">@Error_Code IN (5, 6, 7, 8, 9).<\/span><\/p>\n<p class=\"MsoListParagraphCxSpMiddle\"> c.&#160;&#160;&#160;&#160;&#160;&#160; A final section that performs any audits against other tables in the database. &#160;This normally consists of audits to enforce FOREIGN KEY constraints.&#160; Often, other more complex business rules may also be enforced in this section.&#160; For example, suppose you had different types of invoices for different customer types.&#160; You may have an audit here that determines first the customer&#8217;s type and then that the invoice type is valid for that customer.&#160; These are where @Error_Code IN (10,11). <\/p>\n<\/p><\/div>\n<p class=\"MsoListParagraphCxSpMiddle\"> 5.&#160;&#160;&#160;&#160;&#160;&#160; Then there is the &#8220;actions&#8221; section, also broken into three parts:<\/p>\n<div class=\"indent\">\n<p class=\"MsoListParagraphCxSpMiddle\"> a.&#160;&#160;&#160;&#160;&#160;&#160; For @action=0 (query), we simply retrieve the row from the underlying table into our table variable.<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\"> b.&#160;&#160;&#160;&#160;&#160; For @action=1 (insert), we define all the defaults we&#8217;ll return to the form for display (we can call this a stub header).<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\"> c.&#160;&#160;&#160;&#160;&#160;&#160; For @action IN (2,3) (save and delete) we have to define the most complex part of the processing:<\/p>\n<div class=\"indent\">\n<p class=\"MsoListParagraphCxSpMiddle\">                                                                i.      Begin a transaction.  The next steps may, depending on the action, modify multiple tables, so you&#8217;d want all of those to complete successfully.  Or if one of the steps fails, do a ROLLBACK to ensure that none of them completed.<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\">                                                              ii.      Start a TRY\/CATCH block, to deal with any errors during the actual data processing (e.g., a deadlock).<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\">                                                             iii.      Allocate the next invoice number from the Counters table.  In SQL 2012 or later, you may prefer to use a SEQUENCE for this.  We use the UPDLOCK hint on the table to make sure no one else can allocate a sequence number until our transaction completes.<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\">                                                            iv.      Delete the details when the action is &#8216;<i>delete<\/i>&#8216;.<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\">                                                              v.      Do a MERGE that separately handles the update\/delete actions (based on @action) and does an insert of the newly created invoice number on insert.<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\">                                                            vi.      Note how the MERGE dumps the results using an OUTPUT clause into our table variable, including a row of NULL values in the event a delete occurred (we&#8217;ll remove that later).<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\">                                                           vii.      In the CATCH block, we have some generic error handling that is useful when we run the SP in SQL Server Management Studio and need to know more about any errors it generates (displayed in the Messages pane).<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\">                                                         viii.      Then we COMMIT or ROLLBACK the transaction, depending if we had an error or not.<\/p>\n<\/p><\/div>\n<\/p><\/div>\n<p class=\"MsoListParagraphCxSpMiddle\"> 6.       Finally, we return the three tables that we discussed above.  Naming each table something relevant is one way of retrieving the tables using .Net. <\/p>\n<div class=\"indent\">\n<p class=\"MsoListParagraphCxSpMiddle\"> a.&#160;&#160;&#160;&#160;&#160;&#160; <b>ReturnErrors<\/b> containing an error code and error message in one row, the latter of which can be displayed directly on our form.<\/p>\n<p class=\"MsoListParagraphCxSpMiddle\"> b.&#160;&#160;&#160;&#160;&#160; <b>InvoiceHeader<\/b> containing one row to use to refresh the form&#8217;s fields for the header.&#160; Note that if the <b>ErrorCode<\/b> column of the <b>ReturnErrors <\/b>table is not zero (or if this was a delete), no row is returned.<\/p>\n<p class=\"MsoListParagraphCxSpLast\"> c.&#160;&#160;&#160;&#160;&#160;&#160; <b>InvoiceDetails<\/b> contains all the details for this invoice.&#160; Again, no rows are returned for the case of the<b>  ErrorCode<\/b> being non-zero or for a delete action.&#160; Note that two line numbers are returned: <b>LineItem<\/b> (the table-specific line item for an invoice detail record) and <b>InvoiceLineNo<\/b> (a sequential row number used for display purposes only in the form&#8217;s grid).<\/p>\n<\/p><\/div>\n<p>Notice also how we perform the audits only until one of them fails (<b>IF  @Error_Code = 0 AND &#8230;)<\/b> and do not perform the actions unless all audits pass.&#160; We have omitted the invoice total, which would be simple enough to add as a calculated column to the header table as the sum of the details.<\/p>\n<p>We&#8217;ll show some results of testing &#160;for this SP in the next section.<\/p>\n<h2>Some Test Data and Initial Testing Scripts for the Header SP<\/h2>\n<p>Thanks to the wonders of <a href=\"http:\/\/technet.microsoft.com\/en-us\/library\/ms190686(v=sql.105).aspx\">deferred name resolution<\/a>, we could have already created the above SP without the underlying tables being present.&#160; You can run the script below to create the tables which we will use to demonstrate the actions of the header SP.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">CREATE TABLE dbo.Counters\r\n(\r\n&#160;&#160;&#160; CounterID&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(10)&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,CurrentValue&#160;&#160;&#160;&#160;&#160;&#160; BIGINT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,IDPrefix&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(5)&#160;&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,PRIMARY KEY (CounterID)\r\n);\r\n\r\n-- Seed the Counters table (defines the current invoice number) \r\nINSERT INTO dbo.Counters (CounterID, CurrentValue, IDPrefix)\r\nSELECT 'Invoices', 1000001, 'IN';\r\n\r\nCREATE TABLE dbo.Customers\r\n(\r\n&#160;&#160;&#160; CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,CustomerName&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,CustomerAddr1&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,CustomerAddr2&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160; NULL\r\n&#160;&#160;&#160; ,CustomerZipcode&#160;&#160;&#160; VARCHAR(10)&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; PRIMARY KEY (CustomerID)\r\n);\r\n\r\n-- Seed the Customers table \r\nINSERT INTO dbo.Customers \r\n(\r\n&#160;&#160;&#160; CustomerID, CustomerName, CustomerAddr1, CustomerAddr2, CustomerZipcode\r\n)\r\nSELECT '20001', 'ACME MINERALS', '35 WESTCHESTER ST', 'CAIRNS, AU', '4810'\r\nUNION ALL SELECT '20002', 'ACE CHEMICALS', '22 BROAD AVE', 'CAIRNS, AU', '4810';\r\n\r\nCREATE TABLE dbo.Items\r\n(\r\n&#160;&#160;&#160; ItemNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,ItemDesc&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,ItemPrice&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; MONEY&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; PRIMARY KEY (ItemNo)\r\n);\r\n\r\n-- Seed the Items table \r\nINSERT INTO dbo.Items (ItemNo, ItemDesc, ItemPrice)\r\nSELECT '1000055', 'ORE SORTER', 233.22\r\nUNION ALL SELECT '1000056', 'CONVEYER BELT', 189.33\r\nUNION ALL SELECT '1000057', 'LATHE', 1032.22\r\nUNION ALL SELECT '1000058', 'MURIATIC ACID', 31.11\r\nUNION ALL SELECT '1000059', 'FULMINATE OF MERCURY', 212.23;\r\n\r\nCREATE TABLE dbo.Invoices\r\n(\r\n&#160;&#160;&#160; InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,InvoiceDate&#160;&#160;&#160;&#160;&#160;&#160;&#160; DATE&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,CustomerName&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,CustomerAddr1&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,CustomerAddr2&#160;&#160;&#160;&#160;&#160; VARCHAR(100)&#160;&#160;&#160; NULL\r\n&#160;&#160;&#160; ,CustomerZipcode&#160;&#160;&#160; VARCHAR(10)&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,PRIMARY KEY (InvoiceNo)\r\n&#160;&#160;&#160; ,CONSTRAINT inv_fk1 FOREIGN KEY (CustomerID) REFERENCES dbo.Customers (CustomerID)\r\n);\r\n\r\n-- Seed the Invoices table\r\nINSERT INTO dbo.Invoices \r\n(\r\n&#160;&#160;&#160; InvoiceNo, InvoiceDate, CustomerID, CustomerName\r\n&#160;&#160;&#160; ,CustomerAddr1, CustomerAddr2, CustomerZipcode\r\n)\r\nSELECT 'IN20141000001', '2014-11-03', '20001', 'ACME MINERALS', '35 WESTCHESTER ST'\r\n,'CAIRNS, AU', '4810';\r\n\r\nCREATE TABLE dbo.Invoice_Details\r\n(\r\n&#160;&#160;&#160; InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,LineItem&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; BIGINT IDENTITY\r\n&#160;&#160;&#160; ,ItemNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,Quantity&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; INT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,Price&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; MONEY&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; NOT NULL\r\n&#160;&#160;&#160; ,ExtendedPrice&#160;&#160;&#160;&#160;&#160; AS (Quantity * Price)\r\n&#160;&#160;&#160; ,PRIMARY KEY (InvoiceNo, LineItem)\r\n&#160;&#160;&#160; ,CONSTRAINT invd_fk1 FOREIGN KEY (InvoiceNo) REFERENCES dbo.Invoices (InvoiceNo)\r\n&#160;&#160;&#160; ,CONSTRAINT invd_fk2 FOREIGN KEY (ItemNo) REFERENCES dbo.Items (ItemNo)\r\n);\r\n\r\n-- Seed the Invoice Details table\r\nINSERT INTO dbo.Invoice_Details (InvoiceNo, ItemNo, Quantity, Price)\r\nSELECT 'IN20141000001', '1000055', 12, 233.22\r\nUNION ALL SELECT 'IN20141000001', '1000056', 3, 189.33;\r\n\r\n\/*\r\nGO\r\nDROP PROCEDURE dbo.Maintain_Invoice_Header;\r\nDROP PROCEDURE dbo.Maintain_Invoice_Details;\r\nDROP TABLE dbo.Invoice_Details;\r\nDROP TABLE dbo.Invoices;\r\nDROP TABLE dbo.Items;\r\nDROP TABLE dbo.Customers;\r\nDROP TABLE dbo.Counters;\r\n*\/\r\n<\/pre>\n<p>Note that while you may take exception to some of my chosen data types, and the simplifications that I made regarding prices appearing on price lists and selling units of measure, please indulge me as we&#8217;re not here today to demonstrate how best to structure such data.<\/p>\n<p>The DROPs are provided as a &#8216;teardown&#8217; code to help you clean up your sandbox once all is said and done.<\/p>\n<p>Let&#8217;s now look at some test scripts and their results.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Query an invoice that doesn't exist\r\nEXEC dbo.Maintain_Invoice_Header\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 0\r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 'IN20141000002';\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"154\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image003.png\" width=\"626\" alt=\"2097-clip_image003.png\" \/><\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Query an existing invoice\r\nEXEC dbo.Maintain_Invoice_Header\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 0\r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 'IN20141000001';\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"168\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image005.jpg\" width=\"624\" alt=\"2097-clip_image005.jpg\" \/><\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Insert mode: specify a Customer ID that doesn't exist\r\nEXEC dbo.Maintain_Invoice_Header\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 1\r\n&#160;&#160;&#160; ,@CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160; = '20003';\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"162\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image006.png\" width=\"624\" alt=\"2097-clip_image006.png\" \/><\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Insert mode: specify a Customer ID that does exist\r\nEXEC dbo.Maintain_Invoice_Header\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 1\r\n&#160;&#160;&#160; ,@CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160; = '20002';\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"173\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image007.png\" width=\"624\" alt=\"2097-clip_image007.png\" \/><\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Update the invoice date for an existing invoice\r\nEXEC dbo.Maintain_Invoice_Header\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 2\r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 'IN20141000001'\r\n&#160;&#160;&#160; ,@InvoiceDate&#160;&#160;&#160;&#160;&#160;&#160; = '2014-11-04'\r\n&#160;&#160;&#160; ,@CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160; = '20002'\r\n&#160;&#160;&#160; ,@CustomerName&#160;&#160;&#160;&#160;&#160; = 'ACE CHEMICALS'\r\n&#160;&#160;&#160; ,@CustomerAddr1&#160;&#160;&#160;&#160; = '22 BROAD AVE'\r\n&#160;&#160;&#160; ,@CustomerAddr2&#160;&#160;&#160;&#160; = 'CAIRNS, AU'\r\n&#160;&#160;&#160; ,@CustomerZipcode&#160;&#160; = '4810';\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"173\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image009.jpg\" width=\"624\" alt=\"2097-clip_image009.jpg\" \/><\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Save a new invoice date for the specified customer\r\nEXEC dbo.Maintain_Invoice_Header\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 2\r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@InvoiceDate&#160;&#160;&#160;&#160;&#160;&#160; = '2014-11-03'\r\n&#160;&#160;&#160; ,@CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160; = '20001'\r\n&#160;&#160;&#160; ,@CustomerName&#160;&#160;&#160;&#160;&#160; = 'ACME MINERALS'\r\n&#160;&#160;&#160; ,@CustomerAddr1&#160;&#160;&#160;&#160; = '35 WESTCHESTER ST'\r\n&#160;&#160;&#160; ,@CustomerAddr2&#160;&#160;&#160;&#160; = 'CAIRNS, AU'\r\n&#160;&#160;&#160; ,@CustomerZipcode&#160;&#160; = '4810';\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"144\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image011.jpg\" width=\"623\" alt=\"2097-clip_image011.jpg\" \/><\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Delete an existing invoice (and its details)\r\nEXEC dbo.Maintain_Invoice_Header\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 3\r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 'IN20141000001'\r\n&#160;&#160;&#160; ,@InvoiceDate&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@CustomerID&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@CustomerName&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@CustomerAddr1&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@CustomerAddr2&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@CustomerZipcode&#160;&#160; = NULL;\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"164\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image012.png\" width=\"624\" alt=\"2097-clip_image012.png\" \/><\/p>\n<p>We&#8217;ll leave it to our interested readers to verify that all audit rules trigger the correct error messages when an audit fails.<\/p>\n<h2>An Invoice Details SP Template and some Test Scripts<\/h2>\n<p>As noted earlier, the Invoice details SP is much simpler than for the header.&#160; However it is still based on a trimmed-down version of the header SP&#8217;s template.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">CREATE PROCEDURE dbo.Maintain_Invoice_Details\r\n-- =============================================================================\r\n-- Author:&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; Dwain.C\r\n-- Create date: 15-Nov-2014\r\n-- Description:&#160;&#160;&#160;&#160;&#160; This is a template for an SP that maintains the details of an \r\n--&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; invoice document.&#160; \/* Comment blocks *\/ are explanations of the \r\n--&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; sections of the template and should be removed in a customized \r\n--&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; version of the template.\r\n-- =============================================================================\r\n(\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; ** Parameters Section **\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Which includes:\r\n&#160;&#160;&#160; - An @action variable that corresponds to form actions (buttons), along with\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; any other parameters such as the user that is performing the action, \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; which could be used to maintain a record of the person performing the \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; change.\r\n&#160;&#160;&#160; - The data values (form fields) which will be updated on that action.&#160; Note \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; that this includes @InvoiceNo, which is required on all actions.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; After the @action variable, default values for all parameters are defaulted \r\n&#160;&#160;&#160; to NULL, because the SP will validate (audit) that appropriate values are \r\n&#160;&#160;&#160; provided on each specific action.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; It is a best practice to call this SP using specified parameter names rather \r\n&#160;&#160;&#160; than assuming that the positional placement won't change.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Note that data parameters (including @InvoiceNo) should be identical to the \r\n&#160;&#160;&#160; data types for the underlying table's columns, so that when you publish the \r\n&#160;&#160;&#160; SP's call signature the developers know how that data is stored in the \r\n&#160;&#160;&#160; front-end code.\r\n*******************************************************************************\/\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; TINYINT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- =2 Insert\/update, =3 Delete\r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20) \r\n&#160;&#160;&#160; -- If @LineItem IS NULL then insert on @action=2&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; ,@LineItem&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; BIGINT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL&#160; \r\n&#160;&#160;&#160; ,@ItemNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VARCHAR(20)&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@Quantity&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; INT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; -- If @Price IS NULL then get the price from the Items table\r\n&#160;&#160;&#160; ,@Price&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; MONEY&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL&#160; \r\n)\r\nAS BEGIN;\r\n&#160;&#160;&#160; SET NOCOUNT ON;\r\n\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Declarations Section *** \r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; This includes some local control variables (@Error_Code and @Error_Message\r\n&#160;&#160;&#160; will always be among them).\r\n\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; DECLARE @Error_Code&#160;&#160;&#160;&#160; INT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 0&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@Error_Message&#160;&#160;&#160;&#160; VARCHAR(MAX);\r\n&#160;&#160;&#160; \r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Parameters Cleansing Section ***\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Convert blank values to NULL, transform any other values to something \r\n&#160;&#160;&#160; appropriate for the @action, etc. \r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- Cleanse\/transform the input parameters\r\n&#160;&#160;&#160; SELECT @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = CASE \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHEN @action IN (2, 3) THEN @action \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ELSE -1 \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; END\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULLIF(@InvoiceNo, '')\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,@ItemNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULLIF(@ItemNo, '');\r\n&#160;&#160;&#160; \r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Parameters Auditing Section ***\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Parameters auditing generally occur in these three parts in this sequence:\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; 1. Control audits: These audits are performed to make sure that appropriate\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; parameters are passed to the SP on the specific actions.&#160; Generally in\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; a production application you would not see these messages returned to\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; the form, because they represent cases where the developer did not \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; call the SP correctly (so should be corrected during forms testing).\r\n&#160;&#160;&#160; 2. Simple data audits: Includes things like making sure required values\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; (NOT NULL in the database table) are provided, consistency checks\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; between data elements, i.e., anything that does not require a \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; validation against the database.&#160; Another example is auditing a data\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; element against the CHECK constraint for the underlying column.\r\n&#160;&#160;&#160; 3. Complex data audits: These are any audits requiring validating\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; that a specified parameter is valid based on existing rows in other \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; table.&#160; For example, making sure FOREIGN KEY constraints are met.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; It may be necessary in some cases to perform audits of typs 3 mixed in \r\n&#160;&#160;&#160; with audits of type 2 when it is necessary to do several validations in a \r\n&#160;&#160;&#160; specific sequence.&#160; That is permissible, but generally when possible the \r\n&#160;&#160;&#160; sequence should be adhered to.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Always check that @Error_Code still equals zero before you perform the\r\n&#160;&#160;&#160; next audit.&#160; This is especially important for complex audits to avoid\r\n&#160;&#160;&#160; multiple retrievals from the database (keeps the performance up).\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; When commenting any particular audit, specify what the data should be, i.e.,\r\n&#160;&#160;&#160; the opposite information from what the code audit performs (what the data\r\n&#160;&#160;&#160; should not be).&#160; This adds clarity.\r\n*******************************************************************************\/\r\n\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Perform Type 1 (Control) audits\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- After cleansing, @action should be 2,3\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action NOT IN (2, 3) \r\n&#160;&#160;&#160; SELECT @Error_Code = 1;\r\n\r\n&#160;&#160;&#160; -- @InvoiceNo required on all actions\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @InvoiceNo IS NULL\r\n&#160;&#160;&#160; SELECT @Error_Code = 2;\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; -- @LineItem is required on delete, optional on Insert\/Update\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action = 3 AND @LineItem IS NULL\r\n&#160;&#160;&#160; SELECT @Error_Code = 3;\r\n&#160;&#160;&#160; \r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Perform Type 2 (Simple Data) audits\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- Validate required (NOT NULL) fields on update\/save&#160;&#160;&#160; \r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action = 2 AND @ItemNo IS NULL&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 4;\r\n&#160;&#160;&#160; ELSE IF @Error_Code = 0 AND @action = 2 AND @Quantity IS NULL&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 5;\r\n&#160;&#160;&#160; -- And maybe do some other data range or validity checks\r\n&#160;&#160;&#160; ELSE IF @Error_Code = 0 AND @action = 2 AND @Quantity &lt;=0&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = 6;\r\n\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Perform Type 3 (Complex Data) audits\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- Check that the invoice exists\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @InvoiceNo IS NOT NULL AND NOT EXISTS\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM dbo.Invoices\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE InvoiceNo = @InvoiceNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ) SELECT @Error_Code = 7;\r\n\r\n&#160;&#160;&#160; -- Check that the line number (if specified) exists\r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @LineItem IS NOT NULL AND NOT EXISTS\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM dbo.Invoice_Details\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE InvoiceNo = @InvoiceNo AND LineItem = @LineItem\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ) SELECT @Error_Code = 8;\r\n\r\n&#160;&#160;&#160; -- Confirm foreign key constraints before proceeding&#160;&#160;&#160; \r\n&#160;&#160;&#160; IF @Error_Code = 0 AND @action = 2 AND @ItemNo IS NOT NULL AND NOT EXISTS\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT 1\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM dbo.Items\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE ItemNo = @ItemNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ) SELECT @Error_Code = 9;\r\n\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Actions Section ***\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; This section processes each possible action the user can specify by a \r\n&#160;&#160;&#160; button push on the form.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; Only process actions if @Error_Code = 0.\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- If no errors, then process each action\r\n&#160;&#160;&#160; IF @Error_Code = 0 \r\n&#160;&#160;&#160; BEGIN\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; BEGIN TRANSACTION T1;\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; BEGIN TRY;\r\n\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- And now perform the insert\/update\/delete\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; MERGE dbo.Invoice_Details t\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; USING\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160; = @InvoiceNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,LineItem&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = @LineItem\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ItemNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = @ItemNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,Quantity&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = @Quantity\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160;,Price&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = ISNULL(@Price, ItemPrice)\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Ensure at least one row returned to support delete\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; FROM (SELECT 1) a (n)\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; LEFT JOIN dbo.Items b ON ItemNo = @ItemNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ) s\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ON s.InvoiceNo = t.InvoiceNo AND s.LineItem = t.LineItem\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHEN MATCHED AND @action = 3\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; THEN DELETE\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHEN MATCHED AND @action = 2\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; THEN UPDATE\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SET InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = s.InvoiceNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,ItemNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = s.ItemNo\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,Quantity&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = s.Quantity\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; ,Price&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = s.Price\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHEN NOT MATCHED\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; THEN INSERT\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; InvoiceNo, ItemNo, Quantity, Price\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; )\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; VALUES\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; s.InvoiceNo, s.ItemNo, s.Quantity, s.Price\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; );\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; END TRY\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; BEGIN CATCH;\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Some basic error handling\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code = ERROR_NUMBER(), @Error_Message = ERROR_MESSAGE();\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'Merge to Invoice details table';\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Useful information for debugging \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_NUMBER()' +&#160;&#160;&#160;&#160; CAST(ERROR_NUMBER()&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; AS VARCHAR(10));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_SEVERITY()' +&#160;&#160; CAST(ERROR_SEVERITY()&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160; AS VARCHAR(10));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_STATE()' +&#160;&#160;&#160;&#160;&#160; CAST(ERROR_STATE()&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160; AS VARCHAR(10));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_PROCEDURE() ' + CAST(ERROR_PROCEDURE()&#160;&#160;&#160;&#160;&#160; &#160;&#160;&#160; AS VARCHAR(8000));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_LINE() ' +&#160;&#160;&#160;&#160;&#160; CAST(ERROR_LINE()&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; AS VARCHAR(100));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'ERROR_MESSAGE() ' +&#160;&#160; CAST(ERROR_MESSAGE()&#160;&#160;&#160;&#160;&#160;&#160;&#160; AS VARCHAR(8000));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; PRINT 'XACT_STATE() ' +&#160;&#160;&#160;&#160;&#160; CAST(XACT_STATE()&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; AS VARCHAR(5));\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; END CATCH;\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; IF @Error_Code &lt;&gt; 0 OR XACT_STATE() = -1 ROLLBACK TRANSACTION T1;\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; ELSE IF @Error_Code = 0 AND XACT_STATE() = 1 COMMIT TRANSACTION T1;\r\n&#160;&#160;&#160; END;\r\n\r\n\/*******************************************************************************\r\n&#160;&#160;&#160; *** Returned Tables Section ***\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; The table names you can reference in the front-end code.\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; In this case:\r\n&#160;&#160;&#160; \r\n&#160;&#160;&#160; 1. ReturnErrors - an error message suitable for display on the form\r\n*******************************************************************************\/\r\n\r\n&#160;&#160;&#160; -- SP Returns 1 table to the front end:\r\n&#160;&#160;&#160; -- ReturnErrors\r\n\r\n&#160;&#160;&#160; -- ReturnErrors: whether the action was successful\r\n&#160;&#160;&#160; WITH ReturnErrors (ErrorCode, ErrorMessage) AS\r\n&#160;&#160;&#160; (\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; 0,&#160; 'Requested Operation Successful'&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- Audit failures&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 1,&#160; 'Invalid action specified'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 2,&#160; 'Invoice number is required for this action'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 3,&#160; 'Line item is required for this action'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 4,&#160; 'Not saved - Item number is required'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 5,&#160; 'Not saved - Quantity is required'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 6,&#160; 'Not saved - Quantity must be greater than zero'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 7,&#160; 'Specified invoice number does not exist'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 8,&#160; 'Specified line item does not exist'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL SELECT 9,&#160; 'Specified item number does not exist'\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; UNION ALL \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- For this case @Error_Code contains a SQL error (e.g., deadlock)\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; SELECT @Error_Code, @Error_Message\r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; WHERE @Error_Code NOT BETWEEN 0 AND 9&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- End of the BETWEEN range must correspond to the number of audit \r\n&#160;&#160;&#160;&#160;&#160;&#160;&#160; -- failure error messages above&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; )\r\n&#160;&#160;&#160; SELECT ErrorCode, RequestedAction=@action, ErrorMessage\r\n&#160;&#160;&#160; FROM ReturnErrors ReturnErrors\r\n&#160;&#160;&#160; WHERE @Error_Code = ErrorCode;\r\n\r\nEND;\r\n<\/pre>\n<p>You may notice that, even though there is only one MERGE statement within the TRY\/CATCH block (i.e., no other MERGE, INSERT, UPDATE or DELETE statements), we&#8217;ve still included the TRANSACTION in our template because there could very well be more statements if required by the business logic.<\/p>\n<p>The main difference between the details and header template, is that the details template only returns the one table (<b>ReturnErrors<\/b>) for use by the front end application.&#160; Also it is not necessary to use a table variable to store the details in.&#160; After the details SP is executed, the form simply re-executes the header SP in search mode to recapture any changes to the details (and possibly the header record itself).&#160; The forms developer must then refresh this information to the fields in the form.<\/p>\n<p>Now let&#8217;s test the available actions for the details (from running the test scripts above we now only have invoice number: IN20141000002 available).<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Insert a row to our existing invoice\r\nEXEC dbo.Maintain_Invoice_Details\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 'IN20141000002'&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; ,@LineItem&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL&#160; \r\n&#160;&#160;&#160; ,@ItemNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = '1000058'\r\n&#160;&#160;&#160; ,@Quantity&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 2\r\n&#160;&#160;&#160; ,@Price&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 32.22&#160; -- override price\r\n\r\nSELECT *\r\nFROM dbo.Invoice_Details;\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"124\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image013.png\" width=\"441\" alt=\"2097-clip_image013.png\" \/><\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Insert another row to our existing invoice\r\nEXEC dbo.Maintain_Invoice_Details\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 'IN20141000002'&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; ,@LineItem&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL&#160; \r\n&#160;&#160;&#160; ,@ItemNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = '1000059'\r\n&#160;&#160;&#160; ,@Quantity&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 4\r\n&#160;&#160;&#160; ,@Price&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL&#160; -- default price \r\n\r\nSELECT *\r\nFROM dbo.Invoice_Details;\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"135\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image014.png\" width=\"443\" alt=\"2097-clip_image014.png\" \/><\/p>\n<p>Notice how in each case we&#8217;re selecting all of the rows in our invoice details table to see the effect.&#160; The forms developer would simply rerun the header SP in query mode for the invoice, then pick up the results returned for the details table and refresh that into the grid.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Update price and quantity on LineItem=3\r\nEXEC dbo.Maintain_Invoice_Details\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 2&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 'IN20141000002'&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; ,@LineItem&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 3&#160; \r\n&#160;&#160;&#160; ,@ItemNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = '1000058'\r\n&#160;&#160;&#160; ,@Quantity&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 8\r\n&#160;&#160;&#160; ,@Price&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 30.24&#160; \r\n\r\n-- Then query that invoice\r\nEXEC dbo.Maintain_Invoice_Header\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 0\r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 'IN20141000002';\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"251\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image016.jpg\" width=\"623\" alt=\"2097-clip_image016.jpg\" \/><\/p>\n<p>The first results pane shows the results from calling the details SP.&#160; The remaining three represent the results from rerunning the query against the header record.<\/p>\n<pre class=\"theme:ssms2012 lang:tsql\">-- Delete LineItem=4\r\nEXEC dbo.Maintain_Invoice_Details\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 3&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 'IN20141000002'&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; \r\n&#160;&#160;&#160; ,@LineItem&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 4&#160; \r\n&#160;&#160;&#160; ,@ItemNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@Quantity&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL\r\n&#160;&#160;&#160; ,@Price&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = NULL&#160; \r\n\r\n-- Then query that invoice\r\nEXEC dbo.Maintain_Invoice_Header\r\n&#160;&#160;&#160; @action&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 0\r\n&#160;&#160;&#160; ,@InvoiceNo&#160;&#160;&#160;&#160;&#160;&#160;&#160;&#160; = 'IN20141000002';\r\n<\/pre>\n<p class=\"MsoNormal\"> <img loading=\"lazy\" decoding=\"async\" height=\"230\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/2097-clip_image018.jpg\" width=\"623\" alt=\"2097-clip_image018.jpg\" \/><\/p>\n<h2>Business Logic Left to the Presentation Layer<\/h2>\n<p>The forms developers will still need to implement some specific business logic in the presentation layer where certain actions should be enabled or disabled. For example:<\/p>\n<ul>\n<li>&#160;Enabling\/disabling buttons at various points in the process wherever it makes sense to do so: For example, the Save button should probably only be enabled once an invoice has been retrieved (or the Insert stub is displayed).<\/li>\n<li>&#160;Any role-enabled functionality probably requires some work by the forms developer to operate the form correctly.&#160; For example, suppose there are two types of users that can access the form: normal and super-users.&#160; Perhaps only super-users are allowed to delete an invoice.&#160; The SP should perform this audit, but a more user-friendly forms behavior is to simply disable the Delete button if the user is not a super user.<\/li>\n<li>&#160;Normally, once customer ID is selected and the Insert button pressed, you don&#8217;t want the user changing the customer ID, so making that field display-only probably makes sense.<\/li>\n<li>&#160;After the user performs the Execute Query action for an invoice and that returns an invoice successufully , you&#8217;ll want to make the Invoice number field display-only, because it makes no sense to change the invoice number since they are being auto-generated by the header SP.<\/li>\n<\/ul>\n<p>It is also possible to use the SP itself to flag when some or all of these specific conditions apply.&#160; This can be done by adding additional columns to the <b>ReturnErrors<\/b> table&#8217;s results and then using these as control flags in the front-end code.<\/p>\n<p>If you will follow the strict advice of your DBAs and limit developers&#8217; direct access to tables and views, you&#8217;ll need to take one more step.&#160; For cases where forms populate a lot of dropdown lists, you can write cover FUNCTIONs that return the needed results.&#160; We tend to use schema-bound, in-line Table Valued Functions for best performance.<\/p>\n<h2>Some of the Benefits of the Steps Described Above <\/h2>\n<p>The table below summarizes some of the additional benefits to using this approach.<\/p>\n<table class=\"MsoTableGrid\">\n<tbody>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>No.<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\"><b>Development Phase<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\"><b>Benefit and Remarks<\/b><\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>1<\/b><\/p>\n<\/td>\n<td rowspan=\"2\">\n<p class=\"MsoNormal\"><b>Analysis and Design<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">A simple event-driven model that is the same can be developed for each form based upon the actions to be delivered by the SPs and the user actions (buttons) expected on each form.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>2<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">A skilled SQL Developer should probably be able to translate a requirements specification directly into audits required for a particular form, without the need for an intermediate program specification.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>3<\/b><\/p>\n<\/td>\n<td rowspan=\"2\">\n<p class=\"MsoNormal\"><b>Coding and Unit Testing<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">Forms developers are freed from having to understand the complex business logic underlying the application and can focus on forms behavior and usability improvements.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>4<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">Only essential logic needs to be coded into the SPs on the first pass.&#160; Incremental development of the SPs can add the rest.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>5<\/b><\/p>\n<\/td>\n<td rowspan=\"2\">\n<p class=\"MsoNormal\"><b>Miscellaneous Testing<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">As the SQL Developer writes test scripts, they can be saved as part of the SP documentation, thus allowing any testing team capable of white-box testing to generate many variants on the calling parameters to the SP to ensure defect-free behavior of the SPs.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>6<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">Once forms-development has progressed to the point that they are calling the SPs correctly, using SQL Profiler to trap the SQL generated by the form can allow the creation of multi-step scripts so that complex transactions can be executed repeatedly.&#160; We&#8217;ve used this successfully to generate hundreds of test transactions, within a SQL WHILE loop by simply varying some of the input parameters or assigning random values to them.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>7<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\"><b>Documentation<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">Documentation of the SPs need only be minimal:<\/p>\n<ul>\n<li>The call signature (where parameter data types represent the underlying data type for the database tables).<\/li>\n<li>The returned errors, clearly denoting which audits should never fail when the SP is called correctly, which should be done in the form and which should be done only by the SP.<\/li>\n<li>The columns returned by each of the tables (and of course the table names) returned by the SP.<\/li>\n<\/ul>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>8<\/b><\/p>\n<\/td>\n<td rowspan=\"2\">\n<p class=\"MsoNormal\"><b>Performance<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">Because SPs are compiled and executed as a block of SQL code, they tend to be a little better performing than making multiple database connections and executing individual queries.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>9<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">Eliminating round trips from application to the database.&#160; It is especially crucial to minimize trips from the client, to the application tier and then to the database because of the latency inherent in such a trip.&#160; It is also beneficial in reducing the round trips from the application server to the database server, as each time you make a connection and then close it, it generates latency.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>10<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\"><b>Maintainability<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">Deployment effort was significantly reduced in cases where a fix involved only a change to one of the SPs.&#160; That can be done as an incremental deployment with no need to ask all users to log out while the web application itself is deployed.<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td>\n<p class=\"MsoNormal\"><b>11<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\"><b>Security<\/b><\/p>\n<\/td>\n<td>\n<p class=\"MsoNormal\">Through proper controls within the SP, you can avoid issues with SQL Injection.<\/p>\n<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Some additional information on point #6.&#160; There are automated testing tools that allow complex transactions to be scripted through an application&#8217;s forms.&#160; However, these tend to be expensive, and do not eliminate the requirement for some scripting.&#160; In our case, scripting in SQL is probably a much more widely known skill than writing scripts for an automated testing tool.<\/p>\n<h2>Managing the Development Process<\/h2>\n<p>One of the primary roles of a project manager is to select the right team to execute the project.&#160; This is most certainly true when selecting the person for the role that we&#8217;re calling the SQL Developer.&#160; That is the person that will construct the SPs that enable this method to work.<\/p>\n<p>Some very specific skills are required.&#160; This person must have:<\/p>\n<ul>\n<li>&#160;A deep technical expertise in the dialect of SQL being used, specifically a wide breadth of knowledge of SQL and high-performance code patterns.<\/li>\n<li>The knowledge to translate a logical data model into the physical implementation, and the skills to create all of the necessary SQL objects (tables, views, etc.). <\/li>\n<li>The ability to develop and test quickly, and deliver reasonably defect-free SPs to support at least three or four developers who will be working on the forms.<\/li>\n<li>The ability to make logical decisions on what to include, or what can reasonably be excluded, in the initial SP implementations, because it may be necessary to develop the SPs incrementally so as to stay ahead of the development team.<\/li>\n<li>&#160;The ability to translate (possibly) an incomplete or unclear requirements specification into firm audit rules to be implemented in the SP.<\/li>\n<li>The thoroughness to document the SPs so that the developers can use them during forms-development.&#160; <\/li>\n<\/ul>\n<p>I have found that this documentation need only be sufficient to serve its purpose: the SP&#8217;s call signature, the errors returned and the columns returned in each of the SPs tables, along with some test scripts that can be run to verify that the SP is actually working.&#160; This thoroughness must extend to the data types for the parameters being passed to the SP, because these should be identical to the column data types in the underlying tables.&#160; Providing information to the developers on which of the audits are &#8220;simple data audits,&#8221; and thus should be performed within the form&#8217;s logic is also useful.<\/p>\n<p>In reality of course, the code should also be well-written and commented, to improve troubleshooting, maintenance and the incremental development effort that will go into making the SPs fully functional.&#160; The idea to &#8220;<a href=\"http:\/\/dwaincsql.com\/2014\/04\/01\/make-it-work-make-it-fast-make-it-pretty\/\">Make  it Work, Make it Fast and then Make it Pretty<\/a>&#8221; comes into play here, however to avoid Donald Knuth&#8217;s statement that &#8220;<a href=\"http:\/\/c2.com\/cgi\/wiki?PrematureOptimization\">Premature optimization is the root of all evil<\/a>,&#8221; my recommendation is to leverage best-practices and high-performance code patterns to make it &#8220;fast enough&#8221; at the outset.&#160; For more information on what Knuth&#8217;s quote does not mean, read this article by <a href=\"http:\/\/mvp.microsoft.com\/en-us\/mvp\/gail%20shaw-4020752\">SQL MVP<\/a> Gail Shaw: <a href=\"http:\/\/www.sqlservercentral.com\/articles\/Performance+Tuning\/115825\/\">Premature Optimization<\/a>.<\/p>\n<p>It is probably not sufficient to &#8220;temporarily divert&#8221; a .Net developer to be the SQL Developer, because unless they are very senior they&#8217;ll probably fall short in one or more of the areas mentioned above.<\/p>\n<p>The project manager must plan to give the SQL Developer a head start on the development team.&#160; Probably at least a week should suffice.&#160; After that head start, developers can be scheduled to begin working on visual forms design tasks.&#160; By the time that is complete, there should be sufficiently functional SPs available to begin making the forms work.<\/p>\n<p>For estimation purposes, you can expect that initial development of an SP like the one above for headers should be ready in about 0.5 &#8211; 1.0 man-day including documentation.&#160; Details SPs should be ready in just a few hours.&#160; Note that this is the basic effort to get them ready for the developer to start.&#160; The overall effort is probably double that to make them fully functional and defect-free, and more complex forms and business requirements may take some additional time.<\/p>\n<p>As the SQL Developer completes his work, it is a good idea to turn over his test scripts to a testing team for some &#8220;white box&#8221; testing across different scenarios.<\/p>\n<p>The SQL Developer should also maintain a &#8220;to-do&#8221; list of outstanding, uncompleted items, which can be reviewed periodically and prioritized for implementation.&#160; The project manager should help to prioritize the list, based on progress by the forms-development team.&#160; First priority for the SQL Developer, of course, will always be to fix any issues identified by the forms-development or testing teams, so that his effort is not blocking progress of the forms-development.&#160; <\/p>\n<p>While not specifically discussed in this article, SPs (or views) can also be part of the job of the SQL Developer for reports delivery.&#160; Once again, because reporting requirements may involve complex SQL (and require high-performance), the SQL Developer may be better suited to the task than reports developers.<\/p>\n<p>You may be thinking that it is impossible for one SQL Developer to stay sufficiently ahead of a three-person forms-development team, but I can assure you that it is not impossible because it was done in our reference project <i>with time to spare<\/i>.<\/p>\n<h2>Reflections and Final Commentary<\/h2>\n<p>There were a few variants that came up in our development project, which were relatively easy to handle:<\/p>\n<ul>\n<li>&#160;Some forms had multiple query criteria, each of which would return a unique row of the underlying table; usually the header table.&#160; This was handled by using a <a href=\"http:\/\/www.sqlservercentral.com\/articles\/SQL+catch+all+query\/103529\/\">dynamic search stored procedure<\/a> as recommended by <a href=\"http:\/\/mvp.microsoft.com\/en-us\/mvp\/gail%20shaw-4020752\">SQL MVP<\/a> Gail Shaw in her SQL-in-the-Wild blog: <a href=\"http:\/\/sqlinthewild.co.za\/index.php\/2009\/03\/19\/catch-all-queries\/\">Catch all queries<\/a>. <\/li>\n<li>Some of the forms actually had multiple secondary (details) tables, where one grid may be visible at a time and the user was allowed to switch between the grids.&#160; That&#8217;s another case of some special logic built into the form, but, for that case, the header SP returned more than three tables.<\/li>\n<li>Using SQL Profiler and white-box testing, it was possible to ensure than every audit that should be done in the form, even if done redundantly in the SP, did not invoke a call to the SP.&#160; The overhead of redundancy in the SP tends to be pretty minimal from a performance perspective, so long as it is limited to audits that don&#8217;t check against the database.<\/li>\n<li>&#160;When bugs did show themselves in the SPs, because they were well-written, organized and always followed the same template, the bugs were usually identified and fixed within about fifteen minutes.<\/li>\n<li>As we were testing, none of the forms transactions seemed to respond in an inordinate amount of time (to me that means more than three seconds).&#160; So basically the performance of the resulting forms was at least acceptable, even when the update actions might hit five or more different tables. and we confirmed this with the user during User Acceptance Testing,<\/li>\n<li>In our application, we were careful to include inserted by\/date\/time and updated by\/date\/time on every single record in every header and detail table.&#160; All this was handled within the MERGE statements, and the only requirement imposed on the forms developers was to pass into the SP the login name of the user of the form.<\/li>\n<\/ul>\n<p>I seriously recommend you try this approach because of the benefits we found to the development of our reference application.&#160; It would not have been possible to deliver that level of business logic complexity within the timeframe that we did without using this approach.<\/p>\n<p>Naturally during the initial week after Go Live, some issues were identified. &#160;We were able to rapidly deploy modifications to the business logic by deployment of a single SP.&#160; Regardless of whether this was to correct a bug or improve how the application was able to deliver results to the users, the users&#8217; satisfaction with how quickly those changes could be implemented was frequently communicated to us.&#160; In this case, the users&#8217; business was very time-critical, so deployments of the entire web application requiring locking the users out for a period of time needed to be avoided as much as possible. &#160;The first application deployment after Go Live was not required until after the first week of use.<\/p>\n<p>During that week of Go Live, we also were intensely focused on performance.&#160; The users we were observing were accessing the application over their Intranet, and most form actions were responding <i>instantaneously<\/i>, with just a few of the more complex search operations taking one to two seconds.&#160; While you would not expect remote users of the application to get quite those same performance results, most of the business is conducted at the Go Live site, so our expectations are that performance of the application will meet their needs.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>When faced with a complex business application that had to be delivered with minimum staffing, on-time and within budget, Dwain&#8217;s team chose to encapsulate and implement most of an application&#039;s business logic in SQL Server, using an interface made up of stored procedures. Without this approach, the team was convinced that it would not have been possible to deliver that level of business logic complexity within the timeframe. Dwain explains how it was done, and provides a template.&hellip;<\/p>\n","protected":false},"author":221942,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143531],"tags":[],"coauthors":[6800],"class_list":["post-1908","post","type-post","status-publish","format-standard","hentry","category-t-sql-programming-sql-server"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1908","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221942"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=1908"}],"version-history":[{"count":7,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1908\/revisions"}],"predecessor-version":[{"id":92524,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1908\/revisions\/92524"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=1908"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=1908"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=1908"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=1908"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}