-- prepares metadata for CRUD stored procedures CREATE VIEW dbo.vCRUDMetadata AS WITH CTE([schema_name], Table_Name, id, id_data_type, Column_id, [parameters], [values], [attributes], [update]) AS ( SELECT A.[schema_name] , A.Table_Name , A.Column_Name id , A.system_type id_data_type , A.Column_id , Cast(' @' + A.Column_Name + ' ' + system_type + CASE WHEN A.system_type IN ('char', 'nchar' , 'varchar', 'nvarchar') THEN ' (' + CASE A.max_length WHEN -1 THEN 'max' ELSE Cast(A.max_length AS varchar(10)) END + ')' ELSE '' END + ' output' as nvarchar(max)) [parameters] , Cast(CASE WHEN A.system_type = 'bit' THEN ' IsNull(@' + A.Column_Name + ', 0)' WHEN A.system_type = 'uniqueidentifier' THEN ' IsNull(@' + A.Column_Name + ', NEWID())' ELSE '@' + A.Column_Name END as nvarchar(max)) [values] , Cast(A.Column_Name as nvarchar(max)) [attributes] , Cast('' as nvarchar(max)) [update] FROM dbo.vTableColumns A WHERE A.column_id = 1 UNION ALL SELECT A.[schema_name] , A.Table_Name , B.id , B.id_data_type , A.column_id , B.[parameters] + CHAR(13)+ char(10) + ', @' + A.Column_Name + ' ' + A.system_type + CASE WHEN A.system_type IN ('char', 'nchar' , 'varchar', 'nvarchar') THEN ' (' + CASE A.max_length WHEN -1 THEN 'max' ELSE Cast(A.max_length AS varchar(10)) END + ')' ELSE '' END [parameters] , B.[values] + CHAR(13)+ char(10) + ', ' + CASE WHEN A.system_type = 'bit' THEN ' IsNull(@' + A.Column_Name + ', 0)' WHEN A.system_type = 'uniqueidentifier' THEN ' IsNull(@' + A.Column_Name + ', NEWID())' ELSE '@' + A.Column_Name END [values] , B.[attributes] + CHAR(13)+ char(10) + ', ' + A.Column_Name [attributes] , B.[update] + CHAR(13)+ char(10) + ', ' + A.Column_Name + ' = ' + CASE WHEN A.Column_Name = 'bit' THEN ' IsNull(@' + A.Column_Name + ', 0)' WHEN A.system_type = 'uniqueidentifier' THEN ' IsNull(@' + A.Column_Name + ', NEWID())' ELSE '@' + A.Column_Name END [update] FROM dbo.vTableColumns A JOIN CTE B ON A.column_id - 1 = B.column_id AND A.[schema_name] = B.[schema_name] AND A.Table_Name = B.Table_Name ) SELECT A.[schema_name] , A.Table_Name , A.column_id NumberAttributes , A.id , A.id_data_type , A.[parameters] , A.[values] , A.[attributes] , A.[update] FROM CTE A WHERE column_id IN ( SELECT MAX(column_id) FROM CTE B WHERE A.[schema_name] = B.[schema_name] AND A.Table_Name = B.Table_Name)
The view needs additional work because it treats only the most simple met cases, preferring to keep it (relatively) simple. Even so, the logic might be not so simple to understand within a simple review - it just makes use of CASEs combined with concatenation, building a string for parameters, update, insert or select.
Here’s a call to the view for a table:
-- testing the view SELECT * FROM dbo.vCRUDMetadata WHERE [Schema_Name] = 'Person' AND Table_Name = 'Address'
Now how do we make it useful? I used the same idea as in Excel, the code for creating the Save stored procedure being implemented in the below UDF:
-- Template for Save stored procedure CREATE FUNCTION dbo.TemplateSave( @SchemaName nvarchar(50) , @TableName nvarchar(50) , @id nvarchar(50) , @id_data_type nvarchar(50) , @parameters nvarchar(max) , @values nvarchar(max) , @attributes nvarchar(max) , @update nvarchar(max) ) RETURNS nvarchar(max) AS BEGIN DECLARE @DefaultValue nvarchar(2) SET @DefaultValue = CASE WHEN @id_data_type IN ('char', 'nchar' , 'varchar', 'nvarchar') THEN '''' ELSE '0' END SET @values = Right(@values, Len(@values) - CharIndex(',', @values)-1) SET @attributes = Right(@attributes, Len(@attributes) - CharIndex(',', @attributes)-1) SET @update = Right(@update, Len(@update) - CharIndex(',', @update)-1) RETURN( 'CREATE PROCEDURE ' + @SchemaName + '.pSave' + @TableName + '(' + CHAR(13)+ char(10) + @parameters + ')' + CHAR(13)+ char(10) + 'AS' + CHAR(13)+ char(10) + 'BEGIN' + CHAR(13)+ char(10) + 'BEGIN TRY' + CHAR(13)+ char(10) + ' IF ISNULL(@' + @ID + ', ' + @DefaultValue + ') = ' + @DefaultValue + CHAR(13)+ char(10) + ' BEGIN' + CHAR(13)+ char(10) + ' -- insert statement' + CHAR(13)+ char(10) + ' INSERT INTO ' + @SchemaName + '.' + @TableName + ' (' + @attributes + ')' + CHAR(13)+ char(10) + ' VALUES (' + @values + ')' + CHAR(13)+ char(10) + ' SET @' + @ID + ' = @@IDENTITY' + CHAR(13)+ char(10) + ' END' + CHAR(13)+ char(10) + ' ELSE' + CHAR(13)+ char(10) + ' BEGIN' + CHAR(13)+ char(10) + ' -- update statement' + CHAR(13)+ char(10) + ' UPDATE ' + @SchemaName + '.' + @TableName + CHAR(13)+ char(10) + ' SET ' + @update + CHAR(13)+ char(10) + ' WHERE ' + @ID + '= @' + @ID + CHAR(13)+ char(10) + ' END' + CHAR(13)+ char(10) + ' SELECT @@Rowcount' + CHAR(13)+ char(10) + 'END TRY' + CHAR(13)+ char(10) + 'BEGIN CATCH' + CHAR(13)+ char(10) + ' SELECT Cast(ERROR_NUMBER() as varchar(10)) + '':'' + ERROR_MESSAGE()' + CHAR(13)+ char(10) + 'END CATCH' + CHAR(13)+ char(10) + 'END' ) END
The stored procedure template is slightly modified from the one presented in the previous posts, this time preferring to return the ID in the same parameter used as input, using the select to return the number of records affected. Here’s the test script which creates the stored procedure and its output:
SELECT dbo.TemplateSave( [Schema_Name] , Table_Name , id , id_data_type , [parameters] , [values] , [attributes] , [update]) CodeSP FROM dbo.vCRUDMetadata WHERE [Schema_Name] = 'Person' AND Table_Name = 'Address'
CREATE PROCEDURE Person.pSaveAddress( @AddressID int output , @AddressLine1 nvarchar (120) , @AddressLine2 nvarchar (120) , @City nvarchar (60) , @StateProvinceID int , @PostalCode nvarchar (30) , @rowguid uniqueidentifier , @ModifiedDate datetime) AS BEGIN BEGIN TRY IF ISNULL(@AddressID, 0) = 0 BEGIN -- insert statement INSERT INTO Person.Address ( AddressLine1 , AddressLine2 , City , StateProvinceID , PostalCode , rowguid , ModifiedDate) VALUES ( @AddressLine1 , @AddressLine2 , @City , @StateProvinceID , @PostalCode , IsNull(@rowguid, NEWID()) , @ModifiedDate) SET @AddressID = @@IDENTITY END ELSE BEGIN -- update statement UPDATE Person.Address SET AddressLine1 = @AddressLine1 , AddressLine2 = @AddressLine2 , City = @City , StateProvinceID = @StateProvinceID , PostalCode = @PostalCode , rowguid = IsNull(@rowguid, NEWID()) , ModifiedDate = @ModifiedDate WHERE AddressID= @AddressID END SELECT @@Rowcount END TRY BEGIN CATCH SELECT Cast(ERROR_NUMBER() as varchar(10)) + ':' + ERROR_MESSAGE() END CATCH END
The resulted code could be exported to an Excel or text file and then run in Management Studio. And here are two calls to it, one for update, and the other one for insert:
EXEC Person.pSaveAddress 1, 'AddressLine1', 'AddressLine2', 'City', 1 , 'PostalCode', NULL, '20090101' EXEC Person.pSaveAddress 0, 'AddressLine100', 'AddressLine2', 'City', 1 , 'PostalCode', '6D1EB969-C5CF-420C-BA5F-1153F4D89373', '20090101'
The difference from the Excel version is that it could be run easily for a set of tables and then the stored procedures could be created one by one. A higher level of automation could be achieved by creating a cursor or an SSIS package, and then each stored procedure created with EXEC or sp_executesql stored procedures. The template could be modified after requirements as needed. Eventually the code from template could be included in a CLR function and thus take advantage of some string functionality, but it becomes then more difficult to modify