Вопрос

В процессе администрирования базы данных возникла необходимость определить причину возникновения ошибки. Определенный объём информации импортируется в базу данных, с которым далее пользователи работают. В процессе заполнения определенного набора полей автоматически высчитывалась итоговая сумма в поле «Итого». Но в определённый промежуток времени использования продукта начали появляться ошибки, связанные с несоответствием значения поля «Итого» сумме полей из которых оно вычисляется («Сумма покупки», «Наценка», «Сбор» и т.д.). Так как ошибку не получалось явно повторить, необходимо было разработать механизм для решения данной проблемы.

Естественно самой реальной и первой причиной возникновения такой ошибки приходила идея о сбоях в работе событий полей окна редактирования (то есть значения в полях изменялись, а события данных полей(-я) не срабатывали).

В основу решения было положено создание двух таблиц в базе данных для ведения логов, что происходят с записью набора данных. Первая таблица WindowLog, а вторая TriggerLog.

Первая таблица WindowLog включает в себя поля «Дата создания»(CreatedOn), «Идентификатор записи» (RecordID), «Ответственный» (WindowsUser), «Имя поля породившего событие»(FieldName), «Итого» и поля из которых оно вычисляется («Сумма покупки», «Наценка», «Сбор» и т.д.). Для наполнения таблицы было использованы события невизуального компонента окна dlData: dlDataOnDatasetDataChange, dlDataOnDatasetBeforePost и dlDataOnDatasetAfterPost. В скрипте в событиях была создана функция, которая формировала SQL запрос к таблице WindowLog базы данных с фиксацией информации по указанным полям на момент срабатывания события.

Запрос:

INSERT INTO WindowLog (*набор полей*)
SELECT (*набор полей*) -- Dataset('поле1'), Dataset('поле2'), Dataset('поле2')

Вторая таблица TriggerLog включает в себя поля «Дата создания»(CreatedOn), «Идентификатор записи» (RecordID), «Состояние» (до изменения записи и после), «SystemUser», «Итого» и поля из которых оно вычисляется («Сумма покупки», «Наценка», «Сбор» и т.д.). Для заполнения данной таблицы был создан триггер на инструкцию UPDATE проблемной таблицы с двумя запросами вставки значений в таблицу. В одном запросе вставлялись значения до изменений, а во втором после.

Запрос №1:

INSERT INTO TriggerLog (*набор полей*)       
SELECT (*набор полей*)
FROM deleted

Запрос №2:

INSERT INTO TriggerLog (*набор полей*)       
SELECT (*набор полей*)
FROM inserted

Результатом использования данного решения на основе анализа таблицы WindowLog было установлено, что срабатывают все события окна редактирования, влияющие на вычисление значения поля «Итого». В процессе использования окна редактирования и после сохранения записи значения поля «Итого» были корректны.

Проанализировав записи в таблице TriggerLog было установлено, что в результате выполнения инструкции UPDATE было внесено некорректное значение. Сопоставив даты создания записей в таблице TriggerLog и WindowLog было установлено, что инструкция UPDATE была вызвана не в результате манипуляций с окном редактирования, а иным источником. На основании поля «SystemUser» таблицы TriggerLog было установлено что изменения были внесены с помощью импортера данных.

Таблицу TriggerLog возможно расширить, добавив в нее поля, которые помогут ускорить процесс обнаружение источника изменений записи базы данных. Список дополнительных полей может выгладять следующим образом: ApplicationName, LoginName, HostName.

PS: Принимаю предложения на доработку вашей конфигурации!!! Для более детальной информации можно связаться по следующему e-mail адресу: providnui@ukr.net !!!

В случае возникновения дополнительных вопрос по теме могу поделиться более детальной информацией.

Всем удачи в этом не легком процессе!!!

У меня такой же вопрос

0 комментариев
Войдите или зарегистрируйтесь, чтобы комментировать
Вопрос

Добрый день. Возможно данный вопрос уже был но все же, хотелось бы знать есть ограничение на кол-во добавления колонок(строк, справочников и тд) в карточке продажи или других карточках!
И если есть можно ли его убрать? и как это сделать?
Заранее спасибо!

У меня такой же вопрос

1 комментарий

Добрый день,

Насколько мне известно, ограничений нет. Но не советую перегружать страницу контролами в ущерб удобочитаемости.

Войдите или зарегистрируйтесь, чтобы комментировать
Публикация

