Disable read/write to a table via SqlTransaction in .net? - c#

How to use SqlTransaction in .net 2.0 so that when I start reading data from a table, that table is blocked for others (other programs) to read/write to that table?
If SqlTransaction is not a good option, than what is?

This should be allowed by using Serializable transaction together with TABLOCKX hint in initial select statement. TABLOCKX should take exclusive lock on the table so nobody else can use that table and Serializable transaction should demand HOLDLOCK which means that all locks are kept until end of the transaction (you can use HOLDLOCK directly).
Update: I just tested different scenarios in Management studio and it
looks like you do not need to
explicitly use Serializable
transaction. Using TABLOCKX within any
transaction is enough.
Be aware that such approach can be big bottleneck because only one transaction can operate on such table = no concurrency. Even if you read and work with single record from million nobody else will be able to work with the table until your transaction ends.
So the command should look like:
SELECT * FROM Table WITH (TABLOCKX) WHERE ...
To use serializable transaction you can use SqlTransaction:
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlTransaction transaction = connection.BeginTransaction(IsolationLevel.Serializable);
try
{
...
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
...
}
}
Or System.Transactions.TransactionScope (default isolation level should be Serializable).
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
...
}
scope.Complete();
}

Related

C#, SQLServer - block table row reading, edit if needed and release lock

I need to block database reads on row level while I'm executing update logic for same row. How would nice and clean solution look like? Here is some code I'm working on:
using (SqlConnection conn = new SqlConnection(Configuration.ConnectionString)) {
conn.Open();
using (SqlTransaction tran = conn.BeginTransaction(IsolationLevel.Serializable)) {
//TODO lock table row with ID=primaryKey (block other reads and updates)
using (SqlCommand cmd = new SqlCommand("SELECT Data FROM MyTable WHERE ID=#primaryKey", conn)) {
cmd.Parameters.AddWithValue("#primaryKey", primaryKey);
using (var reader = cmd.ExecuteReader()) {
data = PopulateData(reader);
};
}
if (isUpdateNeeded(data)) {
ChangeExternalSystemStateAndUpdateData(data) //EDIT - concurrent calls not allowed
WriteUpdatesToDatabase(conn, tran, data); //EDIT
tran.Commit();
}
} //realease lock and allow other processes to read row with ID=primaryKey
}
EDIT:
I have following constraints:
Code can be executed within different App pools simultaneously. So memory lock is not an option
ChangeExternalSystemStateAndUpdateData() must be executed only once in the scope of the MyTable row. Concurrent calls will cause problems
Usually the problem here isn't so much row locking, but rather: other SPIDs acquiring read locks between your read and your update - which can lead to deadlock scenarios; the common fix here is to acquire a write lock in the initial read:
SELECT Data FROM MyTable WITH (UPDLOCK) WHERE ID=#primaryKey
Note that if another SPID is explicitly using NOLOCK then they'll still blitz past it.
You could also try adding ROWLOCK, i.e. WITH (UPDLOCK, ROWLOCK) - personally I'd keep it simple initially. Since you're in a serializable context, trying to be too granular may be a lost cause - key-range locks, etc.

How to force extenal library to use SQL transaction

I have an external library to which I pass an instance of System.Data.SqlClient.SqlConnection and I want to wrap everything that library does on that connection in a transaction. When I was working with php/doctrine I would simply do exactly that in such cases - start a transaction in my code, call stuff on the library which issues DB queries and then commit the transaction in my code. When I tried to use this approach in C#, I got the following exception:
ExecuteScalar requires the command to have a transaction when the connection assigned to the command is in a pending local transaction. The Transaction property of the command has not been initialized.
So I took a look at the library code and it always uses SqlCommand without setting the Transaction property. Is it possible to achieve my goal somehow? (changing the library code isn't feasible)
You haven't posted your code but I assume you tried to use an explicit transaction by calling SqlConnection.BeginTransaction().
You can use a TransactionScope to create an implicit transaction. Any connection, command created inside the TransactionScope's lifetime will be enlisted in a transaction automatically.
Copying from Implementing an Implicit Transaction using Transaction Scope's example:
// Create the TransactionScope to execute the commands, guaranteeing
// that both commands can commit or roll back as a single unit of work.
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection1 = new SqlConnection(connectString1))
{
// Opening the connection automatically enlists it in the
// TransactionScope as a lightweight transaction.
connection1.Open();
// Create the SqlCommand object and execute the first command.
SqlCommand command1 = new SqlCommand(commandText1, connection1);
returnValue = command1.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command1: {0}", returnValue);
// If you get here, this means that command1 succeeded. By nesting
// the using block for connection2 inside that of connection1, you
// conserve server and network resources as connection2 is opened
// only when there is a chance that the transaction can commit.
using (SqlConnection connection2 = new SqlConnection(connectString2))
{
// The transaction is escalated to a full distributed
// transaction when connection2 is opened.
connection2.Open();
// Execute the second command in the second database.
returnValue = 0;
SqlCommand command2 = new SqlCommand(commandText2, connection2);
returnValue = command2.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
}
}
// The Complete method commits the transaction. If an exception has been thrown,
// Complete is not called and the transaction is rolled back.
scope.Complete();
}
The connection and both commands in this example run under a single transaction. Should an exception occur, the transaction will be rolled back.
In .NET you can use a TransationScope, and everything will happen in the same transaction:
using (TransactionScope scope = new TransactionScope())
{
// Everything inside this block will be transactional:
// Call the libraries which will use your SqlConnection here
}
Or you can use the BeginTransaction before calling the other library functions, and commit it after the function calls

