Как разблокировать таблицу SQL Server 2008 в ADO.NET/C# 4.0 с помощью SqlClient?

Я создал метод, который:

  1. Заблокировал стол
  2. Прочитать из него значение
  3. Записал обновленное значение обратно
  4. Разблокирована таблица

Код работал для Oracle. Теперь я не могу заставить его работать для SQL Server 2008. Ниже приведен метод, и выполнение моей команды разблокировки приводит к SqlException с текстом:

"NOLOC" не является распознаваемым параметром табличных подсказок. Если он предназначен для использования в качестве параметра функции, возвращающей табличное значение, или функции CHANGETABLE, убедитесь, что режим совместимости вашей базы данных установлен на 90.

Код:

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)
        {
          ...
        }
}

Так что же здесь не так? Откуда эта ошибка? Сервер или клиент? Я скопировал оператор из кода C, и он должен там работать. К сожалению, я не могу отладить и проверить, работает ли он у меня.

Изменить: простая попытка блокировки и разблокировки (без чтения или обновления) приводит к тому же исключению.

Спасибо и BR-Матти


person char m    schedule 28.09.2012    source источник
comment
Во-первых, это NOLOCK. Вы действительно хотите заблокировать каждый другой потенциальный процесс из всей таблицы? Обычный способ сделать что-то подобное — это транзакция, и он будет работать в Oracle и SQL Server. Если вы хотите пойти по этому пути, вам не понадобится Oracle или SQL Server Specific Sql, а с помощью IDbConnection, IDbCommand и т. д. вы можете избавиться от большей части материала if Oracle и приблизиться к тому, чтобы быть независимым от поставщика.   -  person Tony Hopkinson    schedule 29.09.2012
comment
Спасибо за ответ, Тони. Да, каждый клиент, пытающийся получить доступ к таблице, должен быть заблокирован. Я хотел бы получить эту работу первым, но я также открыт для более сложных подходов. Если вы не знаете, почему я получаю это исключение, вы можете опубликовать пример, как это сделать с транзакцией. На самом деле я уже использую транзакцию здесь, но, по-видимому, подход, который вы предложили, - это что-то еще.   -  person char m    schedule 29.09.2012
comment
Это будет одно медленное приложение, если вы всегда блокируете всю таблицу.   -  person a_horse_with_no_name    schedule 29.09.2012
comment
@horse: вместо того, чтобы отмахиваться, можешь предложить какие-нибудь конкретные альтернативы? другая таблица имеет только 1 строку, а другая может быть 30. обе имеют 3 столбца. кроме того, идентификаторы извлекаются очень редко. никак не влияет на общую производительность.   -  person char m    schedule 29.09.2012
comment
Одна запись? Расскажите нам больше, лучшим решением может быть вообще не использовать таблицу. Синглтон для управления доступом к ресурсу, похоже, имеет потенциал.   -  person Tony Hopkinson    schedule 29.09.2012
comment
@tony: это таблица, которую используют десятки различных приложений, написанных на разных языках (рабочие столы, пакетные программы, службы Windows, веб-службы). могут быть сотни экземпляров, которым нужны идентификаторы, работающие на множестве систем. конечно, теперь может быть, например. WS, который дает идентификаторы всем различным приложениям, но это делается так, как сегодня.   -  person char m    schedule 30.09.2012
comment
Тогда, даже если вы придумали новый механизм, фактическое внедрение его во всех этих разрозненных системах будет слишком большим шагом, ну, возможно, несколькими шагами. Похоже, это одна из тех устаревших функций, с которыми вы либо живете, либо начинаете разрабатывать. Получить поддержку последнего, когда несколько строк умного кода исправят это, было бы почти невозможно.   -  person Tony Hopkinson    schedule 01.10.2012
comment
это правда. именно поэтому я задаю этот вопрос. причина, по которой меня интересуют другие механизмы, заключается в том, что приятно слышать идеи от других разработчиков.   -  person char m    schedule 01.10.2012


Ответы (2)


Подсказка TABLOCKX блокирует таблицу, как вы намеревались, но вы не можете разблокировать ее вручную. Как долго блокировка остается, зависит от уровня вашей транзакции. Если у вас нет активной транзакции в вашем соединении, блокировка удерживается, пока выполняется SELECT, и после этого отбрасывается.

Если вы хотите реализовать последовательность «заблокировать таблицу -> сделать что-то с таблицей -> снять блокировку», вам потребуется реализовать эквивалент ADO.NET этого сценария T-SQL:

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

вы можете либо выполнить "BEGIN TRAN"/"COMMIT" через объекты DbCommand, либо использовать класс System.Data.SqlClient.SqlTransaction, чтобы начать транзакцию и зафиксировать ее.

