However, if you Google for information about the best practices involving Stored Procedures, you will, of course, find a gazillion conflicting opinions ... some agreeing with my opinion and others not. But still, I think it's an important best practice.
OK, now I'll get down off my soap box and on to the topic of my post!
I have a base DataAccess class, than can be used directly, or sub-classed to a domain use (for example, a large inventory system may have an OrdersDataAccess, an InventoryDataAccess and a CustomerDataAccess ... all subclassed from the BaseDataAccess). I have a series of DataAccess posts here in my blog, and I will show bits and pieces of it here in this post to illustrate my point. If you'd like to read these additional posts, you can use this search: http://geek-goddess-bonnie.blogspot.com/search?q=DataAccess Some of the earlier posts are incomplete, code-wise, but get "fleshed out" in later posts.
I see people on the Forums struggling with simple CRUD activity, trying to utilize the SqlCommandBuilder (usually incorrectly) and having all kinds of problems. SqlCommandBuilder is not the answer, in fact, I never use it ... there is an easier way to Insert and/or Update from a DataTable and do it by using the same Stored Proc (one for each table). It's what I refer to as PUT procs, and the PUT Stored Proc will either INSERT or UPDATE depending on whether or not the Primary Key (PK) is included in the Parameters.
One caveat here ... I use the DataSet/DataTable paradigm for my DTOs (Data Transfer Objects). The same concepts can apply to POCO, with just different methods in the DataAccess class needed. (POCO means "Plain Old CLR Object which is defined here in Wikipedia: https://en.wikipedia.org/wiki/Plain_old_CLR_object as "a POCO does not have any dependency on an external framework and generally does not have any attached behaviour." ... but go ahead and read WikiPedia if you are unfamiliar with the term).
Here's a simple example of a PUT Stored Proc. I will show some methods you can use to create a Utility for generating such Procs automatically at the end of this blog post:
CREATE PROCEDURE [dbo].[bsp_StatusPut] @statuskey bigint = NULL OUTPUT, @statusdatetime datetime = NULL, @status varchar(max) = NULL, @agency varchar(100) = NULL, @messagetype varchar(128) = NULL, @id uniqueidentifier = NULL AS SET NOCOUNT ON SET XACT_ABORT ON BEGIN TRANSACTION IF (@statuskey <= 0 OR @statuskey IS NULL) BEGIN -- Insert Values into the Status table INSERT Status ( statusdatetime, status, agency, messagetype, id) SELECT @statusdatetime, @status, @agency, @messagetype, @id SELECT @statuskey = SCOPE_IDENTITY() END ELSE BEGIN -- Update the Status table UPDATE Status SET statusdatetime = ISNULL(@statusdatetime, statusdatetime), status = ISNULL(@status, status), agency = ISNULL(@agency, agency), messagetype = ISNULL(@messagetype, messagetype), id = ISNULL(@id, id) WHERE statuskey = @statuskey END COMMIT TRANSACTION RETURN 0 GO
// This is not the entire class, see blog posts for more of the class public class BaseDataAccess : IDisposable { private IDbConnection oConnection; private IDbCommand oCommand; private IDbDataAdapter oAdapter; private IDataParameterCollection oParms; public string ConnectionString = ""; public BaseDataAccess(string connectionString) { this.ConnectionString = connectionString; this.oConnection = new SqlConnection(connectionString); this.oCommand = new SqlCommand(); this.oAdapter = new SqlDataAdapter((SqlCommand)this.oCommand); this.oCommand.Connection = this.oConnection; this.oCommand.CommandType = CommandType.StoredProcedure; } protected void SetAllParameters(DataRow Row) { this.ClearParameters(); for (int i = 0; i < Row.Table.Columns.Count; i++) { this.AddParms(Row.Table.Columns[i].ColumnName, Row[i]); } } public void AddParms(string ParmName, object ParmValue) { if (ParmName.StartsWith("@") == false) ParmName = "@" + ParmName; if (ParmValue != DBNull.Value) { if (this.oCommand.Parameters.IndexOf(ParmName) > -1) this.oCommand.Parameters[ParmName].Value = ParmValue; else this.oCommand.Parameters.AddWithValue(ParmName, ParmValue); } else this.RemoveParm(ParmName); } protected void ClearParameters() { this.oCommand.Parameters.Clear(); } public void RemoveParm(string ParmName) { if (ParmName.StartsWith("@") == false) ParmName = "@" + ParmName; if (this.oCommand.Parameters.IndexOf(ParmName) > -1) this.oCommand.Parameters.RemoveAt(ParmName); } public void SetParmDirection(string ParmName, ParameterDirection direction) { if (ParmName.StartsWith("@") == false) ParmName = "@" + ParmName; if (this.oCommand.Parameters.IndexOf(ParmName) > -1) this.oCommand.Parameters[ParmName].Direction = direction; } public void ExecuteCommand() { this.ExecuteCommand(this.oCommand.CommandText); } public void ExecuteCommand(string StoredProcName) { this.OpenConnection(); this.oCommand.CommandText = StoredProcName; try { this.oCommand.ExecuteNonQuery(); } catch (SqlException ex) { // Process the SqlException with your normal logging methodology } } public void OpenConnection() { try { if (this.oConnection.State != ConnectionState.Open) { this.oConnection.Open(); } } catch (Exception ex) { // Process the Exception with your normal logging methodology } } public void Dispose() { if (this.oConnection != null) { if (this.oConnection.State != ConnectionState.Closed) this.oConnection.Close(); this.oConnection.Dispose(); } } }
public void UpdateStatus(StatusDataSet ds) { using (DataAccess da = new DataAccess(this.MyConnectionString)) { da.SetCommand("bsp_StatusPut"); foreach (StatusDataSet.StatusRow call in ds.Status) { da.SetAllParameters(call); da.SetParmDirection("statuskey", ParameterDirection.InputOutput); da.ExecuteCommand(); } } }
#region Generating PUT Stored Procs StringBuilder ProcBuilder; public string GeneratePutStoredProc(string DatabaseName, string TableName) { DataTable dtColumns = new DataTable(); ; string ErrorMsg = ""; this.ProcBuilder = new StringBuilder(); // First, let's fill a DataTable with the information for each column in the database Table using (SqlConnection conn = new SqlConnection(this.ConnectionStringStatus)) { try { conn.Open(); dtColumns = conn.GetSchema("Columns", new[] { DatabaseName, null, TableName }); } catch (Exception ex) { ErrorMsg = ex.Message + "\r\n"; if (ex.InnerException != null && ex.InnerException.Message != ex.Message) ErrorMsg += ex.InnerException.Message + "\r\n"; } } // Get error-handling out of the way if (dtColumns.Rows.Count == 0 && ErrorMsg != "") ErrorMsg += string.Format("No columns retrieved for {0}/{1}.\r\n", DatabaseName, TableName); if (ErrorMsg != "") { ErrorMsg += "Check your database and/or connection"; MessageBox.Show(ErrorMsg); return ""; } // One of our database conventions is that the name of the PK column is always TableName + "Key" string KeyName = TableName + "Key"; // Then start building the SQL script for the creation of the Stored Proc, // calling a few methods to do the heavy lifting this.BuildHeader(TableName, "PUT"); this.BuildParameterList(dtColumns, KeyName); this.ProcBuilder.Append("\r\nAS\r\n"); this.ProcBuilder.Append("\tSET NOCOUNT ON\r\n"); this.ProcBuilder.Append("\tSET XACT_ABORT ON\r\n\r\n"); this.ProcBuilder.Append("\tBEGIN TRANSACTION\r\n"); this.ProcBuilder.AppendFormat("\tIF (@{0} <= 0 OR @{0} IS NULL)\r\n", KeyName); this.ProcBuilder.Append("\tBEGIN\r\n"); this.BuildInsert(dtColumns, KeyName, TableName); this.ProcBuilder.Append("\tEND\r\n\tELSE\r\n\tBEGIN\r\n"); this.BuildUpdate(dtColumns, KeyName, TableName); this.ProcBuilder.Append("\tEND\r\n\r\n\tCOMMIT TRANSACTION\r\n\tRETURN 0\r\n"); this.BuildFooter(); return this.ProcBuilder.ToString(); } protected void BuildHeader(string TableName, string ProcType) { string prefix = "bsp"; string fullProcName = String.Format("{0}_{1}{2}", prefix, TableName, ProcType); this.ProcBuilder.Append("\r\n"); this.ProcBuilder.Append("---------------------------------------------------------------\r\n"); this.ProcBuilder.AppendFormat("-- Stored Procedure {0}\r\n", fullProcName); this.ProcBuilder.AppendFormat("-- Basic {1} Procedure for {0} \r\n", TableName, ProcType); this.ProcBuilder.AppendFormat("-- Automatically generated {0}\r\n", DateTime.Now); this.ProcBuilder.Append("---------------------------------------------------------------\r\n"); this.ProcBuilder.AppendFormat("IF EXISTS(SELECT * FROM sys.objects WHERE name LIKE '{0}' and type = 'P')\r\n", fullProcName); this.ProcBuilder.AppendFormat("\tDROP PROCEDURE {0}\r\n", fullProcName); this.ProcBuilder.Append("GO\r\n"); this.ProcBuilder.Append("SET QUOTED_IDENTIFIER OFF \r\n"); this.ProcBuilder.Append("GO\r\n"); this.ProcBuilder.Append("SET ANSI_NULLS ON \r\n"); this.ProcBuilder.Append("GO\r\n\r\n"); this.ProcBuilder.AppendFormat("CREATE PROCEDURE {0}\r\n", fullProcName); } protected void BuildParameterList(DataTable dtColumns, string KeyName) { StringBuilder sb = new StringBuilder(); foreach (DataRow row in dtColumns.Rows) { if (sb.Length > 0) sb.Append(",\r\n"); sb.AppendFormat("\t@{0}\t\t{1} = NULL", row["COLUMN_NAME"], row["DATA_TYPE"]); if (row["COLUMN_NAME"].ToString().ToUpper() == KeyName.ToUpper()) sb.Append(" OUTPUT"); } this.ProcBuilder.Append(sb.ToString()); } protected void BuildInsert(DataTable dtColumns, string KeyName, string TableName) { this.ProcBuilder.AppendFormat("\t\t-- Insert Values into the {0} table\r\n", TableName); this.ProcBuilder.AppendFormat("\t\tINSERT {0}\r\n", TableName); StringBuilder sbColumns = new StringBuilder(); StringBuilder sbParms = new StringBuilder(); foreach (DataRow row in dtColumns.Rows) { if (row["COLUMN_NAME"].ToString().ToUpper() == KeyName.ToUpper()) continue; if (sbColumns.Length > 0) sbColumns.Append(",\r\n"); if (sbParms.Length > 0) sbParms.Append(",\r\n"); sbColumns.AppendFormat("\t\t\t{0}", row["COLUMN_NAME"]); sbParms.AppendFormat("\t\t\t@{0}", row["COLUMN_NAME"]); } this.ProcBuilder.Append("\t\t\t(\r\n"); this.ProcBuilder.AppendFormat("{0})\r\n", sbColumns.ToString()); this.ProcBuilder.Append("\t\tSELECT\r\n"); this.ProcBuilder.AppendFormat("{0}\r\n\r\n", sbParms.ToString()); this.ProcBuilder.AppendFormat("\t\tSELECT @{0} = SCOPE_IDENTITY()\r\n", KeyName); } protected void BuildUpdate(DataTable dtColumns, string KeyName, string TableName) { this.ProcBuilder.AppendFormat("\t\t-- Update the {0} table\r\n", TableName); this.ProcBuilder.AppendFormat("\t\tUPDATE {0}\r\n", TableName); this.ProcBuilder.Append("\t\tSET\r\n"); StringBuilder sb = new StringBuilder(); foreach (DataRow row in dtColumns.Rows) { if (row["COLUMN_NAME"].ToString().ToUpper() == KeyName.ToUpper()) continue; if (sb.Length > 0) sb.Append(",\r\n"); sb.AppendFormat("\t\t\t{0} = ISNULL(@{0}, {0})", row["COLUMN_NAME"]); } this.ProcBuilder.Append(sb.ToString()); this.ProcBuilder.AppendFormat("\r\n\t\tWHERE {0} = @{0}\r\n", KeyName); } protected void BuildFooter() { this.ProcBuilder.Append("\r\n"); this.ProcBuilder.Append("GO\r\n"); this.ProcBuilder.Append("SET QUOTED_IDENTIFIER OFF \r\n"); this.ProcBuilder.Append("GO\r\n"); this.ProcBuilder.Append("SET ANSI_NULLS ON \r\n"); this.ProcBuilder.Append("GO\r\n"); } #endregion
That's it. Happy coding!! =0)
Very nice Bonnie,
ReplyDeleteBut, Does that mean that each table has a PK with a name "statuskey" ?
Sorry, I did not see it immediately:
ReplyDelete// One of our database conventions is that the name of the PK column is always TableName + "Key"
string KeyName = TableName + "Key";
Thanks, I'm glad you like it. I hope that it's useful to you! =0)
Delete