I am trying to understand the CSLA Transaction Attribute and would like to ask the more experienced developers here for some help. I have a base object DataPortal_Insert( ) function that is handling the insertion of an object into the database. It is a one to one relationship between the object and the database table.
The method is decorated with the Transactional(TransactionalTypes.TransactionScope)], even though it is probably not needed.
Secondly, I have other objects that inherit from the base object that are "owned" by other objects. These objects override the DataPortal_Insert() method. The overridden method calls the base.DataPortal_Insert() method first to insert the object, then makes another database call to insert the record in the "link" table. This method is also decorated with the same attribute and needs to be handled as a transaction. I want to rollback the database if either table fails to update. With just the attribute in place, the database updates the first table even if the second one fails, but if I wrap the code in a using TransactionScope statement as shown below, the transaction works as expected. I have read back through the book looking for reasons why the attribute does not work as expected, but with my limited knowledge in this area, I do not understand why. I would appreciate any light that can be shed on this subject and advice that can be given on the "correct" or "standard" way to accomplish this.
[
Transactional(TransactionalTypes.TransactionScope)] protected override void DataPortal_Insert(){
using (TransactionScope ts = new TransactionScope()){
base.DataPortal_Insert(); using (SqlCommand cmd = SQLStoredProcedure.New(DataConnection.ConnectionString).Command){
cmd.CommandText =
"InsertEmployeeOwner";cmd.Parameters.AddWithValue(
"@ownerID", _ownerID);cmd.Parameters.AddWithValue(
"@employeeID", ObjectID); IAsyncResult result = cmd.BeginExecuteNonQuery();cmd.EndExecuteNonQuery(result);
}
ts.Complete();
}
}
Thanks in advance....David
That is the point...if I remove the using (ts = ) and the ts.Complete() and just use the attribute, the code does not process in a transaction.
On your other comment, the code is just test code that I am using to try out different scenarios. At one point I was working with asyncronous calls and just left the code as is. But I appreciate the advice.
dmccrory:That is the point...if I remove the using (ts = ) and the ts.Complete() and just use the attribute, the code does not process in a transaction.
I have some very generic objects such as Address, Email and Telephone. These objects can belong to any one of a number of other objects such as an Employee, a Client or a Vendor. The database is designed to allow the 2 tables (i.e. Address and Employee) to be linked via a separate table. The idea was to only have one address table and use in anytime I had the need for an address and link it to the owner of the address regardless who that owner was. I didn't want to repeat fields like street, city and state in every table such as CleintAddress, EmployeeAddress and VendorAddress.
The Address and Client objects are derived from the same Base class because the only difference between the 2 objects is the fact that the Address object has an owner and the Client object does not. The base object takes care of the CRUD function for the base object and the object that has an "owner" has an overriden DataPortal_Insert() method that calls the base.DataPortal_Insert() to populate the main table, then has addditional code to populate the "owner" or link table.
There is probably a "better" or "more intuitive" way to do this, but with my limited experience, it is the solution that I am attempting to make work.
I strongly recommend against using inheritance to reuse fields. Inheritance should be used to reuse methods only. That's a design best practice overall, because inheritance is about reuse of behavior, not reuse of data.
It sounds like you should have two classes: Address and Client. And they should both inherit from BusinessBase<T> directly. They clearly have different responsibilities in your use cases and so should be separate types.
It is fine to have Address be an editable child that can be a child of several parents. There should be no problem with that, as long as you are consistent with how you manage transactions across all your root objects.
But Client is very clearly something different from Address. It sounds like it is an editable root, and that it plays a different role in your use cases from storing address information for some other object. If it has a different responsibility, then it should be its own object.
That'll make your life much simpler.
Remember that reuse isn't the goal. Maintainability is the goal. Too much reuse decreases maintainability because it increases coupling and thus complexity.
Rocky,
I created a DatabaseObject<T> that inherits directly from BusinessBase<T> and contains all of the audit type data fields that are included in every table for these types of objects such as EnteredBy, EnteredDateTime and LastChanged. I also created a “DataHelper” object that uses custom attributes on the object’s properties to “fill” the object’s custom fields or the stored procedure parameters as needed. I did this to try to apply the DRY principle to my code because, after reviewing the initial code, the only differences in the code were the stored procedure name and the custom fields for each object.
This is an area of great frustration to me. I am a self taught programming enthusiast. I have read countless books on every topic imaginable and have tried to understand and apply the principles the author is trying to convey. It is not what I do for a living, but it is definitely what motivates me. It is my hobby and my passion and I aspire to get to a level to allow me to develop “rich” applications applying the “best practices” available. I consider your (Rocky) book one of the best learning tools I have read. From Chapter one, I just felt like I “got it”. While some of the details and examples are over my knowledge level, I felt like I understood “why” you were doing what you were doing. But when trying to apply other principles and practices that I have read, I don’t quite understand which should be applied and which should not. I always think that there has to be a reason why some of the other principles are not applied, but I struggle understanding when to and when NOT do apply certain principles or practices.
I went straight to work in the railroad industry right out of high school so I do not have the college education that most do in this field have, but I have worked myself into an “analyst” type position working for a software company that writes software for the railroad industry. Our software is written for the iSeries platform with a case tool called Synon. We have a small .NET initiative, but most of the code written in Visual Basic in a “procedural” type style with very little object oriented principles applied so I don’t have the luxury of finding someone that can guide me in my current work environment.
What I long for is a “mentor” type person that can review my code and direct me to the correct or “best practice” way to accomplish my task. I am not looking for someone to write the code for me and I am willing to do the research and work needed, I just need someone to help me understand the when and where of it all. I am not looking for something for nothing and do not have a monetary need so if I were to find someone willing to teach or coach me to be a better programmer, I would be willing to trade development time for free. I think I have a good enough understanding to be able to write some of the simple type coding that has to be done every day by someone.
I apologize for the long winded reply and appreciate anyone who has taken the time to read this far. I truly appreciate the amount of work and time required to develop the talent and skill the people in this forum display on a daily basis and I am at “awe” sometimes at how the answers to, what I consider, very technical questions are handled with ease providing much needed insight to all.
Thanks,
-David
Ok...I took the forum's advice and decided to move all of the fields into each object and not inherit them as I was doing before. Now back to the original question. My DataPortal_Insert() method (see below) is not handled as a transaction with the [Transactional(TransactionalTypes.TransactionScope)] tag, but if I use using (TransactionScope ts = new TransactionScope()), it works as expected. Can someone please explain the difference?
This does not work....I intentionally created an error for the second table....the first table is updated and the second is not.
[
Transactional(TransactionalTypes.TransactionScope)] protected override void DataPortal_Insert(){
try{
using (SqlCommand cmd = SQLStoredProcedure.New(DataConnection.ConnectionString).Command){
cmd.CommandText =
"InsertEmployee"; SqlParameter parm = new SqlParameter("@newID", SqlDbType.UniqueIdentifier);parm.Direction =
ParameterDirection.Output;cmd.Parameters.Add(parm);
AssignObjectParameters(cmd);
cmd.Parameters.AddWithValue(
"@enteredBy", Csla.ApplicationContext.User.Identity.Name);cmd.Parameters.AddWithValue(
"@enteredDateTime", DateTime.Now);parm =
new SqlParameter("@newLastChanged", SqlDbType.Timestamp);parm.Direction =
ParameterDirection.Output;cmd.Parameters.Add(parm);
cmd.ExecuteNonQuery();
_auditInfo.LastChanged = (
byte[])cmd.Parameters["@newLastChanged"].Value;EmployeeID = (
Guid)cmd.Parameters["@newID"].Value;cmd.CommandText =
"InsertEmployeeOwner";cmd.Parameters.Clear();
cmd.Parameters.AddWithValue(
"@ownerID", _ownerID);cmd.Parameters.AddWithValue(
"@employeeID", EmployeeID);cmd.ExecuteNonQuery();
UpdateObjectLists();
}
}
catch (SqlException exc){
throw new Exception(exc.Message);}
}
This works....with the intentional error, neither table is updated as expected.
protected
override void DataPortal_Insert(){
try{
using (TransactionScope ts = new TransactionScope()){
using (SqlCommand cmd = SQLStoredProcedure.New(DataConnection.ConnectionString).Command){
cmd.CommandText =
"InsertEmployee"; SqlParameter parm = new SqlParameter("@newID", SqlDbType.UniqueIdentifier);parm.Direction =
ParameterDirection.Output;cmd.Parameters.Add(parm);
AssignObjectParameters(cmd);
cmd.Parameters.AddWithValue(
"@enteredBy", Csla.ApplicationContext.User.Identity.Name);cmd.Parameters.AddWithValue(
"@enteredDateTime", DateTime.Now);parm =
new SqlParameter("@newLastChanged", SqlDbType.Timestamp);parm.Direction =
ParameterDirection.Output;cmd.Parameters.Add(parm);
cmd.ExecuteNonQuery();
_auditInfo.LastChanged = (
byte[])cmd.Parameters["@newLastChanged"].Value;EmployeeID = (
Guid)cmd.Parameters["@newID"].Value;cmd.CommandText =
"InsertEmployeeOwner2";cmd.Parameters.Clear();
cmd.Parameters.AddWithValue(
"@ownerID", _ownerID);cmd.Parameters.AddWithValue(
"@employeeID", EmployeeID);cmd.ExecuteNonQuery();
UpdateObjectLists();
}
ts.Complete();
}
}
catch (SqlException exc){
throw new Exception(exc.Message);}
}
I am not familiar with the SqlStoredProcedure type. My only guess is you are using a DAL framework, and perhaps it is somehow messing with the way CSLA uses TransactionScope.
Though even that seems unlikely, given that all CSLA does is wrap the call to DP_I inside a using block just like the one you are creating by hand.
Are you sure the exception is flowing up from your DP_I method? CSLA relies on your exception to trigger the rollback.
You could step through the code in the debugger (if you have a version of Csla.dll built for debug) and see where the code goes as it flows back into the data portal.
The SqlStoredProcedure isn't really doing anything other than returning a SqlCommand.
I will do some more work to see if the exception is flowing correctly.
Thanks,
Could this be because I am throwning a System.Exception in my catch block? Should I be throwing a different type of exception? I tried throwing a DataPortalException, but got the same results...
-David
System.Exception should be fine.
But where are you opening the db connection? Since that wasn't in your code, I figured it was being opened in that SqlStoredProcedure object or something?
I believe that the connection must be opened after you are inside the TransactionScope, so it gets enlisted in the transaction properly (though I don't know that for a fact).
But that's my guess, is that somehow the connection isn't being opened at the right time or in the right way or something such that it doesn't get enlisted in the transaction created by the TransactionScope.
Yes I am opening the db connection and returning the SqlCommand from that object.
If I am using the TransactionScope attribute, I was assuming that the entire method would be inside the TransactionScope.
I will eliminate the SqlStoredProcedure object and see if the problem is corrected.
Thanks,
David
I have changed the DataPortal_Insert() method to contain all of the code without any outside calls. However the results are the same.....the Insert method is not handled as a transaction.
I will continue to investigating, but I would appreciate any information if someone has had the same problem, or sees something wrong with the code.
[Transactional(TransactionalTypes.TransactionScope)]
protected override void DataPortal_Insert()
{
try
{
SqlConnection con = new SqlConnection(DataConnection.ConnectionString);con.Open();
SqlCommand cmd = con.CreateCommand();cmd.CommandType =
CommandType.StoredProcedure;cmd.CommandText =
"InsertEmployee"; SqlParameter parm = new SqlParameter("@newID", SqlDbType.UniqueIdentifier);parm.Direction =
ParameterDirection.Output;cmd.Parameters.Add(parm);
FillObjectParameters(cmd);
cmd.Parameters.AddWithValue(
"@enteredBy", Csla.ApplicationContext.User.Identity.Name);cmd.Parameters.AddWithValue(
"@enteredDateTime", DateTime.Now);parm =
new SqlParameter("@newLastChanged", SqlDbType.Timestamp);parm.Direction =
ParameterDirection.Output;cmd.Parameters.Add(parm);
cmd.ExecuteNonQuery();
_auditInfo.LastChanged = (
byte[])cmd.Parameters["@newLastChanged"].Value;EmployeeID = (
Guid)cmd.Parameters["@newID"].Value;cmd.CommandText =
"InsertEmployeeOwner2";cmd.Parameters.Clear();
cmd.Parameters.AddWithValue(
"@ownerID", _ownerID);cmd.Parameters.AddWithValue(
"@employeeID", EmployeeID);cmd.ExecuteNonQuery();
}
catch (SqlException exc){
throw new Exception(exc.Message);}
}
Thanks for taking a look and for the suggestion. I have removed the Try and Catch blocks as suggested, but the results are the same.
I know what the exception is because I am purposely generating it to test the transaction. The first stored procedure is inserting the record properly and the second stored procedure (which has an incorrect Stored Procedure name) generates an exception.
If I use a using (TransactionScope ts = new TransactionScope()) block with a call to ts.Complete(); the code runs as a transaction and the first table insert is rolled back when the second table insert fails.
If I use the [Transactional(TransactionalTypes.TransactionScope)] attribute the first table insert does not rollback when the second table insert fails.
Ok. I think I will create a stripped down object to test with. If it passes, I will start adding things back to try to isolate the problem.
Thanks for your time. I will let you know the results.
-David
I have stripped everything out of my object that is not needed and I am still experiencing the same problem so I beginning to think the problem is not related to my code. I have an employee object that inherits directly from BusinessBase<T>. The DataPortal_Insert() method does not make any outside calls other than a static call to get the connection string. The code for the insert event is posted below.
Just to restate the problem so you don't have to read the entire thread.
The DataPortal_Insert() method is not being handled as a transaction when using the [Transactional(TransactionalTypes.TransactionScope)] attribute, but if I use a using (TransactionScope ts = new TransactionScope()) block the transaction is rolled back properly. I am purposely creating an exception by setting the second CommandText property to an invalid stored procedure name.
Could someone please try to reproduce this and let me know if they experience the same problem? I am using Visual Studio 2008 Beta 2 Professional and CSLA 3.0.1
Thanks,
-David
This code is not handled as a transaction. The first table is updated even though the second table fails.
[
Transactional(TransactionalTypes.TransactionScope)] protected override void DataPortal_Insert(){
SqlConnection con = new SqlConnection(DataConnection.ConnectionString);con.Open();
SqlCommand cmd = con.CreateCommand();cmd.CommandType =
CommandType.StoredProcedure;cmd.CommandText =
"InsertEmployee";cmd.Parameters.AddWithValue(
"@firstName", _firstName);cmd.Parameters.AddWithValue(
"@middleName", _middleName);cmd.Parameters.AddWithValue(
"@lastName", _lastName);cmd.Parameters.AddWithValue(
"@hireDate", _hireDate);cmd.Parameters.AddWithValue(
"@hireDateNumber", _hireDateNumber);cmd.Parameters.AddWithValue(
"@active", _active);cmd.Parameters.AddWithValue(
"@enteredBy", Csla.ApplicationContext.User.Identity.Name);cmd.Parameters.AddWithValue(
"@enteredDateTime", DateTime.Now); SqlParameter parm = new SqlParameter("@newID", SqlDbType.UniqueIdentifier);parm.Direction =
ParameterDirection.Output;cmd.Parameters.Add(parm);
parm =
new SqlParameter("@newLastChanged", SqlDbType.Timestamp);parm.Direction =
ParameterDirection.Output;cmd.Parameters.Add(parm);
cmd.ExecuteNonQuery();
_lastChanged = (
byte[])cmd.Parameters["@newLastChanged"].Value;_employeeID = (
Guid)cmd.Parameters["@newID"].Value;cmd.CommandText =
"InsertEmployeeOwner2";cmd.Parameters.Clear();
cmd.Parameters.AddWithValue(
"@ownerID", _ownerID);cmd.Parameters.AddWithValue(
"@employeeID", _employeeID);cmd.ExecuteNonQuery();
}
This code is handled as a transaction. The first table rolls back when the second table fails.
protected
override void DataPortal_Insert(){
using (TransactionScope ts = new TransactionScope()){
SqlConnection con = new SqlConnection(DataConnection.ConnectionString);con.Open();
SqlCommand cmd = con.CreateCommand();cmd.CommandType =
CommandType.StoredProcedure;cmd.CommandText =
"InsertEmployee";cmd.Parameters.AddWithValue(
"@firstName", _firstName);cmd.Parameters.AddWithValue(
"@middleName", _middleName);cmd.Parameters.AddWithValue(
"@lastName", _lastName);cmd.Parameters.AddWithValue(
"@hireDate", _hireDate);cmd.Parameters.AddWithValue(
"@hireDateNumber", _hireDateNumber);cmd.Parameters.AddWithValue(
"@active", _active);cmd.Parameters.AddWithValue(
"@enteredBy", Csla.ApplicationContext.User.Identity.Name);cmd.Parameters.AddWithValue(
"@enteredDateTime", DateTime.Now); SqlParameter parm = new SqlParameter("@newID", SqlDbType.UniqueIdentifier);parm.Direction =
ParameterDirection.Output;cmd.Parameters.Add(parm);
parm =
new SqlParameter("@newLastChanged", SqlDbType.Timestamp);parm.Direction =
ParameterDirection.Output;cmd.Parameters.Add(parm);
cmd.ExecuteNonQuery();
_lastChanged = (
byte[])cmd.Parameters["@newLastChanged"].Value;_employeeID = (
Guid)cmd.Parameters["@newID"].Value;cmd.CommandText =
"InsertEmployeeOwner2";cmd.Parameters.Clear();
cmd.Parameters.AddWithValue(
"@ownerID", _ownerID);cmd.Parameters.AddWithValue(
"@employeeID", _employeeID);cmd.ExecuteNonQuery();
ts.Complete();
}
}
I would but I am embarrased to say that I don't know how to attach anything to posts in this forum......lol
-David
I have attached the files, including the 3 stored procedures. Let me know if there is anything else you need to look at.
Thanks,
-David
Andy,
Thanks for taking the time to examine and explain the problem. I have made the changes and I am now getting the results as you described. This will go a long way to helping me understand this part of the framework.
As for your note...In this case, I think I will need to examine the EditableRootListBase as, although there is a collection of employees, each employee really stands on it's own.
Thanks again, you have been a big help
-David
So maybe I am still off a little. I am displaying the list in a grid, but editing in an edit form. I have read the book (and bought the handbook today) but I will re-read it again and see if it is clearer now.
The one thing that always confuses me is when objects are part of a collection. If I have a collection of employees that belong to a specific client. I am assuming that the client is the root and the employees are children of the client. But each employee is also an indepent object. I need to re-visit this in the book now that I have spent some time with the framework.
Thanks again for all of your help
-David
I re-read chapter 8 last night and came to the same conclusion. I understood the "add" and "edit" functions because the user was presented with an edit screen and the root object (employee) was saved as expected. But the "delete" function was confusing me. The user selects a record from the grid to delete and a confirmation box is all that is displayed. If confirmed, I was able to delete the employee object, but I could not remove it from the list because it was not a child.
I have changed this to match the scenario you describe. I have a Client object (root), an EmployeeInfo object (child), an EmployeeList object (collection of EmployeeInfo objects) and an Employee object (root). If the user wants to add or edit an employee, the root employee object is retrieved and diplayed in an edit screen. If the user wants to delete an employee, the Employee object is deleted and the EmployeeInfo object is deleted from the EmployeeList. I think this is how this is supposed to work.
I do have one question though. In my original design, Employee was a root object and EmployeeList was a collection of Employees. I could use Add() to add an Employee to EmployeeList, but I could not use Remove() to remove an Employee from EmployeeList because Employee was not a child. Shouldn't that work the same either way? Why can I add an object that is not a child, but I cannot remove an object that is not a child?
Copyright (c) Marimer LLC