Внимание: этот подход работает только в том случае, если ваше соединение еще не зачислено в транзакцию! SQL Server не поддерживает вложенные транзакции, поэтому COMMIT ничего не сделает, и блокировка будет удержана. Если у вас уже есть транзакция, вы не можете снять блокировку, пока транзакция не завершится. В этом случае может помочь синхронизация через sp_getapplock/sp_releaseapplock.

Изменить: если вы хотите узнать о транзакциях, блокировках и блокировках, я рекомендую эти два видео: http://technet.microsoft.com/en-us/sqlserver/gg545007.aspx и http://technet.microsoft.com/en-us/sqlserver/gg508892.aspx

person TToni    schedule 29.09.2012
comment
Это подло, не могу сказать, что я бы подумал об этом, но я бы попытался в любом случае спроектировать необходимость в эксклюзивной блокировке. - person Tony Hopkinson; 29.09.2012
comment
@TToni: Спасибо за ответ! Я попытаюсь завтра на работе просто удалить часть разблокировки и зафиксировать/откатить DbTransaction. Хорошо знать это ограничение, так как оно одно. Спасибо, что добавили его сюда, так что мне не нужно задаваться вопросом, что пошло не так. Я принимаю это, когда я могу попробовать. Спасибо еще раз! - person char m; 29.09.2012
comment
@Tony: я думаю, ты абсолютно прав. На самом деле я имитирую здесь устаревший C-код, что всегда плохая идея. Я хотел бы услышать, как бы у сделать это. Мои знания о SQL Server и встроенной блокировке в БД вообще нулевые. Под внутренней блокировкой я подразумеваю то, что происходит, когда я открываю курсор/начинаю транзакцию и закрываю/завершаю их (если какая-либо блокировка вообще происходит в тех или иных случаях). - person char m; 29.09.2012
comment
Я опубликую альтернативу, дайте мне время - person Tony Hopkinson; 30.09.2012
comment
Я ограничился проблемой непосредственной блокировки, но, конечно, в целом вы должны стараться избегать чрезмерной блокировки. Эксклюзивная блокировка таблицы очень редко действительно необходима. Я мог бы подробнее остановиться на двух битах информации: какое плохое поведение вы хотите предотвратить с помощью блокировки и, поскольку у вас, похоже, есть внешняя транзакция, на каком уровне сериализации она выполняется. О, а ALLOW_SNAPSHOT_ISOLATION или READ_COMMITTED_SNAPSHOT в базе данных включены? Все это влияет на поведение блокировки SQL Server в транзакции (см. видео в ответе). - person TToni; 30.09.2012
comment
спасибо Ттони! это ограничение было именно тем, что я хотел. мы пытаемся предотвратить получение дубликатов идентификаторов разными клиентами (это могут быть десятки разных программ). одновременно могут работать сотни клиентов. но они получают идентификаторы довольно редко, поэтому не проблема, что он время от времени блокируется на мгновение. на данный момент этот фрагмент кода не используется многими программами, поэтому IF есть внешняя транзакция, я могу избавиться от нее, получив идентификаторы 1st. - person char m; 30.09.2012
comment
продолжение: Я ничего не знаю о сер. уровень или настройки. Мне жаль, что я не могу сейчас использовать достаточно времени для этого. Думаю решение должно быть достаточно универсальным и не зависящим от настроек. - person char m; 30.09.2012
comment
Правильный способ создания последовательных значений идентификаторов в SQL Server без проблем с блокировкой — это использование столбцов идентификаторов. Не знаю, сработает ли это для вас, но в таком случае посмотрите здесь: sqlteam.com /article/understanding-identity-columns - person TToni; 01.10.2012
comment
на самом деле то, что я сейчас делаю, это не вложенные транзакции. поэтому внешняя транзакция с соединением никогда не выполняется. что я делаю, так это то, что как получение идентификаторов, так и вставка новых элементов в таблицы с этими новыми идентификаторами находятся в одной транзакции. таким образом, если что-то пойдет не так, я не потеряю идентификаторы и не попытаюсь вставить их с временными/поддельными идентификаторами. - person char m; 01.10.2012
comment
@TToni: Еще раз спасибо, я принял это, прежде чем пытаться, потому что я действительно занят на работе. Этот KEEPLOCK привел к исключению и вообще не был нужен. Вот почему я разместил свой ответ здесь. - person char m; 02.10.2012

Вот ответ для одной таблицы для SqlClient с кодом, который я сделал на основе ответа TToni:

    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;
    }

и это я так:

        ...

        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;
        }

        ...

Для клиента oracle желательно иметь:

SELECT MaxId from IdMax WHERE ... FOR UPDATE OF MaxId

-m

person char m    schedule 02.10.2012