Why might a SqlTransaction fail to commit when the commit function is called?

I'm running into an issue where changes made are being rolledback even when none of the queries throw an exception. It's strange since the code works in one environment but isn't committing changes in another.
Here is the function that handles the transaction. When I put a break point on the commit I hit the commit and I can see the changes in the database but when the transaction is disposed the changes are rolled back.
UPDATE:Additional tests show that it isn't a problem with the transaction. If the transaction is completely removed from the code below the app behaves in the same way. The changes are undone when the connection closes.
public bool Transaction(List<string> sqlStatements)
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
conn.Open();
using (SqlTransaction tran = conn.BeginTransaction())
{
try
{
foreach (string query in sqlStatements)
{
SqlCommand cmd = new SqlCommand(query, conn, tran);
cmd.CommandTimeout = 300;
cmd.ExecuteNonQuery();
}
tran.Commit();
return true;
}
catch (SqlException sqlError)
{
tran.Rollback();
//Log Exception
return false;
}
}
}
}
Though I was sure, I tried your code at my end and it worked as expected. Again I am repeating that the method is good enough for the transaction handling. And once the transaction is committed, it can't be roll back.
In the above method, transaction disposal has got nothing to do with any rollback. I think, you have been debugging in wrong direction. Though, you may paste the original method here as you might be doing some other database operations.
Just out of blue, what kind of queries you have been firing? Do note that the DDL commands are auto-commit and transaction won't be effective.
When you say you "can see the changes in the database", how are you determining this? I would expect them to be "in the database" if the following query returns the data that was "committed" (run this tsql after stepping over the commit call in Sql Server Management Studio for example):
-- Force the isolation level to "read committed" so we
-- guarantee we are getting data that has definitely been committed.
-- If the data changes back, it must have been from a separate operation.
set transaction isolation level read committed
begin tran
select * from MyTableWithExpectedChanges;
-- You aren't changing anything so this can be rollback or commit
rollback tran
If the data did indeed commit, I would run a SQL Server Profiler session and see what is causing the data to revert back. It sounds like something separate is triggering to restore the data in that scenario.
If the data didn't commit, you have some sort of transaction count mismatch as per other comments.
This issue was eventually tracked back to a trigger that had recently been updated to include a transaction.
We solved the issue by removing the transaction from the trigger.

Change the TransactionScope IsolationLevel after completing the transaction

