I am attempting to use TransactionScope across two queries to different SQL Server instances (the transaction is promoted to MSDTC). I have it working synchronously without using Tasks but am unable to get the transaction to rollback when using Tasks asynchronously.
I have hidden both the connection strings to the servers and the update statements, but reast assured that they both connect and execute the SQL with no problems. I have intentionally set the SQL in the method runTwo() to fail and raise an exception.
I am using .Net 4.5.2 which includes the enumeration TransactionScopeAsyncFlowOption.Enabled (in the TransactionScope constructor) that I believe should handle a transaction across threads but am unable to get this to work.
static void Main()
{
List<Task> tasks = new List<Task>();
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
try
{
tasks.Add(Task.Run(() => runOne()));
tasks.Add(Task.Run(() => runTwo()));
Task.WhenAll(tasks);
//Complete the scope
scope.Complete();
}
catch (Exception)
{
Transaction.Current.Rollback();
}
}
Console.ReadLine();
}
private static void runOne()
{
//Get the base SQL connection
using (SqlConnection conn = new SqlConnection("Data Source=SERVER1....."))
{
conn.Open();
SqlCommand command1 = new SqlCommand
{
CommandText = "Update .. Complete Successfully",
CommandType = CommandType.Text,
Connection = conn
};
command1.ExecuteNonQuery();
}
}
private static void runTwo()
{
using (
SqlConnection conn =
new SqlConnection("Data Source=SERVER2...")
)
{
conn.Open();
SqlCommand command2 = new SqlCommand
{
CommandText = "Update .... Raises Exception",
CommandType = CommandType.Text,
Connection = conn
};
//Execute the command
command2.ExecuteNonQuery();
}
}
This:
Task.WhenAll(tasks);
Is your problem. Task.WhenAll returns an awaitable, it doesn't block on the method call. Since you're using a console application which cannot be awaited, you'll have to defer to use Task.WaitAll instead which will explicitly block until both requests finish and will propagate any exception via AggregateException
try
{
Task.WaitAll(new[] { Task.Run(() => runOne()),
Task.Run(() => runTwo()) });
scope.Complete();
}
Related
I'm trying to figure out how to setup my database access correctly while using the SqlClient hitting a Microsoft SQL Server. For the most part it is working, but there's a particular scenario that is giving me trouble. Namely: attempting to simultaneously use two connections in the same thread; one with an open data reader and the other performing a delete operation.
The following code demonstrates my conundrum:
public class Database {
...
internal SqlConnection CreateConnection() => new SqlConnection(connectionString);
...
}
public IEnumerable<Model> GetModel() {
var cmd = new SqlCommand() { ... };
using(var conn = db.CreateConnection()) {
conn.Open();
cmd.Connection = conn;
using(var reader = cmd.ExecuteReader()) {
while(reader.Read()) {
var m = new Model();
// deserialization logic
yield return m;
}
}
}
}
public void Delete(int id) {
var cmd = new SqlCommand() { ... }
using(var conn = db.CreateConnection()) {
conn.Open(); // throwing the error here
cmd.Connection = conn;
cmd.ExecuteNonQuery();
}
}
Application Code:
using(var scope = new TransactionScope()) {
var models = GetModels();
foreach(var m in models) {
Delete(m.Id); // throws an exception
}
scope.Complete();
}
For whatever reason, an exception is thrown by the above code while trying to execute the Delete operation:
quote
System.Transactions.TransactionAbortedException: The transaction has aborted. ---> System.Transactions.TransactionPromotionException: Failure while attempting to promote transaction. ---> System.Data.SqlClient.SqlException: There is already an open DataReader associated with this Command which must be closed first. ---> System.ComponentModel.Win32Exception: The wait operation timed out
quote
Now, I have confirmed that if I either set MultipleActiveResultSets=true or Pooling=false on the ConnectionString, that then the above application code will work without error. However, it doesn't seem like I should need to set either of those. If I open two connections simultaneously, should they not be separate connections? Why then am I getting an error from the Delete connection saying that there's an open DataReader?
Please help <3
By far the easiest fix here is to simply load all the models outside the transaction before you go deleting any. Eg
var models = GetModels().ToList();
using(var scope = new TransactionScope()) {
foreach(var m in models) {
Delete(m.Id); // throws an exception
}
scope.Complete();
}
Even fetching the models inside the transaction shold work
using(var scope = new TransactionScope()) {
var models = GetModels().ToList();
foreach(var m in models) {
Delete(m.Id); // throws an exception
}
scope.Complete();
}
so long as you don't leave the connection open during the iteration. If you allow the connection in GetModels() to close, it will be returned to the connection pool, and be available for use for subsequent methods that are enlisted in the same transaction.
In the current code the connection in GetModels() is kept open during the foreach loop and Delete(id) has to open a second connection and try to create a distributed transaction, which is failing.
Without MultipleActiveResultsets, the GetModels connection can't be promoted to a distributed transaction in the middle of returning query results. Setting pooling=false will not make this error go away.
Here's a simplified repro to play with:
using Microsoft.Data.SqlClient;
using System.Collections.Generic;
using System.Transactions;
namespace SqlClientTest
{
class Program
{
static void Main(string[] args)
{
Setup();
var topt = new TransactionOptions();
topt.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;
using (new TransactionScope(TransactionScopeOption.Required, topt ))
{
foreach (var id in GetIds())
{
Delete(id);
}
}
}
static string constr = #"server=.;database=tempdb;Integrated Security=true;TrustServerCertificate=true;";
public static void Setup()
{
using (var con = new SqlConnection(constr))
{
con.Open();
var cmd = con.CreateCommand();
cmd.CommandText = "drop table if exists ids; select object_id id into ids from sys.objects";
cmd.ExecuteNonQuery();
}
}
public static IEnumerable<int> GetIds()
{
using (var con = new SqlConnection(constr))
{
con.Open();
var cmd = con.CreateCommand();
cmd.CommandText = "select object_id id from sys.objects";
using (var reader = cmd.ExecuteReader())
{
while (reader.Read())
{
yield return reader.GetInt32(0);
}
}
}
}
public static void Delete(int id)
{
using (var con = new SqlConnection(constr))
{
con.Open();
var cmd = con.CreateCommand();
cmd.CommandText = "insert into ids(id) values (#id)";
cmd.Parameters.Add(new SqlParameter("#id", id));
cmd.ExecuteNonQuery();
}
}
}
}
And here's what Profiler shows when run:
The main reason here as far as I understand is your yielding iteration.
So the DB connection has not yet called disposed as it's still being used in your iteration (foreach). If for example, you called .ToList() at that point it should return all the entries and then dispose of the connection.
See here for a better explanation on how yield works in an iteration: https://stackoverflow.com/a/58449809/3329836
I am trying to make multiple parallel updates to database using Task.WhenAll. The code flow goes like this.
In the main method, i have created a transaction scope and created clones of the main transactions and passed to the child. Main transactions is blocked until child is completed
using (var scope = DalcHelper.GetTransactionScope())
{
DependentTransaction transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
var task1= Dalc.UpdateDetails1(transaction );
DependentTransaction transaction1 = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
var task2 = Dalc.UpdateDetails2(transaction1);
await Task.WhenAll(task1, task2 ).ConfigureAwait(false);
scope.Complete();
}
The DalcMethod goes like this. Here the clone created from the outer transaction goes as a parameter. The dependent transaction is completed notifying the main transaction that the dependent is completed
try
{
using (SqlCommand databaseCommand = DalcHelper.GetCommand(SPName))
using (var scope = new TransactionScope(dependentCloneTransaction, TransactionScopeAsyncFlowOption.Enabled))
{
-- Update database
scope.Complete();
}
}
finally
{
//Call complete on the dependent transaction
dependentCloneTransaction.Complete();
}
Dalc methods are asynchronous methods which returns Task
I am getting the below exception
The transaction has aborted.Failure while attempting to promote transaction.There is already an open DataReader associated with this Command which must be closed first.The wait operation timed out
. Can anyone tell me what am i doing wrong here?
namespace Playground
{
static class DalcHelper
{
public static TransactionScope GetTransactionScope()
{
return new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
}
public async static Task ReadDetails1(DependentTransaction transaction,SqlConnection conn)
{
try
{
string commandText = "SELECT * FROM dbo.Persons"; // some table, say Persons
using (SqlCommand cmd = new SqlCommand(commandText, conn))
{
cmd.CommandType = System.Data.CommandType.Text;
SqlDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
while (reader.Read())
{
int Id = reader.GetInt32("Id");
Console.WriteLine("Id " + Id);
}
reader.Close();
}
transaction.Complete();
return;
}
catch (Exception ex)
{
Console.WriteLine("Task 1"+ ex.Message);
}
}
public async static Task ReadDetails2(DependentTransaction transaction1, SqlConnection conn)
{
try
{
string commandText = "SELECT * FROM dbo.Persons";
using (SqlCommand cmd = new SqlCommand(commandText, conn))
{
cmd.CommandType = System.Data.CommandType.Text;
SqlDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.Default);
while (reader.Read())
{
int age = reader.GetInt32("Age");
Console.WriteLine("Age " + age);
}
reader.Close();
}
transaction1.Complete();
return;
}
catch (Exception ex)
{
Console.WriteLine("Task 2" + ex.Message);
}
}
}
class Program
{
static void Main(string[] args)
{
string connectionString = "YourConnectionString";
_ = RunMe(connectionString);
}
private async static Task RunMe(string connectionString)
{
try
{
Task task1 = Task.Run( async()=> {
using (TransactionScope scope = DalcHelper.GetTransactionScope())
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
DependentTransaction transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
conn.Open();
await DalcHelper.ReadDetails1(transaction, conn);
/*
* add more tasks if you wish to
*/
Console.WriteLine("Completed task 1");
conn.Close();
}
scope.Complete();
}
});
Task task2 = Task.Run(async () =>
{
using (TransactionScope scope = DalcHelper.GetTransactionScope())
{
using (SqlConnection conn = new SqlConnection(connectionString))
{
DependentTransaction transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete);
conn.Open();
await DalcHelper.ReadDetails2(transaction, conn);
/*
may be update some column of table based on previous op.
// await DalcHelper.UpdateDetails2(transaction, conn);
*/
Console.WriteLine("Completed task 2");
conn.Close();
}
/*
calling `Complete` method will commit all the changes within the transaction scope(including the UpdateDetails2 method)
need not dispose transaction scope explicitly, `using` block takes care of that
*/
scope.Complete();
}
});
await Task.WhenAll(task1, task2);// at this point every task added is complete
Console.WriteLine("completed both tasks");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
}
Some important points to remember when working with transaction scope
It is required to dispose TransactionScope within the same thread it was created or might get an error thrown like Transaction already aborted.
Any update operations is persisted only if TransactionScope.Complete() method is called.
Make sure you open individual connections for each thread and close it after its usage.That being said, I am not sure from a performance standpoint about using an individual connection for each thread. I am happy to get educated more on this and I'll update my answer. However, this solution should help you solve your problem.
Do read some useful answers already posted related to the topic
Dependent transaction
Transaction aborted issues
Thread based connection
I am getting Database is locked exception from SQLite for some queries only.
Below is my code:
When I execute any select statement it works fine.
When I am executing any write statement on Jobs Table it also works fine.
This works fine:
ExecuteNonQuery("DELETE FROM Jobs WHERE id=1");
But the same way if I am executing queries for Employees table it is throwing an exception that database is locked.
This throws Exception:
ExecuteNonQuery("DELETE FROM Employees WHERE id=1");
Below are my functions:
public bool OpenConnection()
{
if (Con == null)
{
Con = new SQLiteConnection(ConnectionString);
}
if (Con.State == ConnectionState.Closed)
{
Con.Open();
//Cmd = new SQLiteCommand("PRAGMA FOREIGN_KEYS=ON", Con);
//Cmd.ExecuteNonQuery();
//Cmd.Dispose();
//Cmd=null;
return true;
}
if (IsConnectionBusy())
{
Msg.Log(new Exception("Connection busy"));
}
return false;
}
public Boolean CloseConnection()
{
if (Con != null && Con.State == ConnectionState.Open)
{
if (Cmd != null) Cmd.Dispose();
Cmd = null;
Con.Close();
return true;
}
return false;
}
public Boolean ExecuteNonQuery(string sql)
{
if (sql == null) return false;
try
{
if (!OpenConnection())
return false;
else
{
//Tx = Con.BeginTransaction(IsolationLevel.ReadCommitted);
Cmd = new SQLiteCommand(sql, Con);
Cmd.ExecuteNonQuery();
//Tx.Commit();
return true;
}
}
catch (Exception exception)
{
//Tx.Rollback();
Msg.Log(exception);
return false;
}
finally
{
CloseConnection();
}
}
This is the Exception:
At line 103 : Cmd.ExecuteNonQuery();
Exception Found:
Type: System.Data.SQLite.SQLiteException
Message: database is locked
database is locked
Source: System.Data.SQLite
Stacktrace: at System.Data.SQLite.SQLite3.Step(SQLiteStatement stmt)
at System.Data.SQLite.SQLiteDataReader.NextResult()
at System.Data.SQLite.SQLiteDataReader..ctor(SQLiteCommand cmd, CommandBehavior behave)
at System.Data.SQLite.SQLiteCommand.ExecuteReader(CommandBehavior behavior)
at System.Data.SQLite.SQLiteCommand.ExecuteNonQuery()
at TimeSheet6.DbOp.ExecuteNonQuery(String sql) in d:\Projects\C# Applications\Completed Projects\TimeSheet6\TimeSheet6\DbOp.cs:line 103
Somewhere along the way a connection is getting left open. Get rid of OpenConnection and CloseConnection and change ExecuteNonQuery to this:
using (SQLiteConnection c = new SQLiteConnection(ConnectionString))
{
c.Open();
using (SQLiteCommand cmd = new SQLiteCommand(sql, c))
{
cmd.ExecuteNonQuery();
}
}
Further, change the way you read data to this:
using (SQLiteConnection c = new SQLiteConnection(ConnectionString))
{
c.Open();
using (SQLiteCommand cmd = new SQLiteCommand(sql, c))
{
using (SQLiteDataReader rdr = cmd.ExecuteReader())
{
...
}
}
}
Do not attempt, to manage connection pooling on your own like you are here. First, it's much more complex than what you have coded, but second, it's handled already inside the SQLiteConnection object. Finally, if you're not leveraging using, you're not disposing these objects properly and you end up with issues like what you're seeing now.
You can use 'using' statement as below, that will make sure connection & command disposed correctly even in exception
private static void ExecuteNonQuery(string queryString)
{
using (var connection = new SQLiteConnection(
ConnectionString))
{
using (var command = new SQLiteCommand(queryString, connection))
{
command.Connection.Open();
command.ExecuteNonQuery();
}
}
}
You should close your DataReader before attempting to write any data to the database. Use:
dr.Close();
after you finish using the DataReader.
In my case it was very stupid of me, I was making changes in SQLite browser and did not click on write changes, which locked the DB to be modified by the services. After I clicked the Write changes button, all the post request worked as expected.
A lot of helpful posts here for folks that may have forgotten to clean up a dangling connection, but there is another way this can happen: SQLite does not support concurrent INSERTs; if you issue two INSERTs at the same time the will be processed in serial. When the INSERTs are quick this is fine, but if an INSERT takes longer than the timeout the second INSERT can fail with this message.
I had this happen when I used a long running transaction to accumulate a bunch of INSERTs into one big commit. Basically I locked the database from any other activity during the transaction. Switching to journal_mode=WAL will allow concurrent writes and reads, but not concurrent writes.
I got rid of the long running transaction and let each INSERT autocommit, and that solved my problem.
Mine was caused by not closing a SqliteDataReader when calling HasRows().
I had this:
using (SQLiteConnection connection = new SQLiteConnection(DbPath))
{
connection.Open();
string sql = $"SELECT * FROM ...";
using (SQLiteCommand command = new SQLiteCommand(sql, connection))
{
return command.ExecuteReader().HasRows;
}
connection.Close();
}
But needed to put a using around the ExecuteReader like so:
using (SQLiteDataReader reader = command.ExecuteReader())
{
return command.ExecuteReader().HasRows;
}
Even though the DbConnection was being disposed and re-created each time the db was still being kept locked by the reader.
I was also getting the same error here:
if (new basics.HindiMessageBox(HMsg, HTitle).ShowDialog()==true)
{
SQLiteConnection m_dbConnection = new SQLiteConnection(MainWindow.con);
m_dbConnection.Open();
sql = "DELETE FROM `users` WHERE `id`=" + SelectedUser.Id;
command = new SQLiteCommand(sql, m_dbConnection);
command.ExecuteNonQuery();
m_dbConnection.Close();
LoadUserDG();
}
but when I just changed SQLiteConnection declaration location
public partial class User : Window
{
SQLiteCommand command;
string sql;
AddUser AddUserObj;
List<basics.users> usersList;
basics.users SelectedUser;
SQLiteConnection m_dbConnection;
// ...
private void DeleteBtn_Click(object sender, RoutedEventArgs e)
{
// ...
if (new basics.HindiMessageBox(HMsg, HTitle).ShowDialog()==true)
{
m_dbConnection = new SQLiteConnection(MainWindow.con);
m_dbConnection.Open();
sql = "DELETE FROM `users` WHERE `id`=" + SelectedUser.Id;
command = new SQLiteCommand(sql, m_dbConnection);
command.ExecuteNonQuery();
m_dbConnection.Close();
LoadUserDG();
}
}
Everything is fine now.
I hope this may work for you, too.
If someone can say how this happened, I would like to know the details to improve my knowledge, please.
I had the same issue when loading a lot of data to different tables from multiple threads.
When trying to do the inserts I was getting database locked because the program was doing too many insert too fast and SQLite didn't have time to complete each transaction before another one came.
The insert are done through threading because I didn't want the interface to be locked and wait for the insert to be done.
My solution is to use BlockingCollection with ThreadPool.QueueUserWorkItem.
This allows me to free the interface while doing the inserts.
All the insert are queued and executed in FIFO (First In First Out) order.
Now the database is never locked while doing any SQL transaction from any thread.
public class DatabaseQueueBus
{
private BlockingCollection<TransportBean> _dbQueueBus = new BlockingCollection<TransportBean>(new ConcurrentQueue<TransportBean>());
private CancellationTokenSource __dbQueueBusCancelToken;
public CancellationTokenSource _dbQueueBusCancelToken { get => __dbQueueBusCancelToken; set => __dbQueueBusCancelToken = value; }
public DatabaseQueueBus()
{
_dbQueueBusCancelToken = new CancellationTokenSource();
DatabaseQueue();
}
public void AddJob(TransportBean dto)
{
_dbQueueBus.Add(dto);
}
private void DatabaseQueue()
{
ThreadPool.QueueUserWorkItem((param) =>
{
try
{
do
{
string job = "";
TransportBean dto = _dbQueueBus.Take(_dbQueueBusCancelToken.Token);
try
{
job = (string)dto.DictionaryTransBean["job"];
switch (job)
{
case "SaveClasse":
//Save to table here
break;
case "SaveRegistrant":
//Save Registrant here
break;
}
}
catch (Exception ex)
{//TODO: Handle this exception or not
}
} while (_dbQueueBusCancelToken.Token.IsCancellationRequested != true);
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
}
});
}
}
The inserts are done this way, but without the queuing I was still getting the lock issue.
using (SQLiteConnection c = new SQLiteConnection(BaseDal.SQLiteCon))
{
c.Open();
using (SQLiteCommand cmd = new SQLiteCommand(sql, c))
{
cmd.ExecuteNonQuery();
}
c.Close();
}
Playing with transactions for the first time I thought I'd get the following code to work:
namespace database
{
class Program
{
static string connString = "Server=ServerName;Database=Demo;Trusted_Connection=True;";
SqlConnection connection = new SqlConnection(connString);
static Random r = new Random();
static void Add()
{
try
{
Thread.Sleep(r.Next(0, 10));
using (var trans = new TransactionScope())
{
using (var conn = new SqlConnection(connString))
{
conn.Open();
var count = (int)new SqlCommand("select balance from bank WITH (UPDLOCK) where owner like '%Jan%'", conn).ExecuteScalar();
Thread.Sleep(r.Next(0, 10));
SqlCommand cmd = new SqlCommand("update bank set balance = " + ++count + "where owner like '%Jan%'", conn);
cmd.ExecuteNonQuery();
}
trans.Complete();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
static void Remove()
{
try
{
Thread.Sleep(r.Next(0, 10));
using (var trans = new TransactionScope())
{
using (var conn = new SqlConnection(connString))
{
conn.Open();
var count = (int)new SqlCommand("select balance from bank WITH (UPDLOCK) where owner like '%Jan%'", conn).ExecuteScalar();
Thread.Sleep(r.Next(0, 10));
SqlCommand cmd = new SqlCommand("update bank set balance = " + --count + "where owner like '%Jan%'", conn);
cmd.ExecuteNonQuery();
}
trans.Complete();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
Thread t = new Thread(new ThreadStart(Add));
t.Start();
}
for (int i = 0; i < 5; i++)
{
Thread t = new Thread(new ThreadStart(Remove));
t.Start();
}
Console.ReadLine();
}
}
}
I assumed that at the end after 100 adds and 100 subtractions my balane would be the same as my starting point - 100, however it keeps changing up and down every time I run the script. Even with isolationlevel serializable. Could anyone tell me why? O_o
EDIT: Moved connection opening and closing to inside the transaction scope.
The problem now is that I get "Transaction (Process ID XX) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction"
Like Marc Gravell Said:
Putting the connection inside the transaction scope and adding UPDLOCK to the select query combined with changing isolationlevel to repeatableRead did the trick :)
static void Add()
{
try
{
Thread.Sleep(r.Next(0, 10));
using (var trans = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.RepeatableRead }))
{
using (var conn = new SqlConnection(connString))
{
conn.Open();
var count = (int)new SqlCommand("select balance from bank WITH (UPDLOCK) where owner like '%Jan%'", conn).ExecuteScalar();
Thread.Sleep(r.Next(0, 10));
SqlCommand cmd = new SqlCommand("update bank set balance = " + ++count + "where owner like '%Jan%'", conn);
cmd.ExecuteNonQuery();
}
trans.Complete();
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
1: currently the TransactionScope might be redundant and unused; try changing the transaction to wrap the connection, not the other way around (oh, and use using):
using (var trans = new TransactionScope(TransactionScopeOption.Required,
new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }))
using (var conn = new SqlConnection(connString))
{
conn.Open();
//...
trans.Complete();
}
this way, the connection should enlist correctly inside the transaction (and be cleaned up properly if something bad happens)
I think the above is the main problem; i.e. not enlisting in the transaction. That means that there can be lost changes, since the read/write operation is not actually being raised to a higher isolation level.
2: however, if you do that by itself, I expect you'll see deadlocks. To avoid deadlocks, if you know you're going to update, you might want to use (UPDLOCK) on that select - this will take a write lock at the start, so that if there is a competing thread you get a block rather than a deadlock.
To be clear, this deadlock scenario is caused by:
thread A reads the row, getting a read lock
thread B reads the row, getting a read lock
thread A tries to update the row, and is blocked by B
thread B tries to update the row, and is blocked by A
Adding the UPDLOCK, this becomes:
thread A reads the row, getting a write lock
thread B tries to read the row, and is blocked by A
thread A updates the row
thread A completes the transaction
thread B is able to continue, reads the row, getting a write lock
thread B updates the row
thread B completes the transaction
3: but querying to do a trivial update is silly; better just to issue an in-place update without selecting, i.e. update bank set balance = balance + 1 where ...
You have to open the connection inside the TransactionScope block.
Instead of
var conn = new SqlConnection(connString);
conn.Open();
using (var trans = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }))
{
// do stuff
}
use it like this
using (var trans = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions() { IsolationLevel = IsolationLevel.Serializable }))
using (var conn = new SqlConnection(connString))
{
conn.Open();
// do stuff
}
This way opening the connection automatically enlists it in the TransactionScope as a lightweight transaction.
You can always look at the examples.
Is this code solid? I've tried to use "using" etc. Basically a method to pass as sequenced list of SQL commands to be run against a Sqlite database.
I assume it is true that in sqlite by default all commands run in a single connection are handled transactionally? Is this true? i.e. I should not have to (and haven't got in the code at the moment) a BeginTransaction, or CommitTransaction.
It's using http://sqlite.phxsoftware.com/ as the sqlite ADO.net database provider.
1st TRY
private int ExecuteNonQueryTransactionally(List<string> sqlList)
{
int totalRowsUpdated = 0;
using (var conn = new SQLiteConnection(_connectionString))
{
// Open connection (one connection so should be transactional - confirm)
conn.Open();
// Apply each SQL statement passed in to sqlList
foreach (string s in sqlList)
{
using (var cmd = new SQLiteCommand(conn))
{
cmd.CommandText = s;
totalRowsUpdated = totalRowsUpdated + cmd.ExecuteNonQuery();
}
}
}
return totalRowsUpdated;
}
3rd TRY
How is this?
private int ExecuteNonQueryTransactionally(List<string> sqlList)
{
int totalRowsUpdated = 0;
using (var conn = new SQLiteConnection(_connectionString))
{
conn.Open();
using (var trans = conn.BeginTransaction())
{
try
{
// Apply each SQL statement passed in to sqlList
foreach (string s in sqlList)
{
using (var cmd = new SQLiteCommand(conn))
{
cmd.CommandText = s;
totalRowsUpdated = totalRowsUpdated + cmd.ExecuteNonQuery();
}
}
trans.Commit();
}
catch (SQLiteException ex)
{
trans.Rollback();
throw;
}
}
}
return totalRowsUpdated;
}
thanks
Yes, it's true, each SQLite unnested command is nested in a transaction. So that if you need to run several queries, without fetching the result, there is much gain is explicitly starting a transaction, doing your queries, and committing.