Generated code - Transactions, Adapter
Preface
Adapter supports, like SelfServicing, both COM+ and ADO.NET transactions. Below is explained how ADO.NET transactions are used in Adapter, and how COM+
transactions can be used in Adapter. COM+, or better: enterprise services, offers more than just transactional behavior. You can also use COM+ to enable Just-In-Time
activation (JIT) or object pooling. The COM+ transaction section below has a small discussion on that as well.
Adapter automatically uses ADO.NET transactions for recursive saves and save/delete actions for collections of entities, unless the adapter object is owned by a
ComPlusAdapterContext object.
Normal native database transactions
(It's assumed the database used supports transactions, which is the case in all major databases like SqlServer).
Native database transactions are provided by ADO.NET; it's a part of an ADO.NET connection object.
Database statements can be executed within the same transaction by using the same connection object.
LLBLGen Pro's native database transactions are implemented in the DataAccessAdapter object. You can simply start a transaction using a DataAccessAdapter
object, execute methods of that DataAccessAdapter object and rollback or commit the transaction. Because the transactional code is inside the DataAccessAdapter,
every method of the DataAccessAdapter object you call after you've started a transaction is ran inside that transaction, including stored procedure calls, entity fetches,
entity collection saves etc. This greatly simplifies the programming of code using the variety of functionality the DataAccessAdapter object offers. You don't have to
add entity objects or entity collection objects to the DataAccessAdapter object to make them participate in the transaction, just call a DataAccessAdapter method and
it's inside the transaction of that particular DataAccessAdapter object. If you wish to run a particular action outside of a transaction, create a new DataAccessAdapter
object for that particular action.
Note:
|
If you start a new transaction by calling StartTransaction() the connection is kept open until Rollback() or Commit() is called.
|
An example will help illustrate the usage of the transaction functionality of the DataAccessAdapter object. We're going to update 2 different entities in one transaction.
For this example, it has to be done in one go, as an atomic unit, and therefore requires a transaction. The data is rather bogus, it's for illustration purposes only).
// [C#]
// create adapter for fetching and the transaction.
DataAccessAdapter adapter = new DataAccessAdapter();
// start the transaction.
adapter.StartTransaction(IsolationLevel.ReadCommitted, "TwoUpates");
try
{
// fetch the two entities
CustomerEntity customer = new CustomerEntity("CHOPS");
OrderEntity order = new OrderEntity(10254);
adapter.FetchEntity(customer);
adapter.FetchEntity(order);
// alter the entities
customer.Fax = "12345678";
order.Freight = 12;
// save the two entities again.
adapter.SaveEntity(customer);
adapter.SaveEntity(order);
// done
adapter.Commit();
}
catch
{
// abort, roll back the transaction
adapter.Rollback();
// bubble up exception
throw;
}
finally
{
// clean up. Necessary action.
adapter.Dispose();
}
' [VB.NET]
' create adapter for fetching and the transaction.
Dim adapter As New DataAccessAdapter()
' start the transaction.
adapter.StartTransaction(IsolationLevel.ReadCommitted, "TwoUpates")
Try
' fetch the two entities
Dim customer As New CustomerEntity("CHOPS")
Dim order As New OrderEntity(10254)
adapter.FetchEntity(customer)
adapter.FetchEntity(order)
' alter the entities
customer.Fax = "12345678"
order.Freight = 12
' save the two entities again.
adapter.SaveEntity(customer)
adapter.SaveEntity(order)
' done
adapter.Commit()
Catch
' abort, roll back the transaction
adapter.Rollback()
' bubble up exception
Throw
Finally
' clean up. Necessary action.
adapter.Dispose()
End Try
First a DataAccessAdapter object is created and a transaction is started. As soon as you start the transaction, a database connection is open and usable. This is
also the reason why you
must include a final clause and call Dispose() when the DataAccessAdapter object is no longer needed.
This is good practice anyway.
The code is the same as if you didn't start a transaction, except for the Commit() / Rollback() combination at the end. Simply start a transaction, call the methods
you want to call, and if no exceptions are caught, Commit(), otherwise Rollback().
It's best practice to embed the usage of a transaction in a try/catch/finally statement as it is done in the example above.
This ensures that if something fails during the usage of the transaction, everything is rolled back or, at the end, everything is committed correctly.
Transaction savepoints
Most databases support transaction savepoints. Transaction savepoints make it possible to do fine grained transaction control on a semi-nested level. This can
be required as ADO.NET doesn't support nested transactions. Savepoints let you define a point in a transaction to which you can roll back, without rolling back
the complete transaction. This can be handy if you have saved some entities in a transaction which were saved OK, and another one fails, however the failure of that
save shouldn't terminate the whole transaction, just roll back the transaction to a given point in the transaction. LLBLGen Pro offers you the ability to define
savepoints in a transaction. The following example illustrates the savepoint functionality. It first saves a new address entity and after that it saves the
transaction. It then saves a new customer entity but takes into account that this can fail. If it does, it should roll back to the savepoint set, it should thus
not rollback the complete transaction. Consider the example an illustration for the feature, in your code, the code utilizing the transaction will probably span
several classes and methods. Savepoints are not supported with COM+ transactions.
//C#
DataAccessAdapter adapter = new DataAccessAdapter();
try
{
adapter.StartTransaction(IsolationLevel.ReadCommitted, "SavepointRollback");
// first save a new address
AddressEntity newAddress = new AddressEntity();
// ... fill the address entity with values
// save it.
adapter.SaveEntity(newAddress, true);
// save the transaction
adapter.SaveTransaction("SavepointAddress");
// save a new customer
CustomerEntity newCustomer = new CustomerEntity();
// ... fill the customer entity with values
newCustomer.VisitingAddress = newAddress;
newCustomer.BillingAddress = newAddress;
try
{
adapter.SaveEntity(newCustomer, true);
}
catch(Exception ex)
{
// something was wrong.
// ... handle ex here.
// roll back to savepoint.
adapter.Rollback("SavepointAddress");
}
// commit the transaction. If the customer save failed,
// only address is saved, otherwise both.
adapter.Commit();
}
catch
{
// fatal error, roll back everything
adapter.Rollback();
throw;
}
finally
{
adapter.Dispose();
}
' VB.NET
Dim adapter As new DataAccessAdapter()
Try
adapter.StartTransaction(IsolationLevel.ReadCommitted, "SavepointRollback")
' first save a new address
Dim newAddress As New AddressEntity()
' ... fill the address entity with values
' save it.
adapter.SaveEntity(newAddress, True)
' save the transaction
adapter.SaveTransaction("SavepointAddress")
' save a new customer
Dim newCustomer As New CustomerEntity()
' ... fill the customer entity with values
newCustomer.VisitingAddress = newAddress
newCustomer.BillingAddress = newAddress
Try
adapter.SaveEntity(newCustomer, True)
Catch(Exception ex)
' something was wrong.
' ... handle ex here.
' roll back to savepoint.
adapter.Rollback("SavepointAddress")
End Try
' commit the transaction. If the customer save failed,
' only address is saved, otherwise both.
adapter.Commit()
Catch
// fatal error, roll back everything
adapter.Rollback()
Throw
Finally
adapter.Dispose()
End Try
Note:
|
Microsoft Access doesn't support savepoints in transactions, so this feature is not supported when you use LLBLGen Pro with MS Access.
|
COM+ transactions
LLBLGen Pro supports COM+ transactions for adapter as well through a special COM+ class called ComPlusAdapterContext. This class is generated in the DataAccessAdapter
class file and is thus located in the database specific project. The ComPlusAdapterContext class is actually a thin wrapper class which implements abstract methods
in its base class ComPlusAdapterContextBase. The ComPlusAdapterContext class embeds a DataAccessAdapter object which will act the same as a normal DataAccessAdapter
object, only this time it will utilize the COM+ context held by the ComPlusAdapterContext instance. Because the ComPlusAdapterContext class takes care of the database
connection creation, the COM+ context held by the ComPlusAdapterContext will make sure that any database activity which uses the connection objects created by the
ComPlusAdapterContext class are monitored by COM+ (the MS DTC service).
The ComPlusAdapterContext class is just one example how COM+ can be used together with Adapter. You can also create your own class, deriving from
ComPlusAdapterContextBase and add different EnterpriseServices attributes to it to enable different services offered by COM+, like Just-In-Time activation or
object pooling. Also you can for example grab the connection string from the COM+ object definition defined in Windows' Component Services.
Because COM+ is implemented in .NET using Enterprise Services, the class
using the ComPlusAdapterContext object has to derive from
ServicedComponent. This way, transactions started outside the class using the ComPlusAdapterContext class can flow through to the ComPlusAdapterContext
class to the actions performed by the DataAccessAdapter object inside the ComPlusAdapterContext object. Also, you have to reference the
System.EnterpriseServices namespace in your code. Below is a short example how a COM+ transaction
flows through to a ServicedComponent derived class and the code inside the TestComPlus() method will be running inside the COM+ transaction. When no
COM+ transaction is available, a new one is created.
// [C#]
[Transaction(TransactionOption.Required)]
public class TestClass : ServicedComponent
{
[AutoComplete]
public void TestComPlus()
{
ComPlusAdapterContext comPlusContext = new ComPlusAdapterContext();
IDataAccessAdapter adapter = comPlusContext.Adapter;
try
{
// start transaction.
adapter.StartTransaction(IsolationLevel.ReadCommitted, "ComPlusTran");
CustomerEntity customer = new CustomerEntity("CHOPS");
OrderEntity order = new OrderEntity(10254);
adapter.FetchEntity(customer);
adapter.FetchEntity(order);
// alter the entities
customer.Fax = "12345678";
order.Freight = 12;
// save the two entities again.
adapter.SaveEntity(customer);
adapter.SaveEntity(order);
// done
adapter.Commit();
}
catch
{
// abort
adapter.Rollback();
throw;
}
finally
{
comPlusContext.Dispose();
((DataAccessAdapter)adapter).Dispose();
}
}
}
' [VB.NET]
<Transaction(TransactionOption.Required)> _
Public Class TestClass
Inherits ServicedComponent
<AutoComplete> _
Public Sub TestComPlus()
Dim comPlusContext As New ComPlusAdapterContext()
Dim adapter As IDataAccessAdapter = comPlusContext.Adapter
Try
' start transaction.
adapter.StartTransaction(IsolationLevel.ReadCommitted, "ComPlusTran")
Dim customer As New CustomerEntity("CHOPS")
Dim order As New OrderEntity(10254)
adapter.FetchEntity(customer)
adapter.FetchEntity(order)
' alter the entities
customer.Fax = "12345678"
order.Freight = 12
' save the two entities again.
adapter.SaveEntity(customer)
adapter.SaveEntity(order)
' done
adapter.Commit()
Catch
' abort
adapter.Rollback()
Throw
Finally
comPlusContext.Dispose();
((DataAccessAdapter)adapter).Dispose();
End Try
End Sub
End Class
Note:
|
COM+ transactions are considered 'advanced material' in .NET applications. Use them with care. You have to give your assemblies a strong name
and your application will cause extra overhead on your machine: every serviced component has a context in the COM+ service. Most of the time you can
fulfill your transactional requirements using native database transactions with the normal ADO.NET transactions provided by the DataAccessAdapter, as
illustrated in the previous section.
|
.NET 2.0: System.Transactions support
.NET 2.0 introduces the System.Transactions namespace. This is a namespace with the TransactionScope class, which eases the creation of distributed transactions,
specifying a given scope. All transactions, for example normal ADO.NET transactions, are automatically elevated to distributed transactions, if required, by the
TransactionScope they're declared in. This requires support by the used database system as the database system has to be able to
promote a non-distributed
transaction to a distributed transaction. When .NET 2.0 shipped, only SqlServer 2005 was able to promote transactions to distributed transactions using
System.Transactions' classes.
The developer can define such a TransactionScope using the normal .NET constructs, like
using(TransactionScope scope = new TransactionScope())
{
// your code here.
}
A DataAccessAdapter object is able to determine if it's participating inside an ambient transaction of
System.Transactions. If so, it enlists a Resource Manager with the System.Transactions transaction. The Resource manager contains the
DataAccessAdapter object. As soon as a Transaction or DataAccessAdapter is enlisted through a Resource Manager, the Commit() and Rollback() methods
are setting the ResourceManager's commit/abort signal which is requested by the System.Transactions' Transaction manager. If multiple transactions are executed
on a DataAccessAdapter and one rolled back, the resource manager will report an abort. As soon as the DataAccessAdapter is enlisted in the
System.Transactions.Transaction, no ADO.NET transaction is
started, it's a no-op. Once one rollback is requested, the transaction will always report a rollback to the MSDTC.
Going out of scope
When the System.Transactions transaction is committed or rolled back, the Resource manager is notified and will then notify the DataAccessAdapter
that it can commit/rollback the transaction. That call will then notify the enlisted entities of the outcome of the transaction.
Multiple transactions executed using a single DataAccessAdapter object
For the DataAccessAdapter it will look like its still inside the same transaction, so no new transaction is started. This will make sure that an entity which
is already participating in the transaction isn't enlisted again and the field values aren't saved again etc.
Example
Below is an example which shows the usage of a TransactionScope in combination of a DataAccessAdapter object. The code contains Assert statements to
illustrate the state / outcome of the various statements.
// C#
CustomerEntity newCustomer = new CustomerEntity();
// fill newCustomer's fields.
// ..
AddressEntity newAddress = new AddressEntity();
// fill newAddress' fields.
// ..
// start the scope.
using( TransactionScope ts = new TransactionScope() )
{
// as we're inside the transaction scope, we can now create a DataAccessAdapter object and
// start a connection + transaction. The connection + transaction will be enlisted through a
// resource manager in the TransactionScope ts and will be controlled by that TransactionScope.
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
// save 2 entities non-recursive. This should be done in one
// transaction, namely the transaction scope we've started.
newCustomer.VisitingAddress = newAddress;
newCustomer.BillingAddress = newAddress;
Assert.IsTrue( adapter.SaveEntity( newCustomer, true) );
// save went well, alter the entities, which are fetched back, and
// save again.
newCustomer.CompanyEmailAddress += " ";
newAddress.StreetName += " ";
Assert.IsTrue( adapter.SaveEntity( newCustomer, true ) );
}
// do not call Complete, as we want to rollback the transaction and see if the rollback indeed succeeds.
// as the TransactionScope goes out of scope, the on-going transaction is rolled back.
}
// at this point the transaction of the previous using block is rolled back.
// let the DTC and the system.transactions threads deal with the objects.
// this sleep is only needed because we're going to access the data directly after the rollback. In normal code,
// this sleep isn't necessary.
Thread.Sleep( 1000 );
// test if the data is still there. Shouldn't be as the transaction has been rolled back.
using( DataAccessAdapter adapter = new DataAccessAdapter() )
{
CustomerEntity fetchedCustomer = new CustomerEntity( newCustomer.CustomerId );
Assert.IsFalse( adapter.FetchEntity( fetchedCustomer ) );
AddressEntity fetchedAddress = new AddressEntity( newAddress.AddressId );
Assert.IsFalse( adapter.FetchEntity( fetchedAddress ) );
Assert.AreEqual( 0, newAddress.AddressId );
}
' VB.NET
Dim NewCustomer As New CustomerEntity()
' fill NewCustomer's fields.
' ..
Dim NewAddress As New AddressEntity()
' fill NewAddress' fields.
' ..
' start the scope.
Using ts As New TransactionScope()
' as we're inside the transaction scope, we can now create a DataAccessAdapter object and
' start a connection + transaction. The connection + transaction will be enlisted through a
' resource manager in the TransactionScope ts and will be controlled by that TransactionScope.
Using adapter As New DataAccessAdapter()
' save 2 entities non-recursive. This should be done in one
' transaction, namely the transaction scope we've started.
NewCustomer.VisitingAddress = NewAddress
NewCustomer.BillingAddress = NewAddress
Assert.IsTrue( adapter.SaveEntity( NewCustomer, True) )
' save went well, alter the entities, which are fetched back, and
' save again.
NewCustomer.CompanyEmailAddress = NewCustomer.CompanyEmailAddress & " "
NewAddress.StreetName = NewAddress.StreetName & " "
Assert.IsTrue( adapter.SaveEntity( NewCustomer, True ) )
End Using
' do not call Complete, as we want to rollback the transaction and see if the rollback indeed succeeds.
' as the TransactionScope goes out of scope, the on-going transaction is rolled back.
End Using
' at this point the transaction of the previous using block is rolled back.
' let the DTC and the system.transactions threads deal with the objects.
' this sleep is only needed because we're going to access the data directly after the rollback. In normal code,
' this sleep isn't necessary.
Thread.Sleep( 1000 )
' test if the data is still there. Shouldn't be as the transaction has been rolled back.
Using adapter As New DataAccessAdapter()
Dim fetchedCustomer As New CustomerEntity( NewCustomer.CustomerId )
Assert.IsFalse( adapter.FetchEntity( fetchedCustomer ) )
Dim fetchedAddress = New AddressEntity( NewAddress.AddressId )
Assert.IsFalse( adapter.FetchEntity( fetchedAddress ) )
Assert.AreEqual( 0, NewAddress.AddressId )
End Using