All code published in this article is published under the Microsoft Public License. See source code download for a copy of the license.
Copyright 2007 Zack Moore
Code can be downloaded from here.
Intro
Data Access Layers are one of those problems that people keep coming up with solutions for. There are many reasons for this, but basically it boils down to two things. People are different and different people solve problems differently. Data is different and people need to access different kinds of data differently.
I wanted to build something that was simple, easy to use, easy to extend, and compatible with data binding. I wanted to be able to automatically generate well written stored procedures for all the standard CRUD (Create Read Update Delete) and also allow me to add my own custom written stored procedures. I wanted to be able to call all of my stored procedures from .NET code without having to manually write all of the ADO.NET and I wanted the compiler to check my method names and my parameters like a regular .NET method. I wanted to be able to use data binding with objects instead of DataSets. And last, the data binding had to be two way and not just for display.
Code Generation
Code Generation is the act of having one computer program write another computer program and it is a great way to build components that repeat a pattern like stored procedures and ADO.NET code.
There are different ways of performing code generation. One way is text output. A computer program can easily spit out a file containing C# or VB.NET or any language you want. That is how MyGeneration and CodeSmith work or you could write a program yourself that builds a code file. In .NET you could also use the Code DOM to write code using a structured framework.
I use MyGeneration: http://www.mygenerationsoftware.com/
MyGeneration is a free open source code code generation tool. Code Generation scripts are writing like classic ASP. You use a set of tags to escape in and out of script text and script code. Usually these are the same tags as used in classic ASP <% and %> but they are configurable. My Generation supports writing scripts in VB Script, JScript, C#, and VB.NET. In addition to the code generation code, each script also supports a user interface section where you can prompt the user for code generation parameters. MyGeneration also supports saving the script parameters in a file so that you can run the script over and over without having to go through the UI each time so long as you don't need to change the parameters.
Stored Procedures
The first step in building a stored procedure based DAL is to generate your stored procedures. I started with an excellent script titled "Script Insert/Update/Delete Procedures for SQL Server" written by Justin Greenwood. Justin wrote his script in February of 2004 for SQL Server 2000 and I wanted my script to work with SQL Server 2005 so I needed to make some changes. In addition, I had some additional features that I wanted to add.
I would like to extend my thanks to Justin for writing his very useful script and for giving me permission to redistribute my modified version.
Justin's original script included a lot of features like its handling of primary keys in where clauses, treatment of computed and identity columns, and its use of timestamp columns in update procedures.
I made a lot of changes, so I am sure that any mistakes are more likely mine.
Lets take a look at how the script handles a simple sample database.
One point I would like to make as we begin looking at generated code is that none of the generated code has a date emended in it. Its common for programmers to want to put a generated on date in code that is output so that they can tell when it was produced, but there is a very good reason not to. If you use source control, every time you check in a file your source control system checks to see if the file changed. If not, then it doesn't do anything, but if it did then it creates a new revision and checks in your changes. Now if you add a generated on date to your output, then every time you regenerate you create a new generated on date even if the actual code that was output didn't change at all. This means that your source control system has to create a new revision and it becomes harder for you to track real changes when you are having to sort through a bunch of revisions where only the dates changed. Let your source control system track when things changed. Leave dates and other unnecessary artifacts out of your generated code.
To begin, run the script "Basic CRUD Plus SelectBy FK". This script is under the MyGeneration namespace "ZacksFiasco.DataAccessLayer.SQLServer2005". You should see a UI screen that looks like the following:
The first parameter is the path on your computer to write the stored procedure files to. For testing, you could leave it to the default. However, if you use this for a project then I suggest you point it to a folder in your project where you would like to store your stored procedure scripts.
The second parameter is the SQL Server schema to store your procedures under. The UI defaults this to 'api'. You could put your procedures under 'dbo' but I suggest that you don't. Lots of things can put procedures into 'dbo'. In a later part of this series we will look at another code generation script that will take our stored procedures and build a .NET DAL and it will use the schema to filter only the stored procedures that we want. (Note: if you use 'api' or another schema that doesn't exist, the be sure to create that schema in your database before trying to load your stored procedures.)
The next parameter is the database to read.
The last parameter is the set of tables to generate procedures for. One file will be generated for each table, so for our test two files while be generated: sql_procs_Table1.sql and sql_procs_Table2.sql.
Generated TSQL
This example generates 381 lines of TSQL so I won't be able to go over the entire output. Instead I will try to give a quick synopsis of the key points.
The insert statements are pretty basic.
-- Proc: Table1Insert
-- This proc was created by script. You can edit it, but if you do
-- then DO NOT regenerate it from the script or you will loose your edits.
-- <MetaData entityName="Table1" commandType="Insert"/>
CREATE PROCEDURE [api].[Table1Insert]
(
@Table1Id uniqueidentifier,
@Col1 varchar(50),
@Col2 int = NULL,
@rowversion timestamp OUTPUT
)
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[Table1]
(
[Table1Id],
[Col1],
[Col2]
)
VALUES
(
@Table1Id,
@Col1,
@Col2
)
SELECT @rowversion = [rowversion]
FROM [dbo].[Table1]
WHERE [Table1Id] = @Table1Id;
RETURN @@Error
END
GO
The stored procedure does not use newid() to generate the key. My personal preference is to generate the key in the application. There are many times when it is beneficial to know the key in the application before inserting the record. In this example we are using a uniqueidentifier which in .NET is a System.Guid but if we were using an int there are other ways of generating a unique value.
If you are concerned about SQL Server clustering of uniqueidentifiers I suggest that you use custom code to generate your Guids.
If this table did use a auto-key, this script is smart enough return the generated value as an OUTPUT parameter.
Also note that the rowversion column is returned as an OUTPUT parameter.
Take a look at the last line of the comment section. It is an XML block. For now I will defer the discussion of this, but we will use this in our DAL code.
The update procedure attempts to update the row in the database. Rowversion is an OUTPUT parameter which is in/out. First rowversion is used for optimistic concurrency. If the update succeeds, the the new rowversion is returned in the same parameter. If the update fails then an error is raised.
-- Proc: Table1Update
-- This proc was created by script. You can edit it, but if you do
-- then DO NOT regenerate it from the script or you will loose your edits.
-- <MetaData entityName="Table1" commandType="Update"/>
CREATE PROCEDURE [api].[Table1Update]
(
@Table1Id uniqueidentifier,
@Col1 varchar(50),
@Col2 int = NULL,
@rowversion timestamp OUTPUT
)
AS
BEGIN
SET NOCOUNT ON
DECLARE @t TABLE(x int);
UPDATE [dbo].[Table1]
SET [Table1Id] = @Table1Id,
[Col1] = @Col1,
[Col2] = @Col2
OUTPUT 1 into @t(x)
WHERE
[Table1Id] = @Table1Id AND
[rowversion] = @rowversion
IF(SELECT COUNT(*) FROM @t) = 0
BEGIN
RAISERROR('Concurrency Error',16,1)
END
SELECT
@rowversion = [rowversion]
FROM [dbo].[Table1]
WHERE
[Table1Id] = @Table1Id;
RETURN @@Error
END
GO
The delete procedure is pretty simple. It just deletes by the primary key.
-- Proc: Table1Delete
-- This proc was created by script. You can edit it, but if you do
-- then DO NOT regenerate it from the script or you will loose your edits.
-- <MetaData entityName="Table1" commandType="Delete"/>
CREATE PROCEDURE [api].[Table1Delete]
(
@Table1Id uniqueidentifier
)
AS
BEGIN
SET NOCOUNT ON
DELETE
FROM [dbo].[Table1]
WHERE
[Table1Id] = @Table1Id
RETURN @@Error
END
GO
The select procedure is also pretty basic. There is also a SelectAll procedure which is identical except it doesn't have a where clause.
-- Proc: Table1Select
-- This proc was created by script. You can edit it, but if you do
-- then DO NOT regenerate it from the script or you will loose your edits.
-- <MetaData entityName="Table1" commandType="Select"><!--
--><Keys><!--
--><Key>Table1Id</Key><!--
--></Keys><!--
--></MetaData>
CREATE PROCEDURE [api].[Table1Select]
(
@Table1Id uniqueidentifier
)
AS
BEGIN
SET NOCOUNT ON
select
[Table1Id],
[Col1],
[Col2],
[rowversion]
FROM [dbo].[Table1]
WHERE
[Table1Id] = @Table1Id
RETURN @@Error
END
GO
One of the cool enhancements of my updated script is that it also generates SelectBy<ForeignKey> procedures. In this example we generated a procedure that selects records from Table2 where it matches the foreign key from Table1.
-- Proc: Table2SelectByTable1
-- This proc was created by script. You can edit it, but if you do
-- then DO NOT regenerate it from the script or you will loose your edits.
-- <MetaData entityName="Table2" commandType="Select"><!--
--><Keys><!--
--><Key>Table2Id</Key><!--
--></Keys><!--
--></MetaData>
CREATE PROCEDURE [api].[Table2SelectByTable1]
(
@Table1Id uniqueidentifier
)
AS
BEGIN
SET NOCOUNT ON
select
[Table2Id],
[Col1],
[Table1Id],
[UpdatedOn],
[UpdatedBy]
FROM [dbo].[Table2]
WHERE
[Table1Id] = @Table1Id
RETURN @@Error
END
GO
This script looks for certain column names that it treats differently. If a column is named 'UpdatedOn' then the script generates code to mark this parameter as an OUTPUT parameter in insert and update procedures and assigns the value from getdate(). A future enhancement might make these columns configurable instead of hard-coding the column names in the script.
-- Proc: Table2Insert
-- This proc was created by script. You can edit it, but if you do
-- then DO NOT regenerate it from the script or you will loose your edits.
-- <MetaData entityName="Table2" commandType="Insert"/>
CREATE PROCEDURE [api].[Table2Insert]
(
@Table2Id uniqueidentifier,
@Col1 varchar(50) = NULL,
@Table1Id uniqueidentifier,
@UpdatedOn datetime OUTPUT,
@UpdatedBy uniqueidentifier = NULL
)
AS
BEGIN
SET NOCOUNT ON
set @UpdatedOn = getdate();
INSERT INTO [dbo].[Table2]
(
[Table2Id],
[Col1],
[Table1Id],
[UpdatedOn],
[UpdatedBy]
)
VALUES
(
@Table2Id,
@Col1,
@Table1Id,
@UpdatedOn,
@UpdatedBy
)
RETURN @@Error
END
GO
-- Proc: Table2Update
-- This proc was created by script. You can edit it, but if you do
-- then DO NOT regenerate it from the script or you will loose your edits.
-- <MetaData entityName="Table2" commandType="Update"/>
CREATE PROCEDURE [api].[Table2Update]
(
@Table2Id uniqueidentifier,
@Col1 varchar(50) = NULL,
@Table1Id uniqueidentifier,
@UpdatedOn datetime OUTPUT,
@UpdatedBy uniqueidentifier = NULL
)
AS
BEGIN
SET NOCOUNT ON
set @UpdatedOn = getdate();
UPDATE [dbo].[Table2]
SET
[Table2Id] = @Table2Id,
[Col1] = @Col1,
[Table1Id] = @Table1Id,
[UpdatedOn] = @UpdatedOn,
[UpdatedBy] = @UpdatedBy
WHERE
[Table2Id] = @Table2Id
RETURN @@Error
END
GO
I've talked mostly in this article about what the code generation script outputs and not how it generates code. The key to generating a good code generation script is to first decide what it should output for each scenario. The second key is a good meta-data engine. MyGeneration has an excellent meta-data engine and this is how I can easily gather information on the tables and their keys.
Download the source code and take a look at how the scripts work.
This concludes Part 1. We have seen how you can generate basic CRUD stored procedures for our database. The script generates good TSQL and includes features for rowversion/timestamp columns, UpdatedOn columns, identities, keys, and foreign keys.
Part 2 will talk about taking these stored procedures, writing custom stored procedures and generating a basic Data Access Layer where we can call each of these stored procedures from .NET applications.
