Здесь РјРЅРµ хотелось Р±С‹ остановиться РЅР° свойствах TField.ProviderFlags Рё TDataSetProvider.UpdateMode. Дело РІ том, что меня часто спрашивают, что зависит РѕС‚ значений этих свойств, Р° зависит РѕС‚ РЅРёС… довольно РјРЅРѕРіРѕ. Р’ справке РїРѕ VCL эти свойства описаны, РЅР° РјРѕР№ взгляд, недостаточно РїРѕРґСЂРѕР±РЅРѕ, Р° СЃРІСЏР·СЊ между РЅРёРјРё достаточно тесная. Ртак, пусть имеется компонент TQuery, TIBQuery или какой-то РґСЂСѓРіРѕР№ (запрос), соединенный СЃ сервером БД, Рё Рє нему присоединен TDataSetProvider. Р’ этом случае РЅР° логику работы оказывают влияние именно значения свойства ProviderFlags полей этого запроса, аналогичные свойства полей РЅР° клиентской стороне никакого влияния РЅРµ оказывают. Комбинация значений этих свойств полностью определяет, как Р±СѓРґСѓС‚ производиться операции обновления данных РЅР° сервере БД. Рассмотрим обновление данных РІ таблице. Добавление Рё удаление записи РїСЂРѕРёСЃС…РѕРґРёС‚ аналогично.
Провайдер с установленным свойством ResolveToDataset = false при обновлении записи формирует SQL-запрос вида UPDATE
SET =, ... WHERE = AND ..., РІ полном соответствии СЃРѕ стандартом SQL (РїСЂРё ResolveToDataset=True производится РїРѕРёСЃРє Рё обновление РїСЂСЏРјРѕ РІ таблице). РРјСЏ таблицы
берется РёР· Dataset (провайдер великолепно понимает запросы SQL РІРёРґР° Select from...), либо задается РІ обработчике OnGetTableName. Значения NewValue Рё OldValue для каждого поля берутся РёР· пакета обновления, посылаемого провайдеру. Рмена полей РІ выражениях SET Рё FROM формируются автоматически, как раз РЅР° РѕСЃРЅРѕРІРµ свойств ProviderFlags Рё UpdateMode того набора данных, через который провайдер работает СЃ базой. Алгоритм следующий: Р’ предложение SET РІС…РѕРґСЏС‚ только те поля, Сѓ которых установлен флаг pfUpdate РІ свойстве ProviderFlags (требуется обновлять РІ базе данных) Рё OldValue NewValue (значение поля было изменено).
Предложение WHERE формируется следующим образом:
Берутся все поля, у который установлены [pfInKey, pfInWhere], фактически это первичный ключ. При UpdateMode=upWhereKeyOnly больше никаких полей не берется.
При UpdateMode=upWhereChanged к полям первичного ключа добавляются те поля, у которых OldValue NewValue и pfWhere in ProviderFlags, что позволяет делать проверку на изменение тех же полей другим пользователем.
При UpdateMode=upWhereAll в список полей WHERE входят все поля записи, у которых pfWhere in ProviderFlags.
В случае, если запись в таблице на сервере не найдена (нет записей, удовлетворяющих условию WHERE), пользователю выдается сообщение вида "Запись изменена другим пользователем", вне зависимости от причины.
Остается одно значение флага, pfHidden. Поля с этим флагом не передаются клиентскому приложению, и не принимаются от него, флаг указывает, что эти поля - только для использования на стороне сервера.
Если уж создан постоянный список полей, можно установить параметры их отображения на клиентской части, в частности, DisplayLabel, DisplayWidth и Visible, а у провайдера - флаги poIncFieldProps. При этом на клиентской части можно не заботиться о списке полей – значения, полученные с сервера приложений, переопределяют заданные на клиенте в любом случае. Заодно у провайдера надо установить опцию poMultiRecordUpdates, чтобы на клиентской части можно было изменять сразу несколько записей в справочнике до отправки изменений на сервер.
Поле CLIENT_ID в справочнике поставщиков и получателей является первичным ключем, а стало быть, в нем должны содержаться уникальные значения. Для получения уникальных значений удобно использовать автоинкрементальные поля (autoincrement field). В IB собственно автоинкрементных полей нет, нарастающие значения получают от генератора с помощью функции Gen_ID, и как правило, присваивают это значение полю в триггере. Мне нравится ситуация, когда новое уникальное значение появляется на клиентской части сразу после добавления записи. Поэтому вместо присвоения значения, полученного от генератора, в триггере, используется хранимая процедура, результатом работы которой и является это значение. Для этого в удаленном модуле данных расположен компонент spNewID: TIBStoredProc, присоединенный к компоненту транзакции ibtDefault, который предоставляет доступ к хранимой процедуре на сервере БД. Процедура описана в базе данных следующим образом:
create procedure CLIENT_ID returns (ID integer) as begin В ID = Gen_ID(CLIENT_ID_GEN,1); end |
Как РІРёРґРЅРѕ, процедура просто выдает следующее значение генератора. Рто гарантирует, что РїСЂРё последовательных запросах Рє ней это значение повторяться РЅРµ будет. Получение значения РЅР° клиентской части обеспечивается методом сервера, РѕР± этом немного РЅРёР¶Рµ.
Вторая хранимая процедура, spClientFullName, присоединена Рє компоненту транзакции ibtClient Рё предназначена для выдачи имени Рё телефона поставщика или получателя РІ РІРёРґРµ единой строки В«РРјСЏ (телефон)В», возвращаемой процедурой сервера БД CLIENT_FULL_NAME. Рта строка также передается РЅР° клиентскую часть через метод сервера.
Группа компонентов ibtDocList, ibqDocList, dspDocList и ibqDelDoc предназначена для работы со списком документов. У IbtDocList, компонента транзакции, установлен режим read committed, а в компоненте ibqDocList содержится SQL-запрос «select * from List_doc(:FromDate, :ToDate)». Весь список документов сразу выводить довольно бессмысленно, их может быть много. Поэтому запрос выбирает список документов, даты которых лежат в промежутке от FromDate до ToDate. Провайдер dspDocList выдает этот список клиентской части.
Дополнительный компонент, ibqDelDoc, как, думаю, видно из его названия, предназначен для удаления документа, в его свойстве SQL стоит запрос «delete from DOC_TITLE where DOC_ID = :DOC_ID». Несмотря на то, что для создания и изменения документа планируется использовать отдельный модуль, rdmDoc, для удаления документа вовсе необязательно его открывать, и с точки зрения интерфейса пользователя удобно делать это прямо из списка документов. На первый взгляд, использование отдельного запроса для удаления кажется излишним, для этого обычно достаточно объявить в обработчике dspDocList.OnGetTableName имя таблицы (DOC_TITLE), и удаление будет автоматически обеспечено. Однако в постановке задачи стоит условие, что открытый в одной клиентской части документ должен быть недоступен для изменения (а значит, и удаления) из других клиентских частей. Поэтому приходится делать это в обработчике события dspDocList.OnBeforeUpdateRecord следующим образом:
procedure TrdmCommon.dspDocListBeforeUpdateRecord(Sender: TObject; В SourceDS: TDataSet; DeltaDS: TClientDataSet; UpdateKind: TUpdateKind; В var Applied: Boolean); var В DocID: Integer; begin В if UpdateKind = ukDelete then //Только если запись удаляется В begin В DocID := DeltaDS.FieldByName('DOC_ID').AsInteger; В try В В if not RegisterDoc(DocID) then //Пытаемся зарегистрировать В В В raise Exception.Create('Документ редактируется'); В В with ibqDelDoc do //Удаляем В В begin В В В paramByName('DocID').AsInteger := DocID; В В В ExecSQL; В В end; В В Applied := True; В finally В В UnregisterDoc(DocID); //Рзменение закончено, удалили В end; В end; end; |
Если удаляется документ, попытаемся его зарегистрировать в списке редактируемых функцией RegisterDoc, затем, если это получилось, удаляем его с помощью запроса ibqDelDoc и удаляем из списка редактирования (UnregisterDoc). Устанавливаем Applied := true, чтобы сказать провайдеру, что все уже сделано.
Конечно, одновременно может редактироваться (удаляться, добавляться) довольно много документов, поэтому нужен единый список этих документов, к которому будут обращаться процедуры RegisterDoc и UnregisterDoc. Поскольку обращение к нему будет производиться из модулей данных, работающих в разных потоках, то наилучшим образом для этого подходит TThreadList (потокобезопасный класс списка). Список документов должен быть единым для всех клиентских частей, поэтому расположить его нужно в отдельном модуле, например, в модуле главной формы сервера. На ней потом можно вывести, например, список редактируемых на данный момент документов. Так и сделаем.
Р’ модуле главной формы сервера РІ разделе implementation РѕР±СЉСЏРІРёРј переменную DocList: TThreadList; Ртот СЃРїРёСЃРѕРє лучше инициализировать сразу РїСЂРё запуске сервера Рё уничтожать РїСЂРё выходе:
initialization В DocList := TThreadList.Create; finalization В if Assigned(DocList) then В begin В DocList.Free; В DocList := nil; В end; end. |
С этим списком работают две функции: RegisterDoc и UnregisterDoc :
function RegisterDoc(DocID: integer): boolean; begin В Result := False; В if DocID = 0 then Exit; В with DocList.LockList do В try В if IndexOf(Pointer(DocID)) В begin В В Add(Pointer(DocID)); В В Result := True; В end; В finally В DocList.UnlockList; В end; end; function UnregisterDoc(DocID: integer): boolean; begin В Result := False; В if DocID = 0 then Exit; В with DocList.LockList do В try В if IndexOf(Pointer(DocID)) >= 0 then В begin В В Remove(Pointer(DocID)); В В Result := True; В end; В finally В DocList.UnlockList; В end; end; |
В списке хранятся идентификаторы документов. Но TThreadList предназначен для хранения указателей. Поэтому для хранения в этом списке идентификатора, имеющего тип Integer, придется привести его к типу pointer. Конечно, если потребуется хранить дополнительную информацию о документе, например, его номер, придется организовать в списке ссылки на записи, с выделением памяти под эту запись и уничтожением ненужных записей. При этом внешний вид функций не изменится, просто усложнится работа со списком, и может понадобиться обращение к БД для получения дополнительной информации.
Теперь все просто: все модули данных, которые работают с документами, используют эти две функции, и если RegisterDoc возвращает false (а это произойдет только в том случае, если номер уже есть в списке), то пользователю выдается сообщение, что с документом уже работают. Функция UnregisterDoc просто удаляет номер из списка.
На клиенте понадобится, кроме доступа к двум провайдерам, еще пара функций – получение нового значения CLIENT_ID для справочника клиентов и получение полного имени клиента. Для этого необходимо создать описание этих функций в библиотеке типов.
В зависимости от того, какой синтаксис используется в редакторе библиотеки типов (IDL или Pascal), объявление этих функций выглядит по-разному, ниже приведены их описания в protected-секции модуля данных:
protected В class procedure UpdateRegistry(Register: Boolean; const ClassID, ProgID: string); В override; В function NewClientID: Integer; safecall; В function Get_ClientName(ClientID: Integer): WideString; safecall; |
На IDL это выглядит так:
[id(0x00000001)] HRESULT _stdcall NewClientID([out, retval] long * Result); [propget, id(0x00000004)] HRESULT _stdcall ClientName([in] long ClientID, [out, retval] BSTR * Value); |
Реализация этих функций довольно проста. Надо вызвать хранимые процедуры, и выдать возвращаемое ими значение в качестве результата:
function TrdmCommon.NewClientID: Integer; begin В lock; В with spNewID do В try В ExecProc; В Result := paramByName('ID').AsInteger; В finally В unlock; В end; end; function TrdmCommon.Get_ClientName(ClientID: Integer): WideString; begin В lock; В try В with spClientFullName do В begin В В paramByName('ID').AsInteger := ClientID; В В ExecProc; В В Result := paramByName('FULL_NAME').AsString; В end; В finally В unlock; В end; end; |
Теперь основной модуль готов, и можно перейти к написанию следующего модуля данных, предназначенного для работы с документом.