When i save data in the database i used TransactionScope with IsolationLevel set to Serializable.
TransactionOptions options = new TransactionOptions
{
IsolationLevel=IsolationLevel.Serializable
};
using (var transaction = new TransactionScope(TransactionScopeOption.Required,options))
{
transation.Complete();
}
Now after the execution is over, i want to change the TransactionScopeIsolationLevel.
Edit
What I understand is this, if IsolationLevel set to Serializable then after completing the transaction, the connection object is closed and return to connection pool and when some other request arrives it fetch that connection object from the pool and thus effected by the previous IsolationLevel. So i want to change isolation level to default after every transaction.
You're right: the isolation level is not reset when returning a connection to the pool. This is horrible behavior but we are stuck with it...
There are two strategies:
Reset the isolation level before returning: This is your approach.
Always use a connection with an explicit transaction (or TransactionScope) so that the isolation level is guaranteed.
I recommend you do the latter.
If you insist on doing (1) you can simply change the isolation level after closing the TransactionScope but you have to do this with the connection object. Example:
using (SqlConnection connection = new SqlConnection(connectionString))
{
using (var transaction = new TransactionScope(TransactionScopeOption.Required,options))
{
connection.Open(); //open inside of scope so that the conn enlists itself
transation.Complete();
}
//conn is still open but without transaction
conn.ExecuteCommand("SET TRANSACTION ISOLATION LEVEL XXX"); //pseudo-code
} //return to pool
Does this work for you?
I've been bit by this. Luckily the Connection String logic was centralized. What I did was to change the connection string's application setting if Transaction.Current is not null (which would imply that we're inside a TransactionScope).
This way the TransactionScope connections don't pool with the others.

How to rollback multiple Queries on different database servers in case of any error

I am using different SQL procedures in an application.
First procedures insert some rows then some processing in C#code and then 2nd procedure
do some updation then again some code processing then third procedure delete some record and then insert new record. When all is done on Sever 1 then data is fetch from this server and sent to Server 2 there record is deleted and new record is inserted.
IF there is error at any stage on any server in any procedure i want to roll back all the record.
I can not use begin trans because processing takes time and can not block table as others users are also using same tables in parallel. So kindly tell me how can i achieve it without blocking the table for other users.
Thanks in advance.
Edited (Added code example):
I tried Transaction Scope but i am getting exception while opening the connection. I configured MS DTC but may be not configured properly.
"
Network access for Distributed Transaction Manager (MSDTC) has been disabled. Please enable DTC for network access in the security configuration for MSDTC using the Component Services Administrative tool."
using (TransactionScope ts = new TransactionScope(TransactionScopeOption.Required))
{
try
{
dl.SetBookReadyToLive(13570, false);
//SetBookReadyToLive
dl.AddTestSubmiitedTitleID(23402);
dl.AddBookAuthorAtLIve(13570, 1);
ts.Complete();
}
catch (Exception ex)
{
Response.Write(ex.Message);
}
}
public void SetBookReadyToLive(long BookID, bool status)
{
try
{
if (dbConMeta.State != ConnectionState.Open)
dbConMeta.Open();
SqlCommand cmd = new SqlCommand("spSetBookReadyToLive", dbConMeta);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Clear();
cmd.Parameters.Add("#BookID", BookID);
cmd.Parameters.Add("#status", status);
cmd.ExecuteNonQuery();
if (dbConMeta.State == ConnectionState.Open)
dbConMeta.Close();
}
catch
{
if (dbConMeta.State == ConnectionState.Open)
dbConMeta.Close();
}
}
I get the exception on opening the connection of method>
I am using SQL Server 2000, i have set the configuration of MS DTC on the machine where SQL Server is installed and also on my PC from where i am running the code. But still same exception.
Kindly help me to configure it
You can use the TransactionScope class. It works generally well but in case of distributed SQL servers like in your case requires the MS DTC enabled in both servers and configured properly (security has to be granted for execution of network transactions, distributed ones and so on...)
here a copy paste from an example on MSDN, you could "almost" use it like this... :)
// Create the TransactionScope to execute the commands, guaranteeing
// that both commands can commit or roll back as a single unit of work.
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection1 = new SqlConnection(connectString1))
{
// Opening the connection automatically enlists it in the
// TransactionScope as a lightweight transaction.
connection1.Open();
// Create the SqlCommand object and execute the first command.
SqlCommand command1 = new SqlCommand(commandText1, connection1);
returnValue = command1.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command1: {0}", returnValue);
// If you get here, this means that command1 succeeded. By nesting
// the using block for connection2 inside that of connection1, you
// conserve server and network resources as connection2 is opened
// only when there is a chance that the transaction can commit.
using (SqlConnection connection2 = new SqlConnection(connectString2))
{
// The transaction is escalated to a full distributed
// transaction when connection2 is opened.
connection2.Open();
// Execute the second command in the second database.
returnValue = 0;
SqlCommand command2 = new SqlCommand(commandText2, connection2);
returnValue = command2.ExecuteNonQuery();
writer.WriteLine("Rows to be affected by command2: {0}", returnValue);
}
}
// The Complete method commits the transaction. If an exception has been thrown,
// Complete is not called and the transaction is rolled back.
scope.Complete();
}
source: TransactionScope Class
to minimize locks you could specify the IsolationLevel with the overload of the constructor which takes a TransactionScopeOptions, default is Serializable if you are fine with that you could set it to ReadCommitted.
Note: Personally I would not use this one unless absolutely needed, because it's a bit of a pain to have the DTC always configured and Distributed Transactions are in general slower than local ones but really depends on your BL / DAL logic.
Short answer : The same way you would do it if you would do it in MS SQL Management Studio.
You open a connection to a server.
Open a transaction for a specific server
You run your queries related to this server
You make sure to keep your connection alive while you... [go back to 1. for next server]
If all your queries worked, commit all your changes.
Else, rollback all your queries.
Warning : The first table will most likely be locked until you're done with all your servers/queries. What you could do here to help this : If you got a lot of data, you can transfer the data to temporary tables on every servers before doing the step #2. Once this is done, you open the transaction, do your fast things, then commit/rollback asap.
Note: I know you asked how to achieve this without locking the tables, hence why I added an idea in the « warning » part.

Categories

Resources