It is common to configure back-end database tables to contain a column with an auto-generated ID unique to each row. When a row is added, the application layer just makes up an ID for each newly added row (-1, -2 etc.).
When the newly added row is added back to the database (think
TableAdapter.Update
) the database auto-generates the actual ID. But the application still skips blithely along with its made-up ID. When subsequent operations are performed on the database (like deleting the newly added row), this situation can result in a Concurrency exception. Yes, a Concurrency exception. Even with a single-user, single-threaded application. Threw me for a loop.
Apparently this is not such a big deal for SQL users, because you can bundle multiple commands into a single transaction. SO they just bundle a command that retrieves the ID from the database after the row has been added and update the
DataTable
accordingly. I don't know much about SQL, so please comment if you can shed more light on this mechanism.
For us poor schlubs stuck with Access, it seems that we can't bundle commands into a single transaction. So what's a guy/gal to do?
An article[
^]from the talented Beth Massi describes a solution for VB .NET, and my tip is closely modeled on her article. I did have to make some changes for C# though.
So here is a walkthrough:
- Include your database in your project. This will auto-generate a xxDataset.xsd (xx is your database name).
- Double-click xxDataset.xsd to bring up the Database Designer.
- Right click any table and choose View Code. This auto-generates a xxDataSet.cs file that EXTENDS the auto-generated code that Visual Studio created for you.
- Delete the contents of xxDataSet.cs and replace it with the following:
namespace DatabaseTest.App_Data.{dbName}DataSetTableAdapters
{
public partial class {tableName}TableAdapter
{
public void HookUpHandlers()
{
this.Adapter.RowUpdated +=
new OleDbRowUpdatedEventHandler(Adapter_RowUpdated);
}
public void Adapter_RowUpdated(object sender, OleDbRowUpdatedEventArgs e)
{
TableAdapterHelper.PopulateKey(e, Connection, Transaction);
}
}
public static class TableAdapterHelper
{
public static void PopulateKey(
OleDbRowUpdatedEventArgs e,
OleDbConnection connection,
OleDbTransaction transaction)
{
if (e.StatementType == StatementType.Insert)
{
DataColumn[] pk = e.Row.Table.PrimaryKey;
if (pk != null && pk.Length > 0)
{
DataColumn primaryKeyColumn = pk[0];
OleDbCommand cmd = new OleDbCommand(
"SELECT @@IDENTITY", connection, transaction);
int id = (int)cmd.ExecuteScalar();
e.Row[primaryKeyColumn] = id;
}
}
}
}
}
Then call HookUpHandlers()
from wherever the tableadapter is initialized:
MyTableAdapter _myTableAdapter = new MyTableAdapter();
_myTableAdapter.HookUpHandlers();
So what does this do? Every time the
TableAdapter
is used to update a row of the database
TableAdapterHelper.PopulateKey
is called. If the update is an
Insert (Add)
, then we need to worry about the database generating an ID different from that in the
DataTable
. We check if a primary key was defined for this table. If so, we execute a nifty little query
(SELECT @@IDENTITY)
that returns the unique ID last generated by the database. Then we update the
DataTable
with the correct ID. Easy as pie. Which is an odd expression, given that pie is one of the more difficult pastries to make. But that's OK - my goal is communication, not accuracy! Wait, that's not right. Maybe I'll write an article about this...
So anyway, why does this work? Because the code is stuck right inside the
TableAdapter.Update RowUpdated
callback - IN THE MIDDLE OF THE TRANSACTION. That's the key. The
SELECT @@IDENTITY
will only work if called when the transaction is still active, which is why this is so much easier with the command-bundling provided by SQL.
Other fun stuff - what if multiple rows are added before
TableAdapter.Update
is called? No problem.
RowUpdated
callback gets fired for each row, one at a time, and everything works fine.
What about multiple tables? This example code is only for one table, it's true. My application has quite a few more tables, and the code got somewhat ugly. I would appreciate any better ideas, but I just added one
TableAdapter
extension (partial class) per table. And then I called
HookUpHandlers
for each
TableAdapter
. Not very extensible, I know. Maybe something nifty with reflection?