Passing Tables of Data Directly To Stored Procedures With SQL Server
Ever wondered how to create multiple rows in a SQL Server table using ASP.NET?
You could of course do a loop inside C# code that adds a row at a time by calling a simple INSERT statement. But what if something goes wrong half way through the loop and you want to delete all the rows already added (rollback) so you can try it all again later?
A better way to add many rows at once is to pass a DataTable object directly to a Stored Procedure. Yes, you can do that. And it's fairly easy. Since discovering this approach I've used it in quite a few places.
Let's look at how simple it is.
Last week I mentioned a C# function to create unique voucher codes. Now, let's say we have two tables in a database to store these voucher codes - one called "VoucherRequests" - to store details of who, why and when the codes were requested and one called "VoucherCodes" to store the actual codes generated for each request. The tables are linked together by VoucherRequest row's ID column as a foreign key.
Now, assume our application POSTs the following information to the server:
description=some+vouchers+to+hand+out&quantity=50&value=100&expires=2011-11-23
The server then uses this information to create the right number of vouchers in our tables.
The C# code would look something like this:
requestsTableAdapter rta = new requestsTableAdapter(); vouchersTableAdapter vta = new vouchersTableAdapter(); DataTable vouchers = new DataTable(); vouchers.Columns.Add("VoucherCode"); vouchers.Columns.Add("VoucherValue"); vouchers.Columns.Add("VoucherExpires"); int count = Convert.ToInt32(context.Request["Quantity"]); int value = Convert.ToInt32(context.Request["Value"]); DateTime validUntil = DateTime.ParseExact(context.Request["Expires"], "dd/MM/yyyy", CultureInfo.InvariantCulture); validUntil = validUntil.AddDays(1).AddSeconds(-1); //Adjust time to 23:59:59 of the date in question int i=1; do { string code = CreateRandomString(8); if (vta.TestCodeUniqueness(code)<1){ DataRow dr = vouchers.NewRow(); dr[0] = code; // System.Guid.NewGuid().ToString("D"); dr[1] = value; dr[2] = validUntil.ToString("yyyy-MM-dd HH:mm:ss"); vouchers.Rows.Add(dr); i++; } } while (i <= count); rta.AddVouchers(context.Request["Description"], vouchers);
You should be able to see what this is doing. Even if some of it makes no sense - the principal is what's important.
What it does is build a temporary DataTable with 3 columns. Then it keeps trying to create a unique code to add to this table until it has added as many as was requested. Then it passes this table as-is, along with a description of the request, to a Stored Procedure (SP) via a TableAdapter.
The key to making this work is having a User-Defined Table Type in SQL Server, so that it knows how many column to expect and what data type they should be.
Here's the SQL to create this Table Type:
CREATE TYPE [dbo].[VouchersTableType] AS TABLE( [VoucherCode] [nvarchar](36) NOT NULL, [VoucherValue] [int] NOT NULL, [VoucherExpires] [datetime] NULL )
Once the Table Type is in place we can create a SP that uses it. Here's the SQL for SP to add multiple rows in one go:
CREATE PROCEDURE [dbo].[VoucherRequestADD] ( @description NVARCHAR(255), @TVP VouchersTableType READONLY ) AS SET NOCOUNT ON DECLARE @RequestID INT; BEGIN TRANSACTION INSERT INTO dbo.voucher_requests ([description], [created]) VALUES (@description, GETDATE()); SET @RequestID = SCOPE_IDENTITY(); INSERT INTO dbo.vouchers ([voucher_request_id] ,[voucher_code] ,[valid_until] ,[value]) SELECT @RequestID, VoucherCode, VoucherExpires, VoucherValue FROM @TVP; IF @@ERROR != 0 BEGIN ROLLBACK TRANSACTION --Roll back transaction on error SET @RequestID=0; GOTO handler END COMMIT TRANSACTION handler: SELECT @RequestID; GO
The key part of this SP is that the INSERT statement uses a SELECT to get its data values. The SELECT statement refers to the Table Type passed in to it.
Notice the INSERT part of the SP is wrapped in a TRANSACTION so that, if anything goes wrong, we can rollback to a previous state and abort the code.
This example is quite specialised, but the principle is the same and can be applied in lots of other scenarios. For example - a shopping cart which has multiple rows per item and one row in the main table to store data about the user and other bits.
I'm posting this here primarily as I hope it will help people Googling in the future, but also as an insight in to a different world for my regular (Domino developer) readers.
There is 'SQLTransaction' that you can use, that it retains the information, and can be rolled back through the vb.net or C#.
Here is a little snippet.
Public Sub Save()
Dim conn As New SqlConnection(connstring)
Try
conn.Open()
Dim trans As SqlTransaction = conn.BeginTransaction()
Save(trans)
If trans.Connection IsNot Nothing Then
trans.Commit()
End If
Finally
If conn.State <> ConnectionState.Closed Then
conn.Close()
End If
End Try
End Sub
Public Sub Save(ByVal trans as SQLTransaction)
Dim result As Integer
Try
result = cmd.ExecuteNonQuery()
If IsNew Then
_ID = Integer.Parse(cmd.Parameters("@ID").Value.ToString)
Else
End If
Catch ex As Exception
trans.Rollback()
Throw ex
End Try
End If
End Sub
Reply
Interesting. Didn't know about that. Like everything - there's always more than one way to do the same thing.
I've grown used to using table adapters and tend not to use connections to SQL from my code.
Reply
Show the rest of this thread
I don't want to sound like a smartarse, but it is never good idea to mix code from different places into your main program (i.e. SQL into C# etc.). I don't want to compile my program every time I need to fix a bug in SQL statement.
Call me old fationed, but I prefer code separation. SQL statements should be in stored procedures and only amount of SQL that you cannot avoid should be kept in your software. If nothing else, for maintenance reasons.
Reply
I used the book "Visual Basic .NET Business Objects" by Rockford Lhotka, as a basis for my classes and business objects. I am sure there is probably a C# version as well.
Reply
Nice Post, I tend to do this sort of thing using an xml type that way I do not have to create the table type up front, I can just do it on the fly, It would be an interesting study to look at a performance comparison though.
Reply
I would recommend using the Microsoft Enterprise Library.
http://msdn.microsoft.com/en-us/library/ff632023.aspx
I know you prefer coding everything by yourself but this framework is really mature these days. And you don't have to use all blocks of the framework but can start using only the Data Access Application Block.
Regards,
Mark
Reply
Thanks Mark. Hadn't seen that before and it looks very interesting.
It's not that I prefer to write my own code it's more a case of wanting to learn ASP.NET from the "ground up". I don't want to dive straight in to using frameworks before I've even got used to and comfortable with the basics.
Reply