I have a very strange problem that only occurs when the code in question is in a high load situations. My ASP.NET web client C# code calls a T-SQL stored procedure that has an OUTPUT parameter.
Under high loads, the data being returned sometimes does not make it back to the calling C# code. I have extracted all the relevant code into the example below;
Stored procedure:
CREATE PROCEDURE GetLoginName
#LoginId BIGINT,
#LoginName VARCHAR(50) OUTPUT
AS
SET NOCOUNT ON
SELECT #LoginName = LoginName
FROM Logins
WHERE Id = #LoginId
SET NOCOUNT OFF
GO
Database base class:
public class DatabaseContextBase : IDisposable
{
private SqlConnection _connection;
private string _connectionString;
private SqlInt32 _returnValue;
private int _commandTimeout;
private static int _maxDatabaseExecuteAttempts = 3;
private static int s_commandTimeout = 30;
protected DBContextBase()
{
// try and get the connection string off the identity first...
string connectionString = GetConnectionStringFromIdentity();
if(connectionString != null)
{
ConstructionHelper(connectionString, s_commandTimeout);
}
else
{
// use the initialised static connection string, and call the other overload
// of the constructor
ConstructionHelper(s_connectionString, s_commandTimeout);
}
}
private void ConstructionHelper( string connectionString, int commandTimeout )
{
// store the connection string in a member var.
_connectionString = connectionString;
// store the timeout in a member var.
_commandTimeout = commandTimeout;
}
public static string GetConnectionStringFromIdentity()
{
IIdentity identity = Thread.CurrentPrincipal.Identity as wxyz.Security.wxyzIdentityBase;
string connectionString = null;
if(identity != null)
{
connectionString = ((wxyz.Security.wxyzIdentityBase) identity ).ConnectionString;
}
return connectionString;
}
public void Dispose()
{
if (_connection.State != ConnectionState.Closed)
{
_connection.Close();
}
_connection.Dispose();
_connection = null;
}
protected void ExecuteNonQuery(SqlCommand command)
{
SqlConnection con = this.Connection;
lock (con)
{
if (con.State != ConnectionState.Open)
{
con.Open();
}
// don't need a try catch as this is only ever called from another method in this
// class which will wrap it.
command.Connection = con;
command.Transaction = _transaction;
command.CommandTimeout = _commandTimeout;
for (int currentAttempt = 1; currentAttempt == _maxDatabaseExecuteAttempts; currentAttempt++)
{
try
{
// do it
command.ExecuteNonQuery();
// done, exit loop
break;
}
catch (SqlException sex)
{
HandleDatabaseExceptions(currentAttempt, sex, command.CommandText);
}
}
}
}
protected void HandleDatabaseExceptions(int currentAttempt, SqlException sqlException, string sqlCommandName)
{
if (DataExceptionUtilities.IsDeadlockError(sqlException))
{
if (!this.IsInTransaction)
{
// Not in a transaction and a deadlock detected.
// If we have not exceeded our maximum number of attemps, then try to execute the SQL query again.
string deadlockMessage = string.Format("Deadlock occured in attempt {0} for '{1}':", currentAttempt, sqlCommandName);
Logging.Write(new ErrorLogEntry(deadlockMessage, sqlException));
if (currentAttempt == DBContextBase.MaxDatabaseExecuteAttempts)
{
// This was the last attempt so throw the exception
throw sqlException;
}
// Wait for a short time before trying again
WaitShortAmountOfTime();
}
else
{
// We're in a transaction, then the calling code needs to handle the deadlock
string message = string.Format("Deadlock occured in transaction for '{0}':", sqlCommandName);
throw new DataDeadlockException(message, sqlException);
}
}
else if (this.IsInTransaction && DataExceptionUtilities.IsTimeoutError(sqlException))
{
// We're in a transaction and the calling code needs to handle the timeout
string message = string.Format("Timeout occured in transaction for '{0}':", sqlCommandName);
// Raise a Deadlock exception and the calling code will rollback the transaction
throw new DataDeadlockException(message, sqlException);
}
else
{
// Something else has gone wrong
throw sqlException;
}
}
/// <summary>
/// get the SqlConnection object owned by this database (already connected to db)
/// </summary>
public SqlConnection Connection
{
get {
// check whether we've got a connection string (from either identity or static initialise)
if ( _connectionString == null )
{
throw new ArgumentNullException( "connectionString", "Connection string not set" );
}
if ( _connection != null )
{
return _connection;
}
else
{
_connection = new SqlConnection( _connectionString );
return _connection;
}
}
}
/// <summary>
/// Return value from executed stored procedure
/// </summary>
public SqlInt32 ReturnValue
{
get { return _returnValue; }
set { _returnValue = value; }
}
}
Database access class:
public class AuthenticationDBCommands
{
public static SqlCommand GetLoginName()
{
System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient.SqlCommand("GetLoginName");
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("#RETURN_VALUE", System.Data.SqlDbType.Int, 0, System.Data.ParameterDirection.ReturnValue, false, 0, 0, "RETURN_VALUE", System.Data.DataRowVersion.Current, SqlInt32.Null));
cmd.Parameters.Add(new SqlParameter("#LoginId", SqlDbType.BigInt, 8, ParameterDirection.Input, false, 0, 0, "LoginId", DataRowVersion.Current, SqlInt64.Null));
cmd.Parameters.Add(new SqlParameter("#LoginName", SqlDbType.VarChar, 50, ParameterDirection.InputOutput,false, 0, 0, "LoginName", DataRowVersion.Current, SqlString.Null));
return cmd;
}
}
public class AuthenticationDBContext : DatabaseContextBase
{
public AuthenticationDBContext() : base()
{
}
public void GetLoginName(SqlInt64 LoginId, ref SqlString LoginName)
{
SqlCommand cmd = AuthenticationDBCommands.GetLoginName();
cmd.Parameters[1].Value = LoginId;
cmd.Parameters[2].Value = LoginName;
base.ExecuteNonQuery(cmd);
base.ReturnValue = (SqlInt32) cmd.Parameters[0].Value;
LoginName = (SqlString)(cmd.Parameters[2].Value);
}
}
So when it's used inside the ASP.NET web client it look like this:
protected string GetLoginName(long loginId)
{
SqlString loginName = SqlString.Null;
using (AuthenticationDBContext dbc = new AuthenticationDBContext())
{
dbc.GetLoginName(loginId, ref loginName);
}
return loginName.Value;
}
As you can see this is fairly standard stuff. But when the AuthenticationDBContext.GetLoginName() method is called by many different users in quick succession the loginName object is sometimes null.
When the SqlCommand.ExecuteNonQuery() fails to return any data it does not throw an exception.
I have tested the SQL and it always finds a value (I've inserted #LoginName into a table and it's never null). So the problem is happening after or in SqlCommand.ExecuteNonQuery();
As I said, this is an example of what it happening. In reality, data is not being returned for lots of different stored procedures.
It's also worth stating that other web clients that share the app pool in IIS are not affected when the web client in question is under a heavy load.
I'm using .NET 4.5 and my database is on SQL Server 2008.
Has anyone seen anything like this before?
Can anyone recommend any changes?
Thanks in advance,
Matt
UPDATE. Thanks for all your comments. I have made the following change to the DatabaseContextBase class.
private void ExecuteNonQueryImpl(SqlCommand command)
{
object _lockObject = new object();
lock (_lockObject)
{
SqlConnection con = this.GetConnection();
if (con.State != ConnectionState.Open)
{
con.Open();
}
// don't need a try catch as this is only ever called from another method in this
// class which will wrap it.
command.Connection = con;
command.Transaction = _transaction;
command.CommandTimeout = _commandTimeout;
for (int currentAttempt = 1; currentAttempt <= _maxDatabaseExecuteAttempts; currentAttempt++)
{
try
{
// do it
command.ExecuteNonQuery();
// done, exit loop
break;
}
catch (SqlException sex)
{
HandleDatabaseExceptions(currentAttempt, sex, command.CommandText);
}
}
if (!this.IsInTransaction)
{
con.Close();
}
}
}
public SqlConnection GetConnection()
{
if (this.IsInTransaction)
{
return this.Connection;
}
else
{
// check whether we've got a connection string (from either identity or static initialise)
if ( _connectionString == null )
{
string exceptionMessage = Language.Translate("DbContextNotInitialized");
throw new ArgumentNullException( "connectionString", exceptionMessage );
}
return new SqlConnection(_connectionString);
}
}
However, in a load test the data still sometimes comes back as null. The web client is not working in a transaction so a new SqlConnection object is created, opened and closed every time a call is made. (there are other areas of code which share the DatabaseContextBase class that do work in a transaction so the original connection property is needed)
I would like to mention that again that I'm confident that the store procedure is working correctly as I have inserted the #LoginName value into a table and it's never null.
Thanks,
Matt
Your "for loop" definition is not correct.
for (int currentAttempt = 1; currentAttempt == _maxDatabaseExecuteAttempts; currentAttempt++)
This will initialize currentAttempt to 1, run the loop, increment currentAttempt, and then check to see if currentAttempt is equal to 3, which it isn't, and exit the loop. I think what you want is
for (int currentAttempt = 1; currentAttempt <= _maxDatabaseExecuteAttempts; currentAttempt++)
Related
How do I store the results from a mysql query for use in other classes most efficiently?
I've tried the following code, which executes properly and stores all data in reader as it should. Reading the DataReader here works fine if I want to!
public class DatabaseHandler
{
public void MySqlGetUserByName(string input_username, MySqlDataReader reader)
{
try
{
_database.Open();
string query = "SELECT * FROM users WHERE username = '#input'";
MySqlParameter param = new MySqlParameter(); param.ParameterName = "#input"; param.Value = input_username;
MySqlCommand command = new MySqlCommand(query, _database);
command.Parameters.Add(param);
reader = command.ExecuteReader();
_database.Close();
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
}
But when I try to read the same DataReader here, it is null and throws an exception (right after Debug6).
public class LoginHandler
{
public static void UserAuth(Client user, string input_username, string input_password)
{
DatabaseHandler dataBase = new DatabaseHandler();
MySqlDataReader dataReader = null;
dataBase.MySqlGetUserByName(input_username, dataReader);
Console.WriteLine("Debug6");
if (!dataReader.HasRows)
{
user.SendChatMessage("No match found.");
return;
}
while (dataReader.Read())
{
user.SetData("ID", (int)dataReader[0]);
user.SetData("username", (string)dataReader[1]);
user.SetData("email", (string)dataReader[2]);
user.SetData("password", (string)dataReader[3]);
}
dataReader.Close();
}
}
Please let me know how to make this work, or if there is a more efficient way of doing this without limiting the function of MySqlGetUserByName. The purpose of it is to input a name and a place to store all info from the match in the database.
Also, feel free to drop in any other suggestions that could make the code more efficient.
You could change your MySqlGetUserByName to return a User instance if all goes well, otherwise you return a null instance to the caller (Or you can thrown an exception, or you can set a global error flag in the DatabaseHandler class..., but to keep things simple I choose to return a null)
public class DatabaseHandler
{
public User MySqlGetUserByName(string input_username)
{
User result = null;
try
{
string query = "SELECT * FROM users WHERE username = #input";
using(MySqlConnection cnn = new MySqlConnection(......))
using(MySqlCommand command = new MySqlCommand(query, cnn))
{
cnn.Open();
command.Parameters.AddWithValue("#input", input_username);
using(MySqlDataReader dataReader = command.ExecuteReader())
{
if (dataReader.Read())
{
result = new User();
result.ID = Convert.ToInt32(dataReader[0]);
..... and so on with the other user properties ....
}
}
}
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
// Return the user to the caller. If we have not found the user we return null
return result;
}
}
In the same way the caller handles the situation
public class LoginHandler
{
public static void UserAuth(string input_username, string input_password)
{
DatabaseHandler dataBase = new DatabaseHandler();
User result = dataBase.MySqlGetUserByName(input_username);
// If we have not found the user we have a null in the variable
if(result == null)
{
// Send your message using a static method in the user class
// User.SendMessage("User with username {input_username} not found!");
}
else
{
// User ok. return it? or do something with its data?
}
}
}
So this is a little bit code-ceptionlike.
I have a function that is checking the last ID in a table, this function is called within another function. At the end of that function, I have another function that's opening another datareader.
Error:
There is already an open Datareader associated with this connection which must be closed first.
getLastIdfromDB()
public string getLastIdFromDB()
{
int lastIndex;
string lastID ="";
var dbCon = DB_connect.Instance();
if (dbCon.IsConnect())
{
MySqlCommand cmd2 = new MySqlCommand("SELECT ID FROM `competitor`", dbCon.Connection);
try
{
MySqlDataReader reader = cmd2.ExecuteReader();
while (reader.Read())
{
string item = reader2["ID"].ToString();
lastIndex = int.Parse(item);
lastIndex++;
lastID = lastIndex.ToString();
}
}
catch (Exception ex)
{
MessageBox.Show("Error:" + ex.Message);
}
}
return lastID;
}
This function is later-on used in this function:
private void addPlayerBtn_Click(object sender, EventArgs e)
{
ListViewItem lvi = new ListViewItem(getLastIdFromDB());
.........................................^
... HERE
...
... irrelevant code removed
.........................................
var dbCon = DB_connect.Instance();
if (dbCon.IsConnect())
{
MySqlCommand cmd = new MySqlCommand("INSERT INTO `competitor`(`ID`, `Name`, `Age`) VALUES(#idSql,#NameSql,#AgeSql)", dbCon.Connection);
cmd.Parameters.AddWithValue("#idSql", getLastIdFromDB());
cmd.Parameters.AddWithValue("#NameSql", playerName.Text);
cmd.Parameters.AddWithValue("#AgeSql", playerAge.Text);
try
{
cmd.ExecuteNonQuery();
listView1.Items.Clear();
}
catch (Exception ex)
{
MessageBox.Show("Error:" + ex.Message);
dbCon.Connection.Close();
}
finally
{
updateListView();
}
}
}
What would be the best way for me to solve this problem and in the future be sure to close my connections properly?
UPDATE: (per request, included DB_connect)
class DB_connect
{
private DB_connect()
{
}
private string databaseName = "simhopp";
public string DatabaseName
{
get { return databaseName; }
set { databaseName = value; }
}
public string Password { get; set; }
private MySqlConnection connection = null;
public MySqlConnection Connection
{
get { return connection; }
}
private static DB_connect _instance = null;
public static DB_connect Instance()
{
if (_instance == null)
_instance = new DB_connect();
return _instance;
}
public bool IsConnect()
{
bool result = true;
try
{
if (Connection == null)
{
if (String.IsNullOrEmpty(databaseName))
result = false;
string connstring = string.Format("Server=localhost; database={0}; UID=root;", databaseName);
connection = new MySqlConnection(connstring);
connection.Open();
result = true;
}
}
catch (Exception ex)
{
Console.Write("Error: " + ex.Message);
}
return result;
}
public void Close()
{
connection.Close();
}
}
}
You are trying to have multiple open readers on the same connection. This is commonly called "MARS" (multiple active result sets). MySql seems to have no support for it.
You will have to either limit yourself to one open reader at a time, or use more than one connection, so you can have one connection for each reader.
My suggestion would be to throw away that singleton-like thingy and instead use connection pooling and proper using blocks.
As suggested by Pikoh in the comments, using the using clause indeed solved it for me.
Working code-snippet:
getLastIdFromDB
using (MySqlDataReader reader2 = cmd2.ExecuteReader()) {
while (reader2.Read())
{
string item = reader2["ID"].ToString();
lastIndex = int.Parse(item);
lastIndex++;
lastID = lastIndex.ToString();
}
}
Your connection handling here is not good. You need to ditch the DB_connect. No need to maintain a single connection - just open and close the connection each time you need it. Under the covers, ADO.NET will "pool" the connection for you, so that you don't actually have to wait to reconnect.
For any object that implements IDisposable you need to either call .Dispose() on it in a finally block, or wrap it in a using statement. That ensures your resources are properly disposed of. I recommend the using statement, because it helps keep the scope clear.
Your naming conventions should conform to C# standards. Methods that return a boolean should be like IsConnected, not IsConnect. addPlayerBtn_Click should be AddPlayerButton_Click. getLastIdFromDB should be GetlastIdFromDb or getLastIdFromDatabase.
public string GetLastIdFromDatabase()
{
int lastIndex;
string lastID ="";
using (var connection = new MySqlConnection(Configuration.ConnectionString))
using (var command = new MySqlCommand("query", connection))
{
connection.Open();
MySqlDataReader reader = cmd2.ExecuteReader();
while (reader.Read())
{
string item = reader2["ID"].ToString();
lastIndex = int.Parse(item);
lastIndex++;
lastID = lastIndex.ToString();
}
}
return lastID;
}
Note, your query is bad too. I suspect you're using a string data type instead of a number, even though your ID's are number based. You should switch your column to a number data type, then select the max() number. Or use an autoincrementing column or sequence to get the next ID. Reading every single row to determine the next ID and incrementing a counter not good.
I recently created a light weight ORM tool in C# and posted it on Github. One of the things my ORM does is manage the opening and closing of connections for you along with opening and disposing of a sql transaction. The way I do this is I open the connection and begin the sql transaction in the constructor of my context class and close the connection and dispose the transaction on the dispose method of my context class. The code below are some methods from my tool:
public class ADOCRUDContext : IDisposable
{
internal IDbConnection sqlConnection;
internal IDbTransaction sqlTransaction;
internal string suffix = "_";
public ADOCRUDContext(string connectionString)
{
sqlConnection = new SqlConnection(connectionString);
sqlConnection.Open();
sqlTransaction = sqlConnection.BeginTransaction();
}
public void Commit()
{
sqlTransaction.Commit();
}
/// <summary>
/// Disposes transaction and closes sql connection
/// </summary>
public void Dispose()
{
sqlTransaction.Dispose();
sqlConnection.Close();
}
public void Insert<T>(T item)
{
string tableName = ObjectTypeHelper.GetTableName<T>(item);
// Properties of object that are not primary keys
PropertyInfo[] modelProperties = ObjectTypeHelper.GetModelProperties<T>(item, false);
// Properties of object that are also primary keys
PropertyInfo[] primaryKeyProperties = ObjectTypeHelper.GetPrimaryKeyProperties<T>(item);
if (modelProperties == null || modelProperties.Count() == 0)
{
// Moves composite keys to model properties if they are the only properties of the table/object
if (primaryKeyProperties.Count() > 0)
modelProperties = primaryKeyProperties;
else
throw new Exception("Class has no properties or are missing Member attribute");
}
// Generates insert statement
StringBuilder query = new StringBuilder();
query.Append("insert into " + tableName + "(" + String.Join(",", modelProperties.Select(x => x.Name)) + ") ");
query.Append("values (" + String.Join(", ", modelProperties.Select(x => "#" + x.Name+suffix)) + ") ");
query.Append("select #primaryKey = ##identity");
try
{
// Generates and executes sql command
SqlCommand cmd = GenerateSqlCommand<T>(item, query.ToString(), modelProperties, null, ADOCRUDEnums.Action.Insert, suffix);
cmd.ExecuteNonQuery();
// Loads db generated identity value into property with primary key attribute
// if object only has 1 primary key
// Multiple primary keys = crosswalk table which means property already contains the values
if (primaryKeyProperties != null && primaryKeyProperties.Count() == 1)
primaryKeyProperties[0].SetValue(item, cmd.Parameters["primaryKey"].Value as object);
}
catch (Exception ex)
{
sqlTransaction.Rollback();
throw new Exception(ex.Message);
}
}
Below is an example of how you use my tool:
public void AddProduct(Product p)
{
using (ADOCRUDContext context = new ADOCRUDContext(connectionString))
{
context.Insert<Product>(p);
context.Commit();
}
}
public void UpdateProduct(int productId)
{
using (ADOCRUDContext context = new ADOCRUDContext(connectionString))
{
Product p = this.GetProductById(productId);
p.Name = "Basketball";
context.Update<Product>(p);
context.Commit();
}
}
Currently if I do something like:
public void DoSomething(Product p)
{
using (ADOCRUDContext context = new ADOCRUDContext(connectionString))
{
this.AddProduct(p);
p.Name = "Test";
throw new Exception("Blah");
}
}
Even though I threw an exception on DoSomething, the product still got added to the database since the inner transaction was committed. Is there a way to get the inner transaction (AddProduct) to fail if the outer transaction fails?
I am using MySql 5.6x with Visual Studio 2015, windows 10, 64-bit. C# as programming language. In my CRUD.cs (Class file) i have created the following method:
public bool dbQuery(string sql,string[] paramList= null)
{
bool flag = false;
try
{
connect();
cmd = new MySqlCommand(sql,con);
cmd.Prepare();
if(paramList != null){
foreach(string i in paramList){
string[] valus = i.Split(',');
string p = valus[0];
string v = valus[1];
cmd.Parameters[p].Value = v;
}
}
if (cmd.ExecuteNonQuery() > 0)
{
flag = true;
}
}
catch (Exception exc)
{
error(exc);
}
}
I am passing the query and Parameters List like this:
protected void loginBtn_Click(object sender, EventArgs e)
{
string sql = "SELECT * FROM dept_login WHERE (user_email = ?user_email OR user_cell = ?user_cell) AND userkey = ?userkey";
string[] param = new string[] {
"?user_email,"+ userid.Text.ToString(),
"?user_cell,"+ userid.Text.ToString(),
"?userkey,"+ userkey.Text.ToString()
};
if (db.dbQuery(sql, param))
{
msg.Text = "Ok";
}
else
{
msg.Text = "<strong class='text-danger'>Authentication Failed</strong>";
}
}
Now the problem is that after the loop iteration complete, it directly jumps to the catch() Block and generate an Exception that:
Parameter '?user_email' not found in the collection.
Am i doing this correct to send params like that? is there any other way to do the same?
Thanks
EDIT: I think the best way might be the two-dimensional array to collect the parameters and their values and loop then within the method to fetch the parameters in cmd.AddWidthValues()? I may be wrong...
In your dbQuery you don't create the parameters collection with the expected names, so you get the error when you try to set a value for a parameter that doesn't exist
public bool dbQuery(string sql,string[] paramList= null)
{
bool flag = false;
try
{
connect();
cmd = new MySqlCommand(sql,con);
cmd.Prepare();
if(paramList != null){
foreach(string i in paramList){
string[] valus = i.Split(',');
string p = valus[0];
string v = valus[1];
cmd.Parameters.AddWithValue(p, v);
}
}
if (cmd.ExecuteNonQuery() > 0)
flag = true;
}
catch (Exception exc)
{
error(exc);
}
}
Of course this will add every parameter with a datatype equals to a string and thus is very prone to errors if your datatable columns are not of string type
A better approach would be this one
List<MySqlParameter> parameters = new List<MySqlParameter>()
{
{new MySqlParameter()
{
ParameterName = "?user_mail",
MySqlDbType= MySqlDbType.VarChar,
Value = userid.Text
},
{new MySqlParameter()
{
ParameterName = "?user_cell",
MySqlDbType= MySqlDbType.VarChar,
Value = userid.Text
},
{new MySqlParameter()
{
ParameterName = "?userkey",
MySqlDbType = MySqlDbType.VarChar,
Value = userkey.Text
},
}
if (db.dbQuery(sql, parameters))
....
and in dbQuery receive the list adding it to the parameters collection
public bool dbQuery(string sql, List<MySqlParameter> paramList= null)
{
bool flag = false;
try
{
connect();
cmd = new MySqlCommand(sql,con);
cmd.Prepare();
if(paramList != null)
cmd.Parameters.AddRange(paramList.ToArray());
if (cmd.ExecuteNonQuery() > 0)
{
flag = true;
}
}
catch (Exception exc)
{
error(exc);
}
}
By the way, unrelated to your actual problem, but your code doesn't seem to close and dispose the connection. This will lead to very nasty problems to diagnose and fix. Try to use the using statement and avoid a global connection variable
EDIT
As you have noticed the ExecuteNonQuery doesn't work with a SELECT statement, you need to use ExecuteReader and check if you get some return value
using(MySqlDataReader reader = cmd.ExecuteReader())
{
flag = reader.HasRows;
}
This, of course, means that you will get troubles when you want to insert, update or delete record where instead you need the ExecuteNonQuery. Creating a general purpose function to handle different kind of query is very difficult and doesn't worth the work and debug required. Better use some kind of well know ORM software like EntityFramework or Dapper.
Your SQL Commands' Parameters collection does not contain those parameters, so you cannot index them in this manner:
cmd.Parameters[p].Value = v;
You need to add them to the Commands' Parameters collection in this manner: cmd.Parameters.AddWithValue(p, v);.
I created a method while back that:
Locked a table
Read value from it
Wrote updated value back
Unlocked the table
The code worked for Oracle. Now I can't get it work for SQL Server 2008. The method is below and executing my unlocking command results in a SqlException with text:
"NOLOC" is not a recognized table hints option. If it is intended as a
parameter to a table-valued function or to the CHANGETABLE function,
ensure that your database compatibility mode is set to 90.
Code:
public static int GetAndSetMaxIdTable(DbProviderFactory factory, DbConnection cnctn, DbTransaction txn, int tableId, string userName, int numberOfIds)
{
bool isLocked = false;
string sql = string.Empty;
string maxIdTableName;
if (tableId == 0)
maxIdTableName = "IdMax";
else
maxIdTableName = "IdMaxTable";
try
{
bool noPrevRow = false;
int realMaxId;
if (factory is OracleClientFactory)
sql = string.Format("lock table {0} in exclusive mode", maxIdTableName);
else if (factory is SqlClientFactory)
sql = string.Format("select * from {0} with (TABLOCKX)", maxIdTableName);
else
throw new Exception(string.Format("Unsupported DbProviderFactory -type: {0}", factory.GetType().ToString()));
using (DbCommand lockCmd = cnctn.CreateCommand())
{
lockCmd.CommandText = sql;
lockCmd.Transaction = txn;
lockCmd.ExecuteNonQuery();
isLocked = true;
}
using (DbCommand getCmd = cnctn.CreateCommand())
{
getCmd.CommandText = CreateSelectCommand(factory, tableId, userName, getCmd, txn);
object o = getCmd.ExecuteScalar();
if (o == null)
{
noPrevRow = true;
realMaxId = 0;
}
else
{
realMaxId = Convert.ToInt32(o);
}
}
using (DbCommand setCmd = cnctn.CreateCommand())
{
if (noPrevRow)
setCmd.CommandText = CreateInsertCommand(factory, tableId, userName, numberOfIds, realMaxId, setCmd, txn);
else
setCmd.CommandText = CreateUpdateCommand(factory, tableId, userName, numberOfIds, realMaxId, setCmd, txn);
setCmd.ExecuteNonQuery();
}
if (factory is OracleClientFactory)
sql = string.Format("lock table {0} in share mode", maxIdTableName);
else if (factory is SqlClientFactory)
sql = string.Format("select * from {0} with (NOLOC)", maxIdTableName);
using (DbCommand lockCmd = cnctn.CreateCommand())
{
lockCmd.CommandText = sql;
lockCmd.Transaction = txn;
lockCmd.ExecuteNonQuery();
isLocked = false;
}
return realMaxId;
}
catch (Exception e)
{
...
}
}
So what goes wrong here? Where does this error come from? Server or client? I copied the statement from C code and it's supposed to work there. Unfortunately I can't debug and check if it works for me.
Edit: Just trying to lock and unlock (without reading or updating) results in same exception.
Thanks & BR -Matti
The TABLOCKX hint locks the table as you intend, but you can't unlock it manually. How long the lock stays on depends on your transaction level. If you don't have an active transaction on your connection, the lock is held while the SELECT executes and is discarded thereafter.
If you want to realize the sequence "lock the table -> do something with the table -> release the lock" you would need to implement the ADO.NET equivalent of this T-SQL script:
BEGIN TRAN
SELECT TOP (1) 1 FROM myTable (TABLOCKX, KEEPLOCK)
-- do something with the table
COMMIT -- This will release the lock, if there is no outer transaction present
you can either execute the "BEGIN TRAN"/"COMMIT" through DbCommand objects or you can use the System.Data.SqlClient.SqlTransaction class to start a transaction and commit it.
Attention: This approach only works if your connection is not enlisted in a transaction already! SQL Server doesn't support nested transaction, so the COMMIT wouldn't do anything and the lock would be held. If you have a transaction already running, you cannot release the lock until the transaction finishes. In this case maybe a synchronisation through sp_getapplock/sp_releaseapplock might help.
Edit: If you want to educate yourself about transactions, locking and blocking, I recommend these two videos: http://technet.microsoft.com/en-us/sqlserver/gg545007.aspx and http://technet.microsoft.com/en-us/sqlserver/gg508892.aspx
Here is answer for one table for SqlClient with code I made based on TToni's answer:
public static int GetAndSetMaxIdTable(DbProviderFactory factory, DbConnection cnctn, DbTransaction txn, int numberOfIds)
{
bool noPrevRow = false;
int realMaxId;
using (DbCommand getCmd = cnctn.CreateCommand())
{
getCmd.CommandText = "SELECT MaxId FROM IdMax WITH (TABLOCKX)"
getCmd.Transaction = txn;
object o = getCmd.ExecuteScalar();
if (o == null)
{
noPrevRow = true;
realMaxId = 0;
}
else
{
realMaxId = Convert.ToInt32(o);
}
}
using (DbCommand setCmd = cnctn.CreateCommand())
{
if (noPrevRow)
setCmd.CommandText = CreateInsertCommand(factory, tableId, userName, numberOfIds, realMaxId, setCmd, txn);
else
setCmd.CommandText = CreateUpdateCommand(factory, tableId, userName, numberOfIds, realMaxId, setCmd, txn);
setCmd.ExecuteNonQuery();
}
return realMaxId;
}
and it i's like this:
...
try
{
using (txn = cnctn.BeginTransaction())
{
oldMaxId = GetAndSetMaxIdTable(factory, cnctn, txn, 5);
for (i = 0; i < 5; i++)
{
UseNewIdToInsertStuff(factory, cnctn, txn, oldMaxId + i + 1)
}
txn.Commit();
return true;
}
}
catch (Exception e)
{
// don't know if this is needed
if (txn != null && cnctn.State == ConnectionState.Open)
txn.Rollback();
throw e;
}
...
For oracle client it seems to be desirable to have:
SELECT MaxId from IdMax WHERE ... FOR UPDATE OF MaxId
-m