Всем привет!
Все мы знаем, что, если при удалении записи на нее есть ссылки из других таблиц, то система выдает список связей для того, чтобы пользователь попробовал разобраться с этим, где нужно удалил эти связи или вообще отказался от затеи удалять ту самую запись. Но, на мой взгляд, в 90% случаев пользователь уверен, что эту запись точно нужно удалить и разбираться со связями он 100% не хочет. Так почему бы не дать ему возможность просто удалить эту запись, предварительно отвязав ее от остальных объектов автоматически?

Для решения этой задачи был взят за основу скрипт отсюда.

1. Создаем хранимую процедуру под sa или под пользователем с правом sysadmin, которая будет делать отвязку записи и ее удаление:

CREATE procedure tsp_UnbindAndDeleteRecord(@RecordID nvarchar(max), @ParentTableName nvarchar(max)) WITH exec AS owner AS
begin
        declare @ColumnName nvarchar(max)
        declare @TableName nvarchar(max)
        declare @TempSQL nvarchar(max)
        declare @RecordValue nvarchar(max)

        declare local_cursor cursor LOCAL FOR
        SELECT DISTINCT
                CONSTRAINT_COLUMN_USAGE.COLUMN_NAME AS COLUMN_NAME,
                CONSTRAINT_COLUMN_USAGE.TABLE_NAME AS TABLE_NAME
        FROM ((INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE AS CONSTRAINT_COLUMN_USAGE
        INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS TABLE_CONSTRAINTS
                ON CONSTRAINT_COLUMN_USAGE.CONSTRAINT_NAME = TABLE_CONSTRAINTS.CONSTRAINT_NAME)
        INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS REFERENTIAL_CONSTRAINTS
                ON CONSTRAINT_COLUMN_USAGE.CONSTRAINT_NAME = REFERENTIAL_CONSTRAINTS.CONSTRAINT_NAME)
        INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS PARENT_TABLES
                ON REFERENTIAL_CONSTRAINTS.UNIQUE_CONSTRAINT_NAME = PARENT_TABLES.CONSTRAINT_NAME
        WHERE TABLE_CONSTRAINTS.CONSTRAINT_TYPE = 'FOREIGN KEY' AND
                PARENT_TABLES.TABLE_NAME = @ParentTableName

        IF (isnull(cast(@RecordID AS nvarchar(max)), '') = '')
                RETURN

        open local_cursor
        fetch next FROM local_cursor INTO @ColumnName, @TableName

        while @@fetch_Status = 0
        begin
                SET @TempSQL = 'UPDATE [' + @TableName + '] SET [' + @ColumnName + '] = null' + ' WHERE ['+ @ColumnName + '] = ''' + @RecordID + ''''
                exec sp_executesql @TempSQL
                fetch next FROM local_cursor INTO @ColumnName, @TableName
        end
        close local_cursor
        deallocate local_cursor

        SET @TempSQL = 'delete from [' + @ParentTableName + '] WHERE [ID] = ''' + @RecordID + ''''
        exec sp_executesql @TempSQL
end

--grant execute on tsp_UnbindAndDeleteRecord to public

Как вы может быть заметили, процедура создана с "with exec as owner". Добавил это я на всякий случай, если вдруг вы не дадите гранты на использование этой процедуры пользователям.

2. Ищем сервис окна wnd_DeleteRecordsWizard, которое появляется, когда выводится список связанных объектов при удалении записей. Добавляем во фрейм с кнопками свою кнопку btnDeleteForce и даем ей "Заголовок" = "Удалить принудительно".

3. На OnClick этой кнопки "вешаем" наш код, в котором запускаем нашу процедуру из пункта 1, передавая в нее в качестве параметров ID удаляемых записей (у нас их ведь может быть сколько угодно) и имя таблицы, из которой производится удаление.

function btnDeleteForceOnClick(Control) {
        var Dataset = dlData.Dataset;
        if (Dataset.IsEmptyPage) {
                return;
        }
        if (ShowConfirmationDialog("Вы уверены?") != wmrYes) {
                return;
        }
        var TableName = Self.Attributes('TableName');
        if (IsEmptyValue(TableName)) {
                return;
        }
        var RecordIDs = Self.Attributes('RecordIDs');
        if (!RecordIDs) {
                return;
        }
        var Count = RecordIDs.length;
        var RecordID;
        try {
                System.BeginProcessing();
                for (var i = 0; i Count; i++) {
                        System.ProcessMessages();      
                        RecordID = RecordIDs[i];
                        var sql = "exec tsp_UnbindAndDeleteRecord '" +
                                RecordID + "', '" + TableName + "'";
                        Connector.DBEngine.ExecuteCustomSQL(sql, System.EmptyValue);
                }
        } finally {
                System.EndProcessing();
        }
        Self.Close();
}

4. Все-таки позволять всем пользователям принудительно удалять запись - дело рискованное. Поэтому предлагаю позволять это делать ответственному человеку, а именно пользователю с правами администратора системы. Для этого в OnPrepare окна wnd_DeleteRecordsWizard добавляем код показа нашей кнопки:

btnDeleteForce.IsVisible = Connector.CurrentUser.IsAdmin;

5. Сохраняем сервисы и проверяем.

Если же вы, все-таки, решите позволять такое удаление любому пользователю, то, как понимаете, пункт 4 делать не нужно.

Примечание: Проверялся этот функционал на MS SQL 2008 (для Oracle он вообще не подходит, как Вы можете судить по синтаксису хранимой процедуры), но, думаю, он будет работать и на MS SQL 2005.

Поделиться

5 комментариев

Спасибо S.Kalishenko. Полезный функционал. Только я так понял доработка для версии террасофт начиная с 3.3.2 так как в версии 3.3.1 еще нет окна wnd_DeleteRecordsWizard. Я думаю как вариант сделать в окне wnd_BaseGridArea в amDelete еще один пунк "Удалить принудительно" и поправить код следующим образом

var Dataset = dlData.Dataset;
        if (Dataset.IsEmptyPage) {
                return;
        }
        if (ShowConfirmationDialog("Вы уверены?") != wmrYes) {
                return;
        }
        var TableName = GetTableFromDataset(Dataset);
        if (IsEmptyValue(TableName)) {
                return;
        }
        var RecordIDs = GetGridSelectedIDsArray();
        if (!RecordIDs) {
                return;
        }
        var Count = RecordIDs.length;
        var RecordID;
        try {
                System.BeginProcessing();
                for (var i = 0; i < Count; i++) {
                        System.ProcessMessages();      
                        RecordID = RecordIDs[i];
                        var sql = "exec tsp_UnbindAndDeleteRecord '" +
                                RecordID + "', '" + TableName.SQLName + "'";
                        Connector.DBEngine.ExecuteCustomSQL(sql, System.EmptyValue);
                }
        } finally {
                System.EndProcessing();
        }

а и еще вопрос а возможно сделать деталь в которой бы отображалось в каких таблицах данная запись имеет связанные записи?

Клёвая штука! Надо использовать обязательно!

"Мещеринов Иван Александрович" написал:Только я так понял доработка для версии террасофт начиная с 3.3.2 так как в версии 3.3.1 еще нет окна wnd_DeleteRecordsWizard.

Да, в 3.3.1 можно реализовать функционал как Вы описали.

"Мещеринов Иван Александрович" написал:а и еще вопрос а возможно сделать деталь в которой бы отображалось в каких таблицах данная запись имеет связанные записи?

Можно, только код я не готов Вам предоставить. Для такой детали можно воспользоваться реализацией получения датасета в wnd_DeleteRecordsWizard из 3.3.2.

Здравствуйте, сообщество.

Сегодня сделал аналогичный функционал для БД Oracle.

Изменяем метод btnDeleteForceOnClick(Control) до следующего вида:

function btnDeleteForceOnClick(Control) {
	ForceDeleteRecord(dlData.Dataset, 
		Self.Attributes('TableName'), Self.Attributes('RecordIDs'));
	Self.Close();
}

Добавляем метод ForceDeleteRecord(Dataset, TableName, RecordIDs) в тот же модуль:

function ForceDeleteRecord(Dataset, TableName, RecordIDs) {
	if (!Dataset || IsEmptyValue(TableName)) {
		return;
	}
	if (Dataset.IsEmptyPage) {
		return;
	}
	if (ShowConfirmationDialog("Вы уверены?") != wmrYes) {
		return; 
	}
	if (!RecordIDs) {
		return;
	}
	var Count = RecordIDs.length;
	var RecordID; 
	try {
		System.BeginProcessing();
		for (var i = 0; i < Count; i++) {
			System.ProcessMessages();       
			RecordID = RecordIDs[i];
			var sql = '';
			switch (Connector.DBExecutor.DBExecutorTypeCode) {
				case 'MSSQL':
					var sql = "exec tsp_UnbindAndDeleteRecord '" + 
						RecordID + "', '" + TableName + "'";
					break;
				case 'Oracle':
					var sql = "begin \"tsp_UnbindAndDeleteRecord\""('"" + 
Войдите или зарегистрируйтесь, чтобы комментировать
Публикация

Как известно, для удаления записей из таблицы, правильней всего создавать
специальный DeleteQuery, но как показала практика - делать это всем лень :)
для таких случаев была написана следующая функция.

function DeleteRecords(Dataset, Dictionary) {
   if (!Assigned(Dataset)) {
      if (!IsEmptyValue(Dataset) &&
         Assigned(Services.InformationsByUSI(Dataset))) {
         Dataset = GetSingleItemByCode(Dataset, 'DeleteRecords');
      } else {
         return;
      }
   }
   if ('DBDataset' == Dataset.ServiceTypeCode) {
      var Table = Dataset.SelectQuery.Items(0).FromTable;
   } else
   if ('Table' == Dataset.ServiceTypeCode) {
      var Table = Dataset;
   } else {
      return;
   }
   var dq = Services.CreateItem('DeleteQuery');
   dq.Table = Table;
   var TableFields = Table.Fields;
   var Filters = dq.Filters;
   var Parameters = dq.Parameters;

   var TableField;
   var FilterFieldType;
   var CompareOperatorType = cotEqual;
   var Parameter;
       
   var Keys = new VBArray(Dictionary.Keys()).toArray();
   var KeysLength = Keys.length;
   for (var i = 0; i KeysLength; i++) {
      TableField = TableFields.ItemsByName(Keys[i]);
      FilterFieldType =
         GetParameterTypeBySQLDataType(TableField.SQLDataType);
      Parameter = AddQueryParameter(Parameters, Keys[i],
         FilterFieldType, Dictionary(Keys[i]));
      AddQueryCompareFilter(Filters, Keys[i], TableField,
         Parameter, CompareOperatorType);
   }
   return dq.Execute();
}

первый параметр - может быть как именем сервиса так и самим объектом Таблица или Датасет;
второй параметр - это набор полей и их значений в виде Словаря.

Функция апдейта:

function UpdateRecord(Dataset, RecordID, Dictionary) {
   if (!Assigned(Dataset)) {
      if (!IsEmptyValue(Dataset) &&
         Assigned(Services.InformationsByUSI(Dataset))) {
         Dataset = GetSingleItemByCode(Dataset, 'UpdateRecord');
      } else {
         return;
      }
   }
   if ('DBDataset' == Dataset.ServiceTypeCode) {
      var Table = Dataset.SelectQuery.Items(0).FromTable;
   } else
   if ('Table' == Dataset.ServiceTypeCode) {
      var Table = Dataset;
   } else {
      return;
   }
   var uq = Services.CreateItem('UpdateQuery');
   uq.Table = Table;
   var TableFields = Table.Fields;
   var Filters = uq.Filters;
   var Parameters = uq.Parameters;
   var Columns = uq.ColumnsValues;

   var TableField;
   var FieldType;
   var CompareOperatorType = cotEqual;
   var Parameter;
   var Column;
       
   TableField = TableFields.ItemsByName('ID');
   Parameter = AddQueryParameter(Parameters, 'ID', sdtGUID, RecordID);
   AddQueryCompareFilter(Filters, 'ID', TableField,
      Parameter, CompareOperatorType);
       
   var Keys = new VBArray(Dictionary.Keys()).toArray();
   var KeysLength = Keys.length;
   for (var i = 0; i KeysLength; i++) {
      TableField = TableFields.ItemsByName(Keys[i]);   
      if (!Assigned(TableField)) {
         continue;
      }
      FieldType = GetParameterTypeBySQLDataType(TableField.SQLDataType);
      AddQueryParameter(Parameters, Keys[i], FieldType,
         Dictionary(Keys[i]));
      Column = Columns.CreateItem();
      Column.ParameterName = Column.KeyValue = Column.Name = Keys[i];
      Column.DataType = FieldType;
      Columns.Add(Column);
   }
   var Result = uq.Execute();
   return Result;
}

первый параметр - может быть как именем сервиса так и самим объектом Таблица или Датасет;
второй параметр - это ID записи в таблице, которую нужно обновить;
третий параметр - это набор полей и их значений в виде Словаря.

Также нужно подключить scr_DB

Пример использования:

var Dictionary = GetNewDictionary();
Dictionary('AccountID') = RecordID;
Dictionary('TypeID') = tOne;
DeleteRecords('tbl_AccountAddress', Dictionary);

var Dictionary = GetNewDictionary();
Dictionary('Name') = 'NewValue';
Dictionary('Name2') = 'NewValue2';
Dictionary('Name3') = 'NewValue3';
UpdateRecord('tbl_Account', RecordID, Dictionary);

Поделиться

1 комментарий

С некоторых пор функция UpdateRecord уже есть "в коробке", в скрипте scr_JobManagerUtils, но с небольшой доработкой, чтобы не пытались обновить значение ID.
После

      TableField = TableFields.ItemsByName(Keys[i]); 

добавлено:

      if (TableField.SQLName == 'ID') {
          continue;		
	  }
Войдите или зарегистрируйтесь, чтобы комментировать