How to reuse code in SQL stored procedures? - sql

We use SQL Server 2005. All our data access is done through stored procedures. Our selection stored procedures always return multiple result sets.
For instance:
CREATE PROCEDURE hd_invoice_select(#id INT) AS
SELECT * FROM Invoice WHERE InvoiceID = #id
SELECT * FROM InvoiceItem WHERE InvoiceID = #id
SELECT * FROM InvoiceComments WHERE InvoiceID = #id
RETURN
Our application's data access layer builds an object graph based on the results (O/R Mapper style).
The problem I have is that we have many different invoice selection stored procs. They all return the same structure, only for different selection criteria. For instance, I also have:
CREATE PROCEDURE hd_invoice_selectAllForCustomer(#customerID INT) AS
SELECT * FROM Invoice WHERE CustomerID = #customerID
SELECT * FROM InvoiceItem WHERE InvoiceID IN
(SELECT InvoiceID FROM Invoice WHERE CustomerID = #customerID)
SELECT * FROM InvoiceComments WHERE InvoiceID = #id
(SELECT InvoiceID FROM Invoice WHERE CustomerID = #customerID)
RETURN
and I have many others including:
hd_invoice_selectActive()
hd_invoice_selectOverdue()
hd_invoice_selectForMonth(#year INT, #month INT)
and I have the same pattern for a lot of concepts (Customers, Employees, etc)
We end up copying a lot of code and maintenance is really hard. When the "structure" of a concept changes, we have to go and fix all procs and it's very error prone.
So my question is: What is the best way to reuse the code in the scenario?
We came up with a solution that uses temp tables. But it's not very elegant. I'll let you share your ideas and if necessary I will post the detail of my solution in an upcoming post to get your comments on that approach.
Thanks

Posting this as a second answer because it is a different approach. If you are using SQL Server 2008:
CREATE TYPE InvoiceListTableType AS TABLE
(
InvoiceId INT
);
GO
CREATE PROCEDURE hd_invoice_selectFromTempTable
(
#InvoiceList InvoiceListTableType READONLY
)
AS
BEGIN
SELECT * FROM Invoice WHERE InvoiceID IN
(SELECT InvoiceId FROM #InvoiceList)
SELECT * FROM InvoiceItem WHERE InvoiceID IN
(SELECT InvoiceId FROM #InvoiceList)
SELECT * FROM InvoiceComments WHERE InvoiceID IN
(SELECT InvoiceId FROM #InvoiceList)
RETURN
END
GO
CREATE PROCEDURE hd_invoice_select(#id INT) AS
BEGIN
DECLARE #InvoiceList AS InvoiceListTableType;
SELECT id AS ID
INTO #InvoiceList
EXEC hd_invoice_selectFromTempTable(#InvoiceList)
RETURN
END
GO
CREATE PROCEDURE hd_invoice_selectAllForCustomer(#customerID INT) AS
BEGIN
DECLARE #InvoiceList AS InvoiceListTableType;
SELECT invoiceID as ID
INTO #InvoiceList
FROM Invoice WHERE CustomerID = #customerID
EXEC hd_invoice_selectFromTempTable(#InvoiceList)
RETURN
END
GO
CREATE PROCEDURE hd_invoice_selectAllActive AS
BEGIN
DECLARE #InvoiceList AS InvoiceListTableType;
SELECT invoiceID as ID
INTO #InvoiceList
FROM Invoice WHERE Status = 10002
EXEC hd_invoice_selectFromTempTable(#InvoiceList)
RETURN
END
GO

The "best" way for this specific scenario would be to use some sort of code generation. Come up with some sort of convention and plug it into a code generator.

Have you tried putting more than 1 query parameter type in the list of parameters for your main proc? I only wrote the proc to cover the Invoice table, you will need to extend it for your additional tables.
CREATE PROCEDURE hd_invoice_select
(
#id INT = NULL
, #customerId INT = NULL
) AS
BEGIN
SELECT *
FROM Invoice
WHERE
(
#id IS NULL
OR InvoiceID = #id
)
AND (
#customerId IS NULL
OR CustomerID = #customerId
)
RETURN
END
This proc can be called wide open by sending #id and #customerId as NULLs, for a specific InvoiceID based on #id with #customerId as NULL (or just leave it off all together), or for a specific customer based on #customerId leaving #id as NULL or exclude it from the query.
You also should look at views and Table-Valued User-Defined Functions. You can put these in your procs to wrap up some of the logic away from the procs so they can be shared and maintained in a single place. Having some of the logic in views/functions also allows you to deal with the data in a query window as if it were a table.

I'm the person who asked this question in the first place. I'm answering my own question here to let you know the code reuse solution I use and to get your comments on that approach. If this answer gets a lot of up votes, I will select it as the final answer.
This approach works and is simple to use. I don’t know if it has a performance impact because it relies heavily on temporary tables.
For each concept in my application, I have one storec proc like this:
CREATE PROCEDURE hd_invoice_selectFromTempTable AS
/* Get the IDs from an existing #TempInvoiceIDs temporary table */
SELECT * FROM Invoice WHERE InvoiceID IN
(SELECT ID FROM #TempInvoiceIDs)
SELECT * FROM InvoiceItem WHERE InvoiceID IN
(SELECT ID FROM #TempInvoiceIDs)
SELECT * FROM InvoiceComments WHERE InvoiceID IN
(SELECT ID FROM #TempInvoiceIDs)
RETURN
Then I create as many selection stored proc as I need:
CREATE PROCEDURE hd_invoice_select(#id INT) AS
/* Fill #TempInvoiceIDs with matching IDs */
SELECT id AS ID INTO #TempInvoiceIDs
EXEC hd_invoice_selectFromTempTable
RETURN
CREATE PROCEDURE hd_invoice_selectAllForCustomer(#customerID INT) AS
/* Fill #TempInvoiceIDs with matching IDs */
SELECT invoiceID as ID
INTO #TempInvoiceIDs
FROM Invoice WHERE CustomerID = #customerID
EXEC hd_invoice_selectFromTempTable
RETURN
CREATE PROCEDURE hd_invoice_selectAllActive AS
/* Fill #TempInvoiceIDs with matching IDs */
SELECT invoiceID as ID
INTO #TempInvoiceIDs
FROM Invoice WHERE Status = 10002
EXEC hd_invoice_selectFromTempTable
RETURN
What do you think of this approach? It is somewhat similar to AlexKuznetsov's answer but I use temp tables instead of a BLOB parameter.

This is one of the main problems with stored procedures and why people don't like them.
I've never found or seen a way around it.

I have started to use stored procedures generated by Code Generators for my basic CRUD. I use stored procs for reports or complex SQL work.
I have a suggestion unrelated to your question as well - instead of using the IN clause, use the EXISTS clause in your SQL statements.

I've inherited an application that used the temp table approach before and I agree that it's very messy.
On that project we were able to remove a lot of the temp tables by replacing them with Views that contained the desired 'objects' we needed then we updated our stored procedures to query off of those views.
Perhaps that may work in your situation as well.

In some cases I use VIEWS to reuse "code". In cases as filters, active items, outdated things, and so on...

Maybe you should learn to use joins. You could put the basic join of the three tables in a view and just query that with the sp handing the different parameters. Also, you should not in general use select * ever in production code. Only return the few columns you actually need in the circumstances and your whole system will be better performing. Plus you won't have unintended results when people change the structure on you.

I sometimes do it in two steps:
I come up with a list of InvoiceID. Then I call my stored procedure with this list as a parameter.
On 2005 we don't have table valued parameters, so I pack my list of IDs in a binary BLOB and submit it to SQL Server, as described here: Arrays and Lists in SQL Server 2005
You can also submit a list of IDs as a comma-separated list (somewhat slow) or as a concatenation of fixed width string representations (much faster).

Related

dynamic sql not working . Regular sql working [duplicate]

It looks like #temptables created using dynamic SQL via the EXECUTE string method have a different scope and can't be referenced by "fixed" SQLs in the same stored procedure.
However, I can reference a temp table created by a dynamic SQL statement in a subsequence dynamic SQL but it seems that a stored procedure does not return a query result to a calling client unless the SQL is fixed.
A simple 2 table scenario:
I have 2 tables. Let's call them Orders and Items. Order has a Primary key of OrderId and Items has a Primary Key of ItemId. Items.OrderId is the foreign key to identify the parent Order. An Order can have 1 to n Items.
I want to be able to provide a very flexible "query builder" type interface to the user to allow the user to select what Items he want to see. The filter criteria can be based on fields from the Items table and/or from the parent Order table. If an Item meets the filter condition including and condition on the parent Order if one exists, the Item should be return in the query as well as the parent Order.
Usually, I suppose, most people would construct a join between the Item table and the parent Order tables. I would like to perform 2 separate queries instead. One to return all of the qualifying Items and the other to return all of the distinct parent Orders. The reason is two fold and you may or may not agree.
The first reason is that I need to query all of the columns in the parent Order table and if I did a single query to join the Orders table to the Items table, I would be repoeating the Order information multiple times. Since there are typically a large number of items per Order, I'd like to avoid this because it would result in much more data being transfered to a fat client. Instead, as mentioned, I would like to return the two tables individually in a dataset and use the two tables within to populate a custom Order and child Items client objects. (I don't know enough about LINQ or Entity Framework yet. I build my objects by hand). The second reason I would like to return two tables instead of one is because I already have another procedure that returns all of the Items for a given OrderId along with the parent Order and I would like to use the same 2-table approach so that I could reuse the client code to populate my custom Order and Client objects from the 2 datatables returned.
What I was hoping to do was this:
Construct a dynamic SQL string on the Client which joins the orders table to the Items table and filters appropriate on each table as specified by the custom filter created on the Winform fat-client app. The SQL build on the client would have looked something like this:
TempSQL = "
INSERT INTO #ItemsToQuery
OrderId, ItemsId
FROM
Orders, Items
WHERE
Orders.OrderID = Items.OrderId AND
/* Some unpredictable Order filters go here */
AND
/* Some unpredictable Items filters go here */
"
Then, I would call a stored procedure,
CREATE PROCEDURE GetItemsAndOrders(#tempSql as text)
Execute (#tempSQL) --to create the #ItemsToQuery table
SELECT * FROM Items WHERE Items.ItemId IN (SELECT ItemId FROM #ItemsToQuery)
SELECT * FROM Orders WHERE Orders.OrderId IN (SELECT DISTINCT OrderId FROM #ItemsToQuery)
The problem with this approach is that #ItemsToQuery table, since it was created by dynamic SQL, is inaccessible from the following 2 static SQLs and if I change the static SQLs to dynamic, no results are passed back to the fat client.
3 around come to mind but I'm look for a better one:
1) The first SQL could be performed by executing the dynamically constructed SQL from the client. The results could then be passed as a table to a modified version of the above stored procedure. I am familiar with passing table data as XML. If I did this, the stored proc could then insert the data into a temporary table using a static SQL that, because it was created by dynamic SQL, could then be queried without issue. (I could also investigate into passing the new Table type param instead of XML.) However, I would like to avoid passing up potentially large lists to a stored procedure.
2) I could perform all the queries from the client.
The first would be something like this:
SELECT Items.* FROM Orders, Items WHERE Order.OrderId = Items.OrderId AND (dynamic filter)
SELECT Orders.* FROM Orders, Items WHERE Order.OrderId = Items.OrderId AND (dynamic filter)
This still provides me with the ability to reuse my client sided object-population code because the Orders and Items continue to be returned in two different tables.
I have a feeling to, that I might have some options using a Table data type within my stored proc, but that is also new to me and I would appreciate a little bit of spoon feeding on that one.
If you even scanned this far in what I wrote, I am surprised, but if so, I woul dappreciate any of your thoughts on how to accomplish this best.
You first need to create your table first then it will be available in the dynamic SQL.
This works:
CREATE TABLE #temp3 (id INT)
EXEC ('insert #temp3 values(1)')
SELECT *
FROM #temp3
This will not work:
EXEC (
'create table #temp2 (id int)
insert #temp2 values(1)'
)
SELECT *
FROM #temp2
In other words:
Create temp table
Execute proc
Select from temp table
Here is complete example:
CREATE PROC prTest2 #var VARCHAR(100)
AS
EXEC (#var)
GO
CREATE TABLE #temp (id INT)
EXEC prTest2 'insert #temp values(1)'
SELECT *
FROM #temp
1st Method - Enclose multiple statements in the same Dynamic SQL Call:
DECLARE #DynamicQuery NVARCHAR(MAX)
SET #DynamicQuery = 'Select * into #temp from (select * from tablename) alias
select * from #temp
drop table #temp'
EXEC sp_executesql #DynamicQuery
2nd Method - Use Global Temp Table:
(Careful, you need to take extra care of global variable.)
IF OBJECT_ID('tempdb..##temp2') IS NULL
BEGIN
EXEC (
'create table ##temp2 (id int)
insert ##temp2 values(1)'
)
SELECT *
FROM ##temp2
END
Don't forget to delete ##temp2 object manually once your done with it:
IF (OBJECT_ID('tempdb..##temp2') IS NOT NULL)
BEGIN
DROP Table ##temp2
END
Note: Don't use this method 2 if you don't know the full structure on database.
I had the same issue that #Muflix mentioned. When you don't know the columns being returned, or they are being generated dynamically, what I've done is create a global table with a unique id, then delete it when I'm done with it, this looks something like what's shown below:
DECLARE #DynamicSQL NVARCHAR(MAX)
DECLARE #DynamicTable VARCHAR(255) = 'DynamicTempTable_' + CONVERT(VARCHAR(36), NEWID())
DECLARE #DynamicColumns NVARCHAR(MAX)
--Get "#DynamicColumns", example: SET #DynamicColumns = '[Column1], [Column2]'
SET #DynamicSQL = 'SELECT ' + #DynamicColumns + ' INTO [##' + #DynamicTable + ']' +
' FROM [dbo].[TableXYZ]'
EXEC sp_executesql #DynamicSQL
SET #DynamicSQL = 'IF OBJECT_ID(''tempdb..##' + #DynamicTable + ''' , ''U'') IS NOT NULL ' +
' BEGIN DROP TABLE [##' + #DynamicTable + '] END'
EXEC sp_executesql #DynamicSQL
Certainly not the best solution, but this seems to work for me.
I would strongly suggest you have a read through http://www.sommarskog.se/arrays-in-sql-2005.html
Personally I like the approach of passing a comma delimited text list, then parsing it with text to table function and joining to it. The temp table approach can work if you create it first in the connection. But it feel a bit messier.
Result sets from dynamic SQL are returned to the client. I have done this quite a lot.
You're right about issues with sharing data through temp tables and variables and things like that between the SQL and the dynamic SQL it generates.
I think in trying to get your temp table working, you have probably got some things confused, because you can definitely get data from a SP which executes dynamic SQL:
USE SandBox
GO
CREATE PROCEDURE usp_DynTest(#table_type AS VARCHAR(255))
AS
BEGIN
DECLARE #sql AS VARCHAR(MAX) = 'SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ''' + #table_type + ''''
EXEC (#sql)
END
GO
EXEC usp_DynTest 'BASE TABLE'
GO
EXEC usp_DynTest 'VIEW'
GO
DROP PROCEDURE usp_DynTest
GO
Also:
USE SandBox
GO
CREATE PROCEDURE usp_DynTest(#table_type AS VARCHAR(255))
AS
BEGIN
DECLARE #sql AS VARCHAR(MAX) = 'SELECT * INTO #temp FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ''' + #table_type + '''; SELECT * FROM #temp;'
EXEC (#sql)
END
GO
EXEC usp_DynTest 'BASE TABLE'
GO
EXEC usp_DynTest 'VIEW'
GO
DROP PROCEDURE usp_DynTest
GO

Reuse results of SELECT query inside a stored procedure

This is probably a very simple question, but my attempts to search for an answer are thwarted by Google finding answers showing how to reuse a query by making a stored procedure instead. I want to reuse the results of a query inside a stored procedure.
Here's a cut-down example where I've chopped out NOCOUNT, XACT_ABORT, TRANSACTION, TRY, and much of the logic.
CREATE PROCEDURE Do_Something
#userId UNIQUEIDENTIFIER
AS
BEGIN
DELETE FROM LikedItems
WHERE likedItemId IN
(
SELECT Items.id FROM Items
WHERE Items.userId = #userId
)
DELETE FROM FollowedItems
WHERE followedItemId IN
(
SELECT Items.id FROM Items
WHERE Items.userId = #userId
)
END
What is the syntax to reuse the results of the duplicated nested SELECT rather than doing it twice?
You can INSERT result of the SELECT into a temporary table or table variable, but it doesn't automatically mean that the overall performance would be better. You need to measure it.
Temp Table
CREATE PROCEDURE Do_Something
#userId UNIQUEIDENTIFIER
AS
BEGIN
CREATE TABLE #Temp(id int);
INSERT INTO #Temp(id)
SELECT Items.id
FROM Items
WHERE Items.userId = #userId;
DELETE FROM LikedItems
WHERE likedItemId IN
(
SELECT id FROM #Temp
)
DELETE FROM FollowedItems
WHERE followedItemId IN
(
SELECT id FROM #Temp
)
DROP TABLE #Temp;
END
Table variable
CREATE PROCEDURE Do_Something
#userId UNIQUEIDENTIFIER
AS
BEGIN
DECLARE #Temp TABLE(id int);
INSERT INTO #Temp(id)
SELECT Items.id
FROM Items
WHERE Items.userId = #userId;
DELETE FROM LikedItems
WHERE likedItemId IN
(
SELECT id FROM #Temp
)
DELETE FROM FollowedItems
WHERE followedItemId IN
(
SELECT id FROM #Temp
)
END
You can declare a table variable to store the results of the select and then simply query that.
CREATE PROCEDURE Do_Something
#userId UNIQUEIDENTIFIER
AS
BEGIN
DECLARE #TempItems TABLE (id int)
INSERT INTO #TempItems
SELECT Items.id FROM Items
WHERE Items.userId = #userId
DELETE FROM LikedItems
WHERE likedItemId IN
(
SELECT id FROM #TempItems
)
DELETE FROM FollowedItems
WHERE followedItemId IN
(
SELECT id FROM #TempItems
)
END
If the subquery is fast and simple - no need to change anything. Item's data is in the cache (if it was not) after the first query, locks are obtained. If the subquery is slow and complicated - store it into a table variable and reuse by the same subquery as listed in the question.
If your question is not related to performance and you are beware of copy-paste: there is no copy-paste. There is the same logic, similar structure and references - yes, you will have almost the same query source code.
In general, it is not the same. Some rows could be deleted from or inserted into Items table after the first query unless your are running under SERIALIZABLE isolation level. Many different things could happen during first delete, between first and second delete statements. Each delete statement also requires it's own execution plan - thus all the information about tables affected and joins must be provided to SERVER anyway. You need to filter by the same source again - yes, you provide subquery with the same source again. There is no "twice" or "reuse" of a partial code. Data collected by a complicated query - yes, it can be reused (without running the same complicated query - by simple querying from prepared source) via temp tables/table variables as mentioned before.

sql lookup ID and insert if no result found

I have a select statement that needs to look up a customer ID from a customer name. If an ID does not exist for that name, a new record needs to be created in the customer table. this has to be done as part of a select statement (related to the app its being run from).
I tried looking at a UDF that returned either the existing ID or a new ID, before realizing that you can't modify tables from a function.
any idea how to accomplish this?
EDIT:
I think i need to clarify things a bit more. The select statement can and will change on a per-implementation basis. What I'm looking for is a generic way of looking up or creating the customer id (that table and the need to do the lookup does not change) as part of a larger select statement.
the app that is using the sql loads the select statement from a config file, and has 'SELECT' hard coded, so there's no chance of adding an exec before the select etc.
It looks like what I need is something like 'select a.1 (exec dotheLookup(name)) as customerID, a.2 FROM table, but I'm not sure how to go about that.
I suggest you to Create a stored procedure for this. Something like
Create procedure customer
--parameters
AS
Begin
IF exists(Select lookup(customerName) as customerID from table)
BEGIN
--Your select goes here
END
ELSE
BEGIN
--Insert into customer table and return scopeidentity
--Select goes here
END
END
Updated Answer:
You cannot perform data manipulation using select statement.
You could execute a stored procedure before you execute the SELECT statement, the run a function that returns ID from name:
exec CheckForCustomerByNameAndAddIDIfItDoesntExist(customerName)
declare iCustomerID int
select iCustomerID = GetCustomerIDFromName(customerName)
select a.1, a.2, iCustomerID as customerID from table
Something like that
Can you modify the database server? If so, add a linked server pointing to the local server.
EXEC master.dbo.sp_addlinkedserver #server = N'LinkedLocal', #srvproduct=N'', #provider=N'SQLNCLI', #datasrc=N'LocalServerName'
EXEC master.dbo.sp_addlinkedsrvlogin #rmtsrvname=N'LinkedLocal',#useself=N'True',#locallogin=NULL,#rmtuser=NULL,#rmtpassword=NULL
Then just run an OPENQUERY that invokes a stored procedure that does your work:
select * from OPENQUERY("LinkedLocal", 'exec Database.Schema.StoredProcedure ''Param1'', ''Param2'')

Can I use a store procedure output in an update statement in SQL Server?

I'm converting some data in SQL Server 2005. I have a table update like this:
update Invoices set Invoices.InvoiceReference = 'NewRef'
where Invoices.InvoiceReference='Unknown'
But what I'd like to plug in instead of 'NewRef' is the output from a stored procedure that uses parameters from the columns of the Invoices table. The stored procedure itself does updates to another table. Is it possible? Something like this below (which is wrong of course :)
DECLARE #Ref nvarchar(20)
update Invoices set Invoices.InvoiceReference = (
EXEC InvoiceGenerateRef
#ClientCode = Invoices.ClientCode,
#EventCode = Invoices.EventCode,
#Ref = #Ref OUTPUT
SELECT #Ref)
where Invoices.InvoiceReference='Unknown'
Do I need to use a cursor or is the syntax just wrong?
Thanks,
Chris.
I think you would be better off changing your stored procedure into either a function or a view (depending on what you actually do in the proc).
I think what you are after is to join to the resultset of a stored proc which would not work.
You are almost there, the correct way to achieve what you are looking to do would be to define an output parameter as part of your stored procedure definition.
This paramter can then be used as part of your update statement.
DECLARE #Ref nvarchar(20)
EXEC InvoiceGenerateRef
#ClientCode = N'ABC2',
#EventCode = N'X1'
#Ref = #Ref OUTPUT
update Invoices
set Invoices.InvoiceReference = #Ref
where Invoices.InvoiceReference='Unknown'
by using OPENROWSET you can query your stored procedure results just like a view:
http://blogs.technet.com/wardpond/archive/2005/08/01/the-openrowset-trick-accessing-stored-procedure-output-in-a-select-statement.aspx
so for your case this might be useful.
1) You could change InvoiceGenerateRef so that it could optionally save the generated Ref into InvoiceReference field. Presumably you would also have to provide parameters to define the selection
2) You could us e cursor to step round each row in
SELECT ...
FROM Invoices
WHERE Invoices.InvoiceReference='Unknown'
and pass the details to InvoiceGenerateRef and then update the row. This is bad IMHO and will be slow (Your best bet is a set-based solution)
3) You could select the appropriate Invoices.ID's into a temporary table which would be in scope for the InvoiceGenerateRef so that it could iterate that (i.e. the choice of WHICH rows to update is external to the SProc, but the SProc does the actual updating)
CREATE TABLE #TEMP
(
T_ID int NOT NULL
)
INSERT INTO #TEMP (T_ID)
SELECT ID
FROM Invoices
WHERE Invoices.InvoiceReference='Unknown'
EXEC InvoiceGenerateRef #ACTION='UpdateFromTemporaryTable'
4) You could change InvoiceGenerateRef to a function that performed the same task:
UPDATE U
SET U.InvoiceReference =
dbo.MyInvoiceGenerateRefFunction(U.ClientCode, U.EventCode)
FROM Invoices AS U
WHERE U.InvoiceReference='Unknown'
MyInvoiceGenerateRefFunction would have to be deterministic (I think!)
This would be my preferred choice

Accessing data with stored procedures

One of the "best practice" is accessing data via stored procedures. I understand why is this scenario good.
My motivation is split database and application logic ( the tables can me changed, if the behaviour of stored procedures are same ), defence for SQL injection ( users can not execute "select * from some_tables", they can only call stored procedures ), and security ( in stored procedure can be "anything" which secure, that user can not select/insert/update/delete data, which is not for them ).
What I don't know is how to access data with dynamic filters.
I'm using MSSQL 2005.
If I have table:
CREATE TABLE tblProduct (
ProductID uniqueidentifier -- PK
, IDProductType uniqueidentifier -- FK to another table
, ProductName nvarchar(255) -- name of product
, ProductCode nvarchar(50) -- code of product for quick search
, Weight decimal(18,4)
, Volume decimal(18,4)
)
then I should create 4 stored procedures ( create / read / update / delete ).
The stored procedure for "create" is easy.
CREATE PROC Insert_Product ( #ProductID uniqueidentifier, #IDProductType uniqueidentifier, ... etc ... ) AS BEGIN
INSERT INTO tblProduct ( ProductID, IDProductType, ... etc .. ) VALUES ( #ProductID, #IDProductType, ... etc ... )
END
The stored procedure for "delete" is easy too.
CREATE PROC Delete_Product ( #ProductID uniqueidentifier, #IDProductType uniqueidentifier, ... etc ... ) AS BEGIN
DELETE tblProduct WHERE ProductID = #ProductID AND IDProductType = #IDProductType AND ... etc ...
END
The stored procedure for "update" is similar as for "delete", but I'm not sure this is the right way, how to do it. I think that updating all columns is not efficient.
CREATE PROC Update_Product( #ProductID uniqueidentifier, #Original_ProductID uniqueidentifier, #IDProductType uniqueidentifier, #Original_IDProductType uniqueidentifier, ... etc ... ) AS BEGIN
UPDATE tblProduct SET ProductID = #ProductID, IDProductType = #IDProductType, ... etc ...
WHERE ProductID = #Original_ProductID AND IDProductType = #Original_IDProductType AND ... etc ...
END
And the last - stored procedure for "read" is littlebit mystery for me. How pass filter values for complex conditions? I have a few suggestion:
Using XML parameter for passing where condition:
CREATE PROC Read_Product ( #WhereCondition XML ) AS BEGIN
DECLARE #SELECT nvarchar(4000)
SET #SELECT = 'SELECT ProductID, IDProductType, ProductName, ProductCode, Weight, Volume FROM tblProduct'
DECLARE #WHERE nvarchar(4000)
SET #WHERE = dbo.CreateSqlWherecondition( #WhereCondition ) --dbo.CreateSqlWherecondition is some function which returns text with WHERE condition from passed XML
DECLARE #LEN_SELECT int
SET #LEN_SELECT = LEN( #SELECT )
DECLARE #LEN_WHERE int
SET #LEN_WHERE = LEN( #WHERE )
DECLARE #LEN_TOTAL int
SET #LEN_TOTAL = #LEN_SELECT + #LEN_WHERE
IF #LEN_TOTAL > 4000 BEGIN
-- RAISE SOME CONCRETE ERROR, BECAUSE DYNAMIC SQL ACCEPTS MAX 4000 chars
END
DECLARE #SQL nvarchar(4000)
SET #SQL = #SELECT + #WHERE
EXEC sp_execsql #SQL
END
But, I think the limitation of "4000" characters for one query is ugly.
The next suggestion is using filter tables for every column. Insert filter values into the filter table and then call stored procedure with ID of filters:
CREATE TABLE tblFilter (
PKID uniqueidentifier -- PK
, IDFilter uniqueidentifier -- identification of filter
, FilterType tinyint -- 0 = ignore, 1 = equals, 2 = not equals, 3 = greater than, etc ...
, BitValue bit , TinyIntValue tinyint , SmallIntValue smallint, IntValue int
, BigIntValue bigint, DecimalValue decimal(19,4), NVarCharValue nvarchar(4000)
, GuidValue uniqueidentifier, etc ... )
CREATE TABLE Read_Product ( #Filter_ProductID uniqueidentifier, #Filter_IDProductType uniqueidentifier, #Filter_ProductName uniqueidentifier, ... etc ... ) AS BEGIN
SELECT ProductID, IDProductType, ProductName, ProductCode, Weight, Volume
FROM tblProduct
WHERE ( #Filter_ProductID IS NULL
OR ( ( ProductID IN ( SELECT GuidValue FROM tblFilter WHERE IDFilter = #Filter_ProductID AND FilterType = 1 ) AND NOT ( ProductID IN ( SELECT GuidValue FROM tblFilter WHERE IDFilter = #Filter_ProductID AND FilterType = 2 ) )
AND ( #Filter_IDProductType IS NULL
OR ( ( IDProductType IN ( SELECT GuidValue FROM tblFilter WHERE IDFilter = #Filter_IDProductType AND FilterType = 1 ) AND NOT ( IDProductType IN ( SELECT GuidValue FROM tblFilter WHERE IDFilter = #Filter_IDProductType AND FilterType = 2 ) )
AND ( #Filter_ProductName IS NULL OR ( ... etc ... ) )
END
But this suggestion is littlebit complicated I think.
Is there some "best practice" to do this type of stored procedures?
For reading data, you do not need a stored procedure for security or to separate out logic, you can use views.
Just grant only select on the view.
You can limit the records shown, change field names, join many tables into one logical "table", etc.
First: for your delete routine, your where clause should only include the primary key.
Second: for your update routine, do not try to optimize before you have working code. In fact, do not try to optimize until you can profile your application and see where the bottlenecks are. I can tell you for sure that updating one column of one row and updating all columns of one row are nearly identical in speed. What takes time in a DBMS is (1) finding the disk block where you will write the data and (2) locking out other writers so that your write will be consistent. Finally, writing the code necessary to update only the columns that need to change will generally be harder to do and harder to maintain. If you really wanted to get picky, you'd have to compare the speed of figuring out which columns changed compared with just updating every column. If you update them all, you don't have to read any of them.
Third: I tend to write one stored procedure for each retrieval path. In your example, I'd make one by primary key, one by each foreign key and then I'd add one for each new access path as I needed them in the application. Be agile; don't write code you don't need. I also agree with using views instead of stored procedures, however, you can use a stored procedure to return multiple result sets (in some version of MSSQL) or to change rows into columns, which can be useful.
If you need to get, for example, 7 rows by primary key, you have some options. You can call the stored procedure that gets one row by primary key seven times. This may be fast enough if you keep the connection opened between all the calls. If you know you never need more than a certain number (say 10) of IDs at a time, you can write a stored procedure that includes a where clause like "and ID in (arg1, arg2, arg3...)" and make sure that unused arguments are set to NULL. If you decide you need to generate dynamic SQL, I wouldn't bother with a stored procedure because TSQL is just as easy to make a mistake as any other language. Also, you gain no benefit from using the database to do string manipulation -- it's almost always your bottleneck, so there is no point in giving the DB any more work than necessary.
I disagree that create Insert/Update/Select stored procedures are a "best practice". Unless your entire application is written in SPs, use a database layer in your application to handle these CRUD activities. Better yet, use an ORM technology to handle them for you.
My suggestion is that you don't try to create a stored procedure that does everything that you might now or ever need to do. If you need to retrieve a row based on the table's primary key then write a stored procedure to do that. If you need to search for all rows meeting a set of criteria then find out what that criteria might be and write a stored procedure to do that.
If you try to write software that solves every possible problem rather than a specific set of problems you will usually fail at providing anything useful.
your select stored procedure can be done as follows to require only one stored proc but any number of different items in the where clause. Pass in any one or combination of the parameters and you will get ALL items which match - so you only need one stored proc.
Create sp_ProductSelect
(
#ProductID int = null,
#IDProductType int = null,
#ProductName varchar(50) = null,
#ProductCode varchar(10) = null,
...
#Volume int = null
)
AS
SELECT ProductID, IDProductType, ProductName, ProductCode, Weight, Volume FROM tblProduct'
Where
((#ProductID is null) or (ProductID = #ProductID)) AND
((#ProductName is null) or (ProductName = #ProductName)) AND
...
((#Volume is null) or (Volume= #Volume))
In SQL 2005, it supports nvarchar(max), which has a limit of 2G, but virtually accepting all string operations upon normal nvarchar. You may want to test if this can fit into what you need in the first approach.