In college last year I came across something I found very interesting with regard to stored procedures. It was mostly with regard to reducing code repetition for transaction management, error handling, and security. But I have researched it since and cannot find it anywhere. Perhaps I don't know what it is called so I will explain.
Let's say I have a simple table (forget its relationships, just a single table). I have at least 5 possible operations I can do on it namely CRUD, with the R being read the details of one row, or read a list of rows for a given criteria. Again, let's not get into too much detail with complicated stored procedures and let's pretend, for this example, we want nothing but the following 5 operations to be performed:
- Create (simple insert)
- Read (one for reading a single row)
- Read multiple (reading a list of rows, simple
select * from sometable where some condition
, nothing complicated), - Update (simple update)
- Delete (again, simple delete).
For the purposes of this example, a simple table with 2 or 3 columns, and the most simple procedures you can come up with to do those operations.
The problem:
My lecturer, when talking to us about transactions, advised us to take these simple procedures, and keep them separate as is normal. But then to write a master procedure which executes each of these. This means we have a master procedure into which we pass a character lets say 'C' for create and so on, and a series of conditions which decide which job to run. Of course, we should pass in the details needed from the master to the child proc which means we need to get them from the user as parameters. This master procedure is already beginning to sound complicated.
His reasoning, do the transaction handling, validation, error handling, and security handling in the master procedure. Then call that master procedure with the required parameters, which does all the checking, and passes the parameters to the child procedure. Below is an example.
Simple table, don't go worrying too much about relationships, keys, indexes, constraints, triggers and so on.
For the below two pieces of code, the only real relevant bit is the parts between the try...catch. The rest is boiler plate for the master.
Below is the code for a create procedure:
ALTER proc [Actions].[AreaCreate]
--External Variables - Input
@AreaName varchar(50), @AreaAvailablity bit, @Description varchar(max),
--External Variables - Output
@NoOfRecords int output
as
--Internal Variables
declare @ErrorMessage varchar(max)
Begin try
insert [Tennis3rdYrMVC].[dbo].Areas
([AreaName], [AreaAvailablity], [Description])
values (@AreaName, @AreaAvailablity, @Description)
--Show # of records affected so you can detect nulls or empty lists
--and handle them as you choose in the code
set @NoOfRecords = @@ROWCOUNT
End try
Begin Catch
set @ErrorMessage = ERROR_MESSAGE()
raiserror(@ErrorMessage,16,1)
return ERROR_NUMBER()
End Catch
--All Ok
return 0
Below is another piece of code for a delete procedure
ALTER proc [Actions].[AreaDelete]
--External Variables - Input
@Id int,
--External Variables - Output
@NoOfRecords int output
as
--Internal Variables
declare @ErrorMessage varchar(max)
begin try
delete from [Tennis3rdYrMVC].[dbo].Areas
where Id = @Id
--Show # of records affected so you can detect nulls or empty lists
--and handle them as you choose in the code
set @NoOfRecords=@@ROWCOUNT
end try
begin catch
set @ErrorMessage = ERROR_MESSAGE()
raiserror(@ErrorMessage,16,1)
return ERROR_NUMBER()
end catch
--All Ok
return 0
Finally, the suggested, and complicated master proc.
ALTER proc [Admin].[AreaMaster]
--External Variables - Input
@JobType int, -- 1=Create, 2=Read 1, 3=Read Many, 4=Update, 5=Delete
--Master sprock uses null defaults because not every job requires every field to be present.
--i.e. We dont need to know the area name of an area we want to delete.
@Id int = null, @AreaName varchar(50) = null,
@AreaAvailablity bit = null, @Description varchar(max) = null,
--External Variables - Output
@NoOfRecords int = null output --Used to count the number of records affected where needed.
as
BEGIN
--Internal Variables
declare @ErrorMessage varchar(max)
declare @return_value int
-- SET NOCOUNT ON added to reduce network traffic and speed things up a little.
SET NOCOUNT ON;
/*
--VALIDATION
--Logic for ensuring all required values are entered should be done in processing below in
--the master sprock, NOT in the children (i.e. why check for valid id in 5 sprocks when you
--can do it here in one).
We will do all the processing needed to ensure valid and required values are entered where
needed.
--SECURITY
This is also where we put the security code required to stop SQL Injection and other
attacks, NOT in the child sprocks. The child sprocks would not be allowed to execute by
pages directly.
*/
--Once all validation is done, call relevant child sprocks
--Call AreaCreate Sprock
if(@JobType='1')
begin
exec @return_value = [Actions].[AreaCreate]
@AreaName = @AreaName,
@AreaAvailablity = @AreaAvailablity,
@Description = @Description,
@NoOfRecords = @NoOfRecords OUTPUT
--select @return_value 'Return Value'
if @return_value<>0
begin
raiserror('Error: Problem creating area.',16,0)
--rollback transaction
return 99
end
end
--Call AreaShowDetail Sprock
if(@JobType='2')
begin
exec @return_value = [Actions].[AreaShowDetail]
@Id = @Id,
@NoOfRecords = @NoOfRecords output
----Testing
--select 'Return Value' = @return_value
if @return_value<>0
begin
raiserror('Error: Problem reading area details.',16,0)
--rollback transaction
return 99
end
end
--Call AreaShowList Sprock
if(@JobType='3')
begin
exec @return_value = [Actions].[AreasShowList]
@NoOfRecords = @NoOfRecords output
----Testing
--select 'Return Value' = @return_value
if @return_value<>0
begin
raiserror('Error: Problem reading areas list.',16,0)
--rollback transaction
return 99
end
end
--Call AreaUpdate Sprock
if(@JobType='4')
begin
EXEC @return_value = [Actions].[AreaUpdate]
@Id = @Id,
@AreaName = @AreaName,
@AreaAvailablity = @AreaAvailablity,
@Description = @Description,
@NoOfRecords = @NoOfRecords OUTPUT
--select 'Return Value' = @return_value
if @return_value<>0
begin
raiserror('Error: Problem updating area.',16,0)
--rollback transaction
return 99
end
end
--Call AreaDelete Sprock
if(@JobType='5')
begin
exec @return_value = [Actions].[AreaDelete]
@Id = @Id,
@NoOfRecords = @NoOfRecords output
--select 'Return Value' = @return_value
if @return_value<>0
begin
raiserror('Error: Problem deleting area(s).',16,0)
--rollback transaction
return 99
end
end
--All Ok
return 0
END
Is that complicated or what? Now imagine another level which if you have several tables to operate on, will decide which sub master to call, and this MasterMaster for want of a better name must have external parameters for every field in every table. However, the validation, security, and transaction code would be moved up to that level instead.
More importantly, is that the way a professional would do it. And finally, I am using MVC with entity framework. Suppose I have a database like this (I have), and access is only allowed through this master stored procedure (it is). How do I call such a stored procedure from within EF and MVC. Even better, how would I bypass EF all together and get the data into my controller in a way my view will understand. I know how to do this with ASP.Net code behind. But not in MVC.
Any suggestions. Please, feel free to tell me that this is completely crazy as it seems like a hell of a lot of work (what if you have 50 tables (I do). Imagine, 5 simple operations, across 50 tables = 250 child stored procedures + 50 Sub-Sub-Masters, + 5 Sub-Masters + 1 Master. I have my work cut out. Is there a tool that can do this or at least generate a template for me?
So to summarise, My questions are:
- Is this overly complicated or is this the professional way to do this?
- What is the term used to describe this Master / child procedural relationship.
- Is there a tool that can automate this process.
- Is there a tool that can help me generate some kind of boilerplate template.
- How can I use this procedure with EF and MVC
- How can I bypass EF altogether and just find a way to call the procedure directly.
- Is there anything like the equivalent of classes, or even methods in T-SQL which can structure this much like a standard piece of code. Meaning that each child procedure is a method, each table has its properties, and the constructor will be the the initial (or altered) representation of the object.
- If 7 does exist, which would be more standard, the way I described (i.e.) splitting everything up, or the idea in 7.
With regard to this point 4 I am talking about something simple, like the one already in SQL Server where you right click a procedure and choose execute and it generates some simple code for testing your procedure, or like the one for making functions.
Thanks for reading this and for any help you can offer.