Обработка набора изменений на сервере
На стороне сервера доступ к динамическому набору изменений отличается от доступа к статическому набору данных. В динамическом наборе вы не можете получить доступ к временным таблицам и их полям по имени, вы должны обращаться к ним динамически.
Следующий пример демонстрирует шаблон кода внутренней процедуры, в которой определён параметр с типом INPUT-OUTPUT и атрибутом DATASET-HANDLE, который используется для обновления базы данных:
{include\} {include\} PROCEDURE update-procedure-name: DEFINE INPUT-OUTPUT PARAMETER DATASET-HANDLE phChangeDataSet. RETURN. END PROCEDURE.
Процедура обновления должна выполнять следующее:
- Определить количество временных таблиц в динамическом наборе изменений чтобы процедура обновления могла обработать каждую из них.
- Для каждой временной таблицы:
- Подключить источник данных.
- Прочитать каждую запись из таблицы.
- Применить изменения из каждой записи к базе данных.
- Отключить источник данных.
Рассмотрим каждое из этих действий подробно.
Для определения количества временных таблиц в динамическом наборе данных используется свойство набора данных NUM-BUFFERS.
Синтаксис команды:
change-dataset-handle:NUM-BUFFERS.
Следующий пример демонстрирует часть внутренней процедуры обновления, в которой свойство NUM-BUFFERS используется для прохождения по каждой временной таблице из набора данных.
PROCEDURE updateOrderOrderLineDS: DEFINE INPUT-OUTPUT PARAMETER DATASET-HANDLE phChangeDataSet. DEFINE VARIABLE iBufferCount AS INTEGER NO-UNDO. . . . /* проход по каждой временной таблице из набора данных */ DO iBufferCount = 1 TO phChangeDataSet:NUM-BUFFERS: /* определить, есть ли изменения в таблице, и выполните изменение */ END. . . . RETURN. END PROCEDURE.
Обработка изменений в каждой таблице
Для обработки изменений процедура обновления должна пройти по всем временным таблицам из набора изменений. Если для временной таблицы существует таблица BEFORE, то это является индикатором наличия изменений для обработки этой таблицы. Синтаксис определения наличия таблица BEFORE следующий:
temp-table-buffer-handle = change-dataset-handle:GET-BUFFER-HANDLE(buffer-num). temp-table-before-buffer-handle = temp-table-buffer-handle:BEFORE-BUFFER. IF temp-table-before-buffer-handle <> ? THEN . . .
Следующий пример демонстрирует часть внутренней процедуры обновления, в которой определяется наличие таблицы BEFORE.
PROCEDURE updateOrderOrderLineDS: DEFINE INPUT-OUTPUT PARAMETER DATASET-HANDLE phChangeDataSet. DEFINE VARIABLE iBufferCount AS INTEGER NO-UNDO. DEFINE VARIABLE hBuffer AS HANDLE NO-UNDO. DEFINE VARIABLE hBeforeBuffer AS HANDLE NO-UNDO. . . . /* проход по каждой временной таблице из набора данных */ DO iBufferCount = 1 TO phChangeDataSet:NUM-BUFFERS: /* определить, есть ли изменения в таблице, и выполните изменение */ hBuffer = phChangeDataSet:GET-BUFFER-HANDLE(iBufferCount). hBeforeBuffer = hBuffer:BEFORE-BUFFER. /* если существует таблица BEFORE */ IF hBeforeBuffer <> ? THEN DO: /* Обработка записей с изменениями в этой таблице BEFORE*/ . . . END. END. . . . RETURN. END PROCEDURE.
Как только мы определили, что временная таблица содержит записи, которые должны быть обновлены в базе данных, мы должны подключить соответствующий источник данных. Для подключения источника данных мы должны знать имя временной таблицы.
Для определения имени временной таблицы используется атрибут NAME хэндла текущей обрабатываемой таблицы. Следующий пример дополняет код наших предыдущих примером блоком, в котором определяется имя таблицы и подключается соответствующий источник данных.
PROCEDURE updateOrderOrderLineDS: DEFINE INPUT-OUTPUT PARAMETER DATASET-HANDLE phChangeDataSet. DEFINE VARIABLE iBufferCount AS INTEGER NO-UNDO. DEFINE VARIABLE hBuffer AS HANDLE NO-UNDO. DEFINE VARIABLE hBeforeBuffer AS HANDLE NO-UNDO. . . . /* проход по каждой временной таблице из набора данных */ DO iBufferCount = 1 TO phChangeDataSet:NUM-BUFFERS: /* определить, есть ли изменения в таблице, и выполните изменение */ hBuffer = phChangeDataSet:GET-BUFFER-HANDLE(iBufferCount). hBeforeBuffer = hBuffer:BEFORE-BUFFER. /* если существует таблица BEFORE */ IF hBeforeBuffer <> ? THEN DO: /* Обработка записей с изменениями в этой таблице BEFORE*/ /* Определение имени таблиц и подключение источника данных */ IF hBuffer:NAME = "ttOrder" THEN hBuffer:ATTACH-DATA-SOURCE(DATA-SOURCE srcOrder:HANDLE). ELSE hBuffer:ATTACH-DATA-SOURCE( DATA-SOURCE srcOrderLine:HANDLE). /*Обработка каждой записи в таблице BEFORE*/ END. END. . . . RETURN. END PROCEDURE.
Для обработки каждой записи в таблице BEFORE применяется динамический запрос (QUERY). Для этого необходимо выполнить следующие действия:
- Создать объект QUERY.
- Добавить буфер BEFORE к запросу.
- Подготовить запрос.
- Использовать запрос для прохода по всем записям.
- Удалить объект QUERY.
В следующем примере мы создаём объект Query, добавляем буфер BEFORE к этому объекту, подготавливаем запрос используя имя таблицы, проходим по записям с помощью оператора DO WHILE, где условием завершения будет если GET-NEXT не вернёт доступный буфер. В заключение, когда проход завершён, мы удаляем объект QUERY.
PROCEDURE updateOrderOrderLineDS: DEFINE INPUT-OUTPUT PARAMETER DATASET-HANDLE phChangeDataSet. DEFINE VARIABLE iBufferCount AS INTEGER NO-UNDO. DEFINE VARIABLE hBuffer AS HANDLE NO-UNDO. DEFINE VARIABLE hBeforeBuffer AS HANDLE NO-UNDO. . . . /* проход по каждой временной таблице из набора данных */ DO iBufferCount = 1 TO phChangeDataSet:NUM-BUFFERS: /* определить, есть ли изменения в таблице, и выполните изменение */ hBuffer = phChangeDataSet:GET-BUFFER-HANDLE(iBufferCount). hBeforeBuffer = hBuffer:BEFORE-BUFFER. /* если существует таблица BEFORE */ IF hBeforeBuffer <> ? THEN DO: /* Обработка записей с изменениями в этой таблице BEFORE*/ /* Определение имени таблиц и подключение источника данных */ IF hBuffer:NAME = "ttOrder" THEN hBuffer:ATTACH-DATA-SOURCE(DATA-SOURCE srcOrder:HANDLE). ELSE hBuffer:ATTACH-DATA-SOURCE( DATA-SOURCE srcOrderLine:HANDLE). /* Подготовка к обновлению данных в базе */ CREATE QUERY hQuery. hQuery:ADD-BUFFER(hBeforeBuffer). hQuery:QUERY-PREPARE('FOR EACH ' + hBeforeBuffer:NAME). hQuery:QUERY-OPEN. hQuery:GET-FIRST. DO WHILE hBeforeBuffer:AVAILABLE: /* применение изменений к источнику данных */ hQuery:GET-NEXT. END. /* проход по каждой записи в таблице BEFORE */ DELETE OBJECT hQuery. END. END. . . . RETURN. END PROCEDURE.
Теперь мы готовы применить изменения к базе данных. Для этого мы должны вызвать встроенный метод временной таблицы SAVE-ROW-CHANGES. Синтаксис вызова:
before-table-buffer-handle:SAVE-ROW-CHANGES().
Метод SAVE-ROW-CHANGES() проверяет соответствие записи из таблицы BEFORE записи в базе данных. Если соответствие есть, то происходит обновление базы данных. Затем метод заполняет временную таблицу набора изменений данными из базы данных, которые могут содержать автоматические изменения, внесённые с помощью триггеров базы данных, или изменения, сделанные другими клиентами. И наконец, удаляет запись из таблицы BEFORE.
В некоторых случаях могут возникать ошибки если запись из таблицы BEFORE не соответствует или если обновление не прошло успешно. Позже мы рассмотрим, как обрабатывать ошибки метода SAVE-ROW-CHANGES.
Следующая часть кода демонстрируется процесс прохода по таблице BEFORE с применением изменений к базе данных:
hQuery:GET-FIRST. DO WHILE hBeforeBuffer:AVAILABLE: /* применение изменений к источнику данных */ hBeforeBuffer:SAVE-ROW-CHANGES(). hQuery:GET-NEXT. END. /* проход по каждой записи в таблице BEFORE */
Область действия транзакции для метода SAVE-ROW-CHANGES
Поскольку не существует метода SAVE-CHANGES для всего набора данных или всей временной таблицы, то мы можем использовать SAVE-ROW-CHANGES только на уровне записи. Но поскольку мы обрабатываем изменения на уровне строк, то у нас есть полный контроль над размером и объёмом каждой транзакции. При этом каждое выполнение метода SAVE-ROW-CHANGES стартует собственную транзакцию.
В некоторых случаях границ транзакции в пределах индивидуальной записи может быть достаточным. Однако если ваше приложение требует использование больших транзакций, например, изменение всех записей таблицы в рамках одной транзакции, то необходимо явно указать границы такой транзакции с помощью бока TRANSACTION.
В зависимости от взаимосвязей между таблицами набора данных, вы не сможете просто перебирать каждую таблицу BEFORE и поочерёдно выполнять метод SAVE-ROW-CHANGES для каждой записи. Так как для поддержания целостности базы данных может потребоваться обрабатывать каждую запись в определённой последовательности в пределах группы таблицы, задавая область действия транзакции для каждой такой группы. Например, сначала обрабатывается запись родительской таблицы, затем запись дочерней, затем следующая запись родительской таблицы и связанная с ней запись дочерней. Такой подход позволит нам откатить транзакцию, охватывающую несколько записей в нескольких таблицах в случае, если по какой-либо причине транзакция завершится с ошибкой.
Управление конфликтами
В то время, как процесс сохранения изменений может показаться очень простым при использовании поведения метода SAVE-ROW-CHANGES по умолчанию, вы можете обнаружить, что вам необходим больший контроль над приоритетом изменений при возникновении конфликтов во время обновления базы данных. Наборы данных ABL позволяют обрабатывать такие конфликты различными способами.
Поскольку метод SAVE-ROW-CHANGES поддерживает стратегию оптимистичной локировки, то возможны ситуации, когда другие клиенты могли изменить в базе данных запись из нашего набора данных с момента её первого чтения из базы. Поэтому в таких ситуациях, в зависимости от бизнес-требований к приложению, мы можем выполнить одно из следующих действий:
- Отклонить изменения из набора данных в пользу изменений, внесённых другими пользователями. Это поведение по умолчанию. В этом случае запись из базы данных копируется обратно в набор изменений, который возвращается клиенту.
- Отклонить изменения базы данных, внесённые другими пользователями, в пользу изменений из нашего набора данных.
Приоритет изменений контролируется атрибутом PREFER-DATASET, значением по умолчанию которого FALSE, что говорит о том, что приоритет имеют изменения базы данных.
Если вы хотите, чтобы ваше приложение отдавало предпочтение изменениям в вашем наборе данных при возникновении конфликта, то атрибуту PREFER-DATASET следует установить значение TRUE.
Наборы данных ABL позволяют осуществлять больший контроль над приоритетом при обработке изменений на сервере. За дополнительной информацией обратитесь к соответствующему разделу документации ProDataSets.
Отключение источника данных
После того, как изменения были обработаны, необходимо отключить источник данных, и лучше это сделать сразу по завершению обработки.
Ниже приведена полная реализация процедуры обновления для набора данных dsOrderOrderLine:
PROCEDURE updateOrderOrderLineDS: DEFINE INPUT-OUTPUT PARAMETER DATASET-HANDLE phChangeDataSet. DEFINE VARIABLE iBufferCount AS INTEGER NO-UNDO. DEFINE VARIABLE hBuffer AS HANDLE NO-UNDO. DEFINE VARIABLE hBeforeBuffer AS HANDLE NO-UNDO. /* проход по каждой временной таблице из набора данных */ DO iBufferCount = 1 TO phChangeDataSet:NUM-BUFFERS: /* определить, есть ли изменения в таблице, и выполните изменение */ hBuffer = phChangeDataSet:GET-BUFFER-HANDLE(iBufferCount). hBeforeBuffer = hBuffer:BEFORE-BUFFER. /* если существует таблица BEFORE */ IF hBeforeBuffer <> ? THEN DO: /* Обработка записей с изменениями в этой таблице BEFORE*/ /* Определение имени таблиц и подключение источника данных */ IF hBuffer:NAME = "ttOrder" THEN hBuffer:ATTACH-DATA-SOURCE(DATA-SOURCE srcOrder:HANDLE). ELSE hBuffer:ATTACH-DATA-SOURCE( DATA-SOURCE srcOrderLine:HANDLE). /* Подготовка к обновлению данных в базе */ CREATE QUERY hQuery. hQuery:ADD-BUFFER(hBeforeBuffer). hQuery:QUERY-PREPARE('FOR EACH ' + hBeforeBuffer:NAME). hQuery:QUERY-OPEN. hQuery:GET-FIRST. DO WHILE hBeforeBuffer:AVAILABLE: /* применение изменений к источнику данных */ hBeforeBuffer:SAVE-ROW-CHANGES(). hQuery:GET-NEXT. END. /* проход по каждой записи в таблице BEFORE */ DELETE OBJECT hQuery. /*Отключение набора данных*/ hBuffer:DETACH-DATA-SOURCE(). END. END. RETURN. END PROCEDURE.