Р РёСЃСѓРЅРѕРє 3.
Здесь уже все немного сложнее. Разумеется, здесь тоже есть соединение с базой ibdDoc, настроенное на сервер БД. Хранимая процедура spNewID выдает на этот раз номер для нового документа, используя процедуру DOC_TITLE_ID, аналогичную процедуре CLIENT_ID.
РќР° этот раз РІ модуле данных, РїРѕРјРёРјРѕ компонентов запросов Рє серверу, присутствуют РґРІР° компонента TРЎlientDataSet Рё РґРІР° провайдера данных. Рти дополнительные компоненты предназначены именно для организации расчетов РЅР° сервере. Поскольку, как РјС‹ договорились, РЅР° сервере приложений должна рассчитываться СЃСѓРјРјР° документа, то РЅР° нем должно быть известно содержимое документа РґРѕ того, как РѕРЅРѕ будет сохранено РІ БД. Разумеется, это РјРѕР¶РЅРѕ осуществить, используя события провайдера для предварительной обработки пакета изменений, поступившего РѕС‚ клиентской части, РЅРѕ РјРЅРµ хотелось показать возможность организации работы СЃ документом как СЃ полноценным объектом.
Рдея простая: пусть весь удаленный модуль данных работает СЃ РѕРґРЅРёРј документом. Р’ таком случае этот модуль будет выглядеть для клиентской части как полноценный объект, владеющий всеми данными документа Рё предоставляющий клиентской части РІСЃРµ необходимые свойства. Разумеется, РѕС‚ пакетов данных никто РЅРµ отказывается.
Таким образом, организуется следующий алгоритм работы: Клиентская часть создает на сервере либо новый документ, либо открывает существующий (удаление документов уже реализовано). Сервер приложений создает модуль данных и, если необходимо, закачивает в него содержимое документа с сервера БД. После этого клиентская часть и удаленный модуль данных совместно обрабатывают эти данные, занимаясь каждый своим делом: клиентская часть предоставляет средства для изменения этих данных пользователем, а удаленный модуль производит все необходимые расчеты.
К одному компоненту транзакции ibtDoc присоединено на этот раз два запроса ibqTitle и ibqBody, соответственно выбирающих одну строку заголовка документа (select * from DOC_TITLE where DOC_ID = :DocID) и все строки этого документа (select * from DOC_BODY where DOC_ID = :DocID).
РџР РМЕЧАНРР• Хотя MIDAS требует наличия своей IBTransaction для каждой пары компонентов "IBQuery-провайдер", РІ данном случае это необязательно. Провайдеры РЅРµ Р±СѓРґСѓС‚ начинать Рё завершать транзакции, открываться Рё закрываться транзакция будет СЏРІРЅРѕ, РІ соответствующих методах. |
К этим запросам присоединены провайдеры dspTitleInner и dspBodyInner, назначение которых – получить данные с сервера БД и передать их в соответствующие ClientDataSet. Свойство Exported у этих провайдеров установлено в false, они нужны только внутри сервера приложений, и видеть их на клиентской части незачем. Соответственно, клиентский набор данных cdsTitle (компонент TClientDataSet) получает одну строку заголовка из dspTitle и cdsBody, содержимое документа из dspBody.
Для того, чтобы клиентская часть могла получать и изменять данные документа, к клиентским наборам данных cdsTitle и cdsBody присоединены провайдеры данных, dspTitle и dspBody, соответственно. Свойству Exported этих провайдеров оставлено значение по умолчанию, True, зато свойство ResolveToDataSet установлено в True, для того, чтобы эти провайдеры не пытались работать с ClientDataSet с помощью запросов. Таким образом, клиентская часть может получать и изменять данные не из TIBQuery, но из TClientDataSet, причем совершенно об этом не догадываясь. По команде с клиентской части изменения, передаются серверу приложений, который и сохраняет их в БД.
Теперь посмотрим, что нам нужно для подобной реализации. Функции для синхронизации обработки документов RegisterDoc и UnregisterDoc уже есть, нужно их только использовать. С их помощью гарантируется, что одновременно один и тот же документ редактироваться не будет, поэтому у провайдеров данных dspTitleInner и dspTitleBody достаточно установить UpdateMode = upWhereKeyOnly, и указать ключевые поля у запросов. Содержимое документа может состоять из нескольких строк, поэтому у dspBodyInner и dspBody нужно установить флаг poAllowMultiRecordUpdates. Теперь нужно разобраться с полями клиентских наборов данных, установив у них соответствующие свойства. Я остановлюсь здесь только на свойстве ProviderFlags. Поскольку поле «Ссылка на документ» (DOC_ID) на клиентской части не нужно, ему можно задать флаг pfHidden. Разумеется, у всех ключевых полей (DOC_ID и LINE_NUM) и в наборе данных заголовка, и в содержимом документа надо указать флаг pfInKey. У провайдеров dspTitle и dspBody нужно установить политику обновления UpdateMode = upWhereKeyOnly, клиентская часть у модуля данных одна, и другие значения совершенно ни к чему.
Теперь компоненты для хранения и обработки данных подготовлены, осталось написать сами методы работы с ними.
Давайте разберемся, что именно требуется. Модуль rdmDoc предназначен как для создания РЅРѕРІРѕРіРѕ документа, так Рё для редактирования существующего. Ртот модуль можен находиться РІ РѕРґРЅРѕРј РёР· трех состояний, описанных РІ перечислении TObjState:
osInactive: данных нет, документ не редактируется,
osInsert: создан новый документ и
osUpdate – происходит изменение существующего документа.
Состояние хранится в переменной Fstate, находящейся внутри модуля. Сразу после создания и после окончания обработки документа модуль данных должен находиться в неактивном состоянии.
Переход из одного состояния в другое должен обеспечиваться соответствующими методами. Я назвал эти методы DoInactiveState (перевод в неактивное состояние), DoOpen (открыть существующий документ) и DoCreateNew (создание нового документа). При редактировании или добавлении документа нужно знать его уникальный номер, записываемый в поле DOC_ID. Для этого достаточно объявить в секции private переменную FDocID: integer, которая и будет его хранить.
В библиотеке типов нужно реализовать методы, которые будут создавать документ или открывать существующий, а также сохранять изменения. Кроме этого, понадобится свойство, позволяющее получить в любой момент сумму по документу. Сумма каждой строки содержимого пусть рассчитывается на клиентской части.
Ртак, приступим. Сначала описываются методы перехода между состояниями, РѕРЅРё предназначены для внутреннего использования, Рё поэтому РёС… объявления содержатся РІ секции private:
В procedure DoInactiveState; В procedure DoCreateNew; В procedure DoOpen(DocID: integer); |
Рассмотрим их по порядку.
procedure TrdmDoc.DoInactiveState; begin В UnregisterDoc(FDocID); В FDocID := 0; В cdsTitle.Active := False; В cdsBody.Active := False; В ibtDoc.Active := False; В FState := osInactive; end; |
Процедура DoInactiveState удаляет документ из списка редактируемых, закрывает все клиентские наборы данных, а также производит откат транзакции (если она была активна).
procedure TrdmDoc.DoOpen(DocID: Integer); begin  if DocID = 0 then Exit;  try  if not RegisterDoc(DocID) then   raise Exception.Create('Документ редактируется');  FDocID := DocID; // и только здесь, иначе DoInactiveState удалит документ  ibdDocs.Connected := True;  ibtDoc.StartTransaction;  with cdsTitle do  begin   params.paramByName('DocID').AsInteger := FDocID;   Active := True;   if BOF and EOF then    raise Exception.Create('Документ не найден');  end;  with cdsBody do  begin   params.paramByName('DocID').AsInteger := FDocID;   Active := True;  end;  FState := osUpdate;  ibtDoc.Commit;  except  DoInactiveState;  raise;  end; end; |
DoOpen предназначена для открытия существующего документа, идентификатор DOC_ID которого равен входному параметру DocID. Первым делом с помощью RegisterDoc производится проверка того, что документ в данный момент не редактируется. Затем идентификатор документа запоминается, и в клиентские наборы данных загружаются данные документа. В случае ошибки состояние документа переводится в osInactive.
procedure TrdmDoc.DoCreateNew; var  NewDocID: Integer; begin  try  NewDocID := NewID;  if not RegisterDoc(NewDocID) then   raise Exception.Create('Документ редактируется');  FDocID := NewDocID;  ibdDocs.Connected := True;  ibtDoc.StartTransaction;  with cdsTitle do  begin   params.paramByName('DocID').AsInteger := FDocID;   Active := True;   Append;   Post;  end;  with cdsBody do  begin   params.paramByName('DocID').AsInteger := FDocID;   Active := True;  end;  ibtDoc.Commit;  FState := osInsert;  except  DoInactiveState;  raise;  end; end; |
Процедура DoCreateNew предназначена для создания нового документа. Она практически аналогична предыдущей, за исключением того, что идентификатор документа получается от сервера БД с помощью процедуры NewID, которая обращается к хранимой процедуре на сервере. Реализация процедуры DoCreateNew очень похожа на аналогичную реализацию в rdmCommon.
Для того, чтобы вставка новой записи в документ происходила верно, достаточно написать обработчик cdsTitle.OnNewRecord, задающий начальное значение полей записи, и практически такой же обработчик для cdsBody:
procedure TrdmDoc.cdsTitleNewRecord(DataSet: TDataSet); var В Day, Month, Year: Word; begin В DecodeDate(Date, Year, Month, Day); В with cdsTitle do В begin В FieldByName('DOC_ID').AsInteger := FDocID; В FieldByName('DOC_NUM').AsString := IntToStr(FDocID) В В + '/' + IntToStr(Year); В FieldByName('DOC_DATE').asDateTime := Date; В FieldByName('DOC_SUM').asCurrency := 0; В FieldByName('FROM_ID').AsInteger := 0; В FieldByName('TO_ID').AsInteger := 0; В end; end; procedure TrdmDoc.cdsBodyNewRecord(DataSet: TDataSet); begin В cdsBody.FieldByName('DOC_ID').AsInteger := FDocID; end; |
В дополнение ко всему нужна еще одна процедура в секции private, для подсчета суммы документа:
function TrdmDoc.CalcSum: Currency; begin В Result := 0; В if not cdsBody.Active then Exit; В with cdsBody do В begin В First; В while not EOF do В begin В В Result := Result В В В + FieldByName('COUNT_NUM').asCurrency В В В * FieldByName('PRICE').asCurrency; В В Next; В end; В end; end; |
В функции CalcSum просматривается содержимое документа и рассчитывается общая сумма, которая возвращается в качестве результата.
Теперь надо позаботиться о клиентской части, то есть создать необходимые внешние методы сервера в библиотеке типов. Описание этих методов, созданное редактором библиотек типов, выглядит следующим образом:
protected В function ApplyChanges: WideString; safecall; В function Get_DocID: Integer; safecall; В procedure CreateNewDoc; safecall; В procedure Set_DocID(Value: Integer); safecall; В function Get_DocSum: Currency; safecall; |
Функциональность этих методов такова:
ApplyChanges – сохраняет текущий документ в БД.
DocID – свойство, доступное на запись и чтение При чтении выдается текущий ID документа (FDocID). При изменении значения свойства документ открывается для редактирования с ID, равным новому значению. Если значение свойства равно 0, документ закрывается, и модуль переводится в неактивное состояние.
CreateNewDoc – создает новый документ (вызывает методы DoInactiveState и DoCreateNew).
DocSum – выдается текущая сумма документа, результат работы метода CalcSum.
Реализация этих методов довольно проста, все основные процедуры уже есть, сложность представляет только функция ApplyChanges:
function TrdmDoc.ApplyChanges: WideString; begin  lock;  try  FLastUpdateErrors := '';  if FState = osInactive then   raise Exception.Create('Нет нового или открытого документа');  // Вычисляем итоговую сумму документа  with cdsTitle do  begin   Edit;   FieldByName('DOC_SUM').asCurrency := CalcSum;   Post;  end;  RenumLines; // перенумерация содержимого  // Сохранение в БД...  ibtDoc.StartTransaction;  // При вставке сначала сохраняем изменения в cdsTitle...  if FState = osInsert then  begin   if cdsTitle.ChangeCount > 0 then    cdsTitle.ApplyUpdates(0);   if cdsBody.ChangeCount > 0 then    cdsBody.ApplyUpdates(-1);  end;  // ...а при изменении – в cdsBody.  if FState = osUpdate then  begin   if cdsBody.ChangeCount > 0 then    cdsBody.ApplyUpdates(-1);   if cdsTitle.ChangeCount > 0 then    cdsTitle.ApplyUpdates(0);  end;  // FLastUpdateErrors заполняется на OnReconcileError.  Result := FLastUpdateErrors;  if Result = '' then   ibtDoc.Commit  else  begin   ibtDoc.Rollback;  end;  finally  ibtDoc.Active := False;  unlock;  end; end; |
Дело РІ том, что изменение данных РІ БД РїСЂРѕРёСЃС…РѕРґРёС‚ РЅРµ РІ методе провайдера, Р° РІ методе модуля, Рё клиентские наборы данных ничего РѕР± этом РЅРµ знают. Поэтому функция ApplyChanges возвращает СЃРїРёСЃРѕРє ошибок, возникших РїСЂРё обновлении данных. РЎРїРёСЃРѕРє накапливается РІ переменной FLastUpdateErrors, описанной РІ секции private как FLastUpdateErrors: String;. Перед сохранением изменений рассчитывается СЃСѓРјРјР° документа. Процедура RenumLines нумерует строки содержимого РїРѕ РїРѕСЂСЏРґРєСѓ. Рто просто дополнительный сервис. Затем ClientDataSet-С‹ пытаются сохранить изменения РІ БД. РџСЂРё возникновении ошибки заполняется поле FLastUpdateErrors:
procedure TrdmDoc.cdsTitleReconcileError(DataSet: TClientDataSet;  E: EReconcileError; UpdateKind: TUpdateKind;  var Action: TReconcileAction); begin  Action := raCancel;  FLastUpdateErrors := FLastUpdateErrors + 'Заголовок: ' + E.Message + #13#10; end; procedure TrdmDoc.cdsBodyReconcileError(DataSet: TClientDataSet;  E: EReconcileError; UpdateKind: TUpdateKind;  var Action: TReconcileAction); begin  Action := raCancel;  FLastUpdateErrors := FLastUpdateErrors + 'Содержимое: '            + E.Message + #13#10; end; |
При этом происходит откат транзакции. Сообщения об ошибке записываются в строку. В случае возникновения ошибки клиент должен вывести сообщение и обновить клиентские наборы данных. Как будет видно ниже, в данном случае все проверки можно сделать заранее, и практически возможны только ошибки, связанные с непредвиденными обстоятельствами (например, неожиданный разрыв соединения с сервером БД).
Процедура RenumLines перенумерует строки содержимого документа так, чтобы номера шли РїРѕ РїРѕСЂСЏРґРєСѓ, причем РІСЃРµ номера сначала делаются отрицательными, иначе РїСЂРё попытке запомнить вторую запись СЃ тем Р¶Рµ ключем сразу генерируется исключение Key violation, что, разумеется, совершенно РЅРµ РЅСѓР¶РЅРѕ (Дело РІ том, что провайдер великолепно знает, какие поля составляют первичный ключ, РІРѕС‚ Рё контролирует – Сѓ ClientDataSet создается контроль первичного ключа. Рсключение генерируется сразу, РїСЂРё попытке вставки (РґРѕ записи РІ БД)):
procedure TrdmDoc.RenumLines; var  Num: Integer; begin  cdsBody.IndexFieldNames := 'DOC_ID;LINE_NUM';  // Чтобы избежать Key violation при перенумерации, делаем все номера  // На клиенте нужна проверка LINE_NUM >= 0  cdsBody.Last;  with cdsBody do  while FieldByName('LINE_NUM').AsInteger > 0 do  begin   Edit;
Похожие статьи
|