質問

I created a method while back that:

  1. Locked a table
  2. Read value from it
  3. Wrote updated value back
  4. 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

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top