====== Взаимодействие UniLines с Метой и БД ======
Хранение данных происходит с помощью типа TLinesData, являющегося record.
В TLinesData самое важное - это NodeColList, список класса TVariantList, хранит в себе значения всех колонок для конкретной строки данных.
====== Как отображается результат запроса из метаобъектов? ======
Здесь нам важно ознакомиться и понять принцип заполнения следующих полей класса UniLines:
* Cls: TList; используется для хранения ссылок на колонки VST и соответствующих метаколонок;
* Fields: TStringList; используется для хранения названий колонок;
Для этого используется процедура Open. Она принимает в себя следующие параметры:
* AMetaObject_Tag: int64 - таг нужного метаобъекта из Меты;
* AObject_ID: variant - ID записи, по которой нужно выполнить запрос (является параметров :MAIN_ID в запросе метаобъекта);
* AProcGenID: string - процедура генерации ID Для новых записей;
* ARdTr: TpFIBTransaction - считывающая транзакция;
* AIsModal: boolean = False - используется если основная форма, на которой располагается UniLines, является модальной. Позволяет инициировать процедуру DoModalClose, если она была определена, при двойном клике на строчке или нажатии Enter. В противном случае (значение False) запускается процедура редактирования записи (если возможно).
* sAddWhere: string =' ' - дополнительное условие для запроса метаобъекта (не забыть, что в нём должен быть %FILTER_EXPRESSION%)
* UseTVariantList: Boolean = True - не используется;
* Query: String= ' '; - не используется;
* WillBeRefresh: boolean = False - признак того, что результат выполнения запроса метаобъекта может быть изменён в процессе работы с UniLines. Нужно включить, если планируется обновление данных.
* CheckColumns: boolean = False - если выключено, то используются дополнительные условия в %FILTER_EXPRESSION% при формировании запроса (такие как ограничение по дате, указанное в метаколонках, или ограничение по складам)
* const iCastlingTables: Integer =0 - позволяет менять таблицы в метазапросе местами. Используется для оптимизации выполнения в случае использования тех или иных фильтров. Сейчас работает только для 26-го метаобъекта; 0 означает, что первой таблицей должен быть DOCS, 1 - DOCS_ORDER, 2 - MOBILE_PLAN
* AsyncOpenProc: TAsyncOpenProc=nil - процедура для асинхронной загрузки данных UniLines;
* const params: TVariantList = nil - позволяет передать список параметров для запроса метаобъекта.
Учитывая все параметры по умолчанию, то для самой простой загрузки данных, без необходимости их редактирования, будет достаточно и такого:
ulf.Open(2820, null, '', RdTr);
Краткая последовательность загрузки данных такова:
MetaObject:=DMForm.MetaObjectList.MainRoot.FindObjectByTag(AMetaObject_Tag); //находится нужный метаобъект по тагу;
qQuery.SelectSQL.Text:=MetaQuery.GetFullSQL(sAddWhere, CheckManyToOneLinks,
True, False, CheckColumns, False, iCastlingTables); //формируется запрос;
qQuery.Open; //открывается датасет
FillColumns; //заполнение колонок
LoadData(null); //загрузка данных
Самое интересное происходит как раз в двух последних процедурах.
FillColumns выглядит следующим образом:
//цикл по всем метаколонкам метазапроса
for i:=0 to MetaQuery.MetaColumnsCount-1 do
begin
cl:=MetaQuery.MetaColumns[i]; //получение конкретной метаколонки
if cl.Is_LineNumber or cl.OnlyForFilters then //условия, что колонке не нужно имя
fn := ''
else
begin
//определение названия колонки
if MetaQuery.SQL='' then
fn := qQuery.Fields[j].FieldName
else
fn := cl.SQL_Select;
inc(j);
end;
//добавление колонки
if FillColumn(cl,fn) then
inc(cnt);
end;
В FillColumn из интересного происходит следующее:
col:=AddColumn; //вызывает TVirtualTreeColumnFooter(VST.Header.Columns.Add) - добавление колонки в заголовок VST
{дальше много кода по выставлению различных свойств у этой колонки}
Fields.Add(AFieldName); //добавляет название колонки в Fields
fr:=ClsAdd(Cl, col); //добавляет в список Cls "фрейм-колонку" - которая объединяет в себе ссылки на объекты колонок VST и мета-колонки
{выставление оставшихся свойств}
Таким образом после FillColumns у нас заполняется список названий колонок Fields, список колонок Cls (элементы которого являются ссылками на колонки VST и на метаколонки).
Затем вызывается LoadData, у которого из основного:
qQuery.First;
while (not qQuery.Eof) do
begin
//addNewLine по умолчанию True
if (AddNewLine) then
Dat := AddData(nil) //здесь формируется NodeColList об этом будет ниже
else
//... несущественные сейчас операции
if (Dat <> nil) then
begin
//проходимся по списку наших колонок
for i:=0 to Cls.Count-1 do
begin
//вытаскиваем ссылку на метаколонку
fr := TFrameColumn(Cls.Items[i]);
cl := fr.cl;
//...
//заполняем значение в NodeColList для текущей колонки в массиве
tempVariant := qQuery.FieldByName(Fields[i]).Value;
if VarType(tempVariant)=8209 then
begin
if VarIsNull(tempVariant) then
Dat^.NodeColList.Items[i]:= qQuery.FieldByName(Fields[i]).Value
else
Dat^.NodeColList.Items[i]:= qQuery.FieldByName(Fields[i]).asString;
end
else
Dat^.NodeColList.Items[i]:= qQuery.FieldByName(Fields[i]).Value;
end
end
end
И теперь ещё посмотрим, что происходит внутри AddData:
function TUniLinesFrame.AddData(ANode: PVirtualNode = nil): PLinesData;
var Node: PVirtualNode;
i: integer;
begin
//если мы передали ANode = nil, то будем создавать новую ноду, иначе работаем с переданной
if ANode=nil then
begin
Node:=VST.AddChild(nil);
if FIsMultiline then
Node.States:=Node.States+[vsMultiline];
end
else
Node:=ANode;
//...
Result:=VST.GetNodeData(Node); //получаем данные текущей ноды (запись типа PLinesData)
Result.NodeColList:=TVariantList.Create; //создаём объект NodeColList
for i:=0 to Cls.Count-1 do
Result.NodeColList.Add(Null); //заполняем null-ами элементы по числу наших колонок в Cls
//инициализация остальных параметров, указание опций по умолчанию
end;
Подытожим последовательность происходящих действий:
- TUniLinesFrame.Open - основная процедура, которую используем извне.
- Формируется датасет на основе результата запроса метаобъекта;
- Вызывается FillColumns;
- FillColumns определяет название каждой колонки метаобъекта и затем для них вызывает FillColumn;
- FillColumn добавляет колонку в VST, название колонки в Fields и элемент в Cls;
- После отработки FillColumns вызывается LoadData;
- LoadData вызывает AddData для каждой строки сформированного датасета;
- AddData добавляет ноду в VST, создаёт для неё NodeColList и заполняет его null-ами по числу колонок;
- Затем в цикле происходит перебор всех названий колонок и заполнение NodeColList соответствующими значениями из датасета.
====== Примеры использования ======
Рассмотрим модуль DocInWay.pas (карточка накладной).
Например, в модуле , есть UniLines, который отображает данные по услугам из этой накладной. Сами накладные у нас хранятся в таблице DOCS_IN_WAY, а услуги в ней - в таблице DOCS_IN_WAY_SERVS.
Когда мы создаём новую накладную, в ней никаких услуг быть не может, но для инициализации колонок нам всё равно нужно выполнить процедуру Open:
ULF.Open(1581, -1, 'SP_GEN_DOCS_IN_WAY_SERVS_ID', UpTr);
* 1581 - это таг метабъекта, запрос которого выдаёт данные об услугах из накладной;
* -1 - т.к. накладная новая, у нас нет ID записи, которое мы бы могли подставить как условие;
* 'SP_GEN_DOCS_IN_WAY_SERVS_ID' - это название процедуры, которая генерирует ID записей;
* UpTr - записывающая транзакция.
Но когда открывается существующая накладная, мы делаем так:
ULF.Open(1581, ID, 'SP_GEN_DOCS_IN_WAY_SERVS_ID', UpTr);
В качестве ID используется ID текущей накладной, которую мы открываем.
При формировании запроса будет сформировано условие dis.doc_in_way_id = :MAIN_ID, которое оставит только те строки, которые относятся к накладной. В этот :MAIN_ID будет подставляться переданный ID. Это делается за счёт наличия колонки со следующими опциями:
{{:develop:pasted:20220330-055825.png}}
{{:develop:pasted:20220330-060018.png}}
Другой пример, журнал заказов - модуль ZakazPrepHim.pas.
ULF.Open(66, Null, ' ', RdTr, False, WhereSQL, True, ' ', True, False, cbDateKind.ItemIndex);
* 66 - таг метаобъекта с запросом по заказам;
* null - заказы не имеют никакой "аггрегирующей" записи (как было со строками услуг в накладной), поэтому здесь нам ничего никогда не потребуется;
* ' ' - сохранение заказа происходит не средством UniLines, так что в качестве процедуры генерации ID передаём пустую строку;
* RdTr - считывающая транзакция;
* False - отсутствие модальной формы;
* WhereSQL - строка с доп. условием фильтрации заказа. Например, чтобы при первом открытии нам не показывались заказы в отменённом статусе;
* Следующие два True, ' ' неактуальны;
* True - означает, что журнал может быть модифицирован и требуется его обновление;
* False - использование дополнительных условий из параметров колонок метаобъекта 66;
* cbDateKind.ItemIndex - как нужно поменять в запросе местами таблицы для его оптимальной работы. cbDateKind несёт смысловую нагрузку в выборе фильтра - по дате приёма (таблица DOCS), по дате выдачи (DOCS_ORDER) или по дате выезда (MOBILE_PLAN).
====== Обновление данных ======
Обновление данных происходит с помощью процедуры procedure TUniLinesFrame.Refresh. Она принимает следующие параметры:
* Object_ID: variant - аналогично своему параметру из Open, подставляется в :MAIN_ID метазапроса;
* sAddWhere: string = ' ' - аналогично, обычно сюда записываются значения быстрых фильтров;
* FullReload:Boolean = False - очень важный параметр, который влияет на то, будет ли полностью выполняться весь запрос данных метаобъекта (полная перезагрузка), или только те данные, которые могли быть обновлены;
* LoadNewFromMetachanges: boolean = True - позволяет отобразить новые строки данных;
* DontControlLocalSclad: boolean = False - используется при формировании %FILTER_EXPRESSION%, подставляя условие локального склада (актуально для документов);
* const iCastlingTables: Integer = 0 - аналогично такому же параметру в Open;
* AsyncOpenProc: TAsyncOpenProc=nil - аналогично такому же параметру в Open.
Основная логика Refresh такова:
//...предварительные проверки и инициализации
//если у нас не поменялось условие для фильтрации, нам не нужен :MAIN_ID и у нас неполная перезагрузка данных, то:
if (sAddWhere=sLastAddWhere) and (Object_ID=Null) and (not FullReload) and (not IsVisiblePage) then
begin
//получаем список конкретных записей, которые были изменены (добавлены, изменены или удалены)
ids:=GiveMeResultSET('SELECT DISTINCT ITEM_ID FROM META_OBJECTS_CHANGES MOC '+
'WHERE ID>:p1', [FLastLoadId], RdTr);
try
//по каждой записи получаем все произошедшие изменения
while not ids.Eof do
begin
Ch:=GiveMeResultSET('SELECT * FROM META_OBJECTS_CHANGES MOC '+
'WHERE ID>:p1 AND MOC.ITEM_ID=:p2 '+
'ORDER by MOC.change_date,MOC.item_id',
[FLastLoadId, FldAsInt64(ids.FieldByName('ITEM_ID'))], RdTr);
try
WasUpdate:=False;
WasInsert:=False;
WasDels:=False;
while not Ch.Eof do
begin
case Ch.FieldByName('CHANGE_TYPE').AsInteger of
0: WasInsert:=True;
1: WasUpdate:=True;
2: WasDels:=True;
end;
Ch.Next;
end;
//через флаги определяем конечное состояние записи, были ли она в конечном счёте добавлена, обновлена или удалена
if WasInsert and WasUpdate then
WasUpdate:=false;
if WasDels and WasInsert then
begin
WasDels:=False;
WasInsert:=False;
end;
//если запись добавлена или обновлена, то выполнить LoadLine
if WasInsert then
begin
if LoadNewFromMetachanges then
begin
Located:=LocateByPrimaryValue(FldAsInt64(ids.FieldByName('ITEM_ID')), False); //определяем, есть ли у нас эта запись
LoadLine(FldAsInt64(ids.FieldByName('ITEM_ID')), not Located); //<----------
WasChanges;
end;
end
else
begin
if LocateByPrimaryValue(FldAsInt64(ids.FieldByName('ITEM_ID')), True) then
begin
if WasUpdate then
begin
// update line
if Self.Value[PrimaryFieldParsed]=FldAsInt64(ids.FieldByName('ITEM_ID')) then
begin
LoadLine(FldAsInt64(ids.FieldByName('ITEM_ID')), False); //<----------------------
WasChanges;
end;
end
else
//если запись удалена, то удаляем соответствующую ноду из VST
if WasDels then
begin
if Self.Value[PrimaryFieldParsed]=FldAsInt64(ids.FieldByName('ITEM_ID')) then
begin
Self.VST.DeleteNode(Self.VST.FocusedNode);
WasChanges;
end;
end;
end;
end;
finally
ch.Free;
end;
ids.Next;
end;
finally
ids.Free;
end;
FLastLoadId:=FastSQLInt('SELECT * FROM SP_GEN_META_OBJECTS_CHANGES_ID', [])-1;
end
Основная загрузка данных происходит в процедуре LoadLine. В качестве входящих параметров принимает PrimaryKey нужной записи и AddNewLine - флаг, что нам надо добавить новую строку.
PrimaryKey нужен, чтобы в запрос метаобъекта поставить условие по конкретному ID. Поле для условия берётся по флагу Primary Field у колонки метаобъекта.
Происходит это так:
procedure TUniLinesFrame.LoadLine(PrimaryKey: Variant; AddNewLine: Boolean = False);
var
vOldCur: TCursor;
vWhere: String;
begin
//...предварительные обработки
//если запрос ещё не сформирован, то формируем его, добавляя в условие параметр PRIM
if (MetaQuery.SQL = '') then
qQuery.SelectSQL.Text := MetaQuery.GetFullSQL(MetaQuery.PrimaryField +'=:PRIM'+ vWhere, CheckManyToOneLinks)
else
qQuery.SelectSQL.Text := MetaQuery.GetFullSQL(MetaQuery.PrimaryFieldOrigin +'=:PRIM'+ vWhere, False);
//передаём параметр
qQuery.ParamByName('PRIM').Value := PrimaryKey;
qQuery.Open; //формируем датасет
LoadData(PrimaryKey, AddNewLine); //используем уже рассмотренную ранее процедуру по загрузке данных
end;
Т.е. LoadLine, по своей сути, формирует запрос данных, но с таким условием, чтобы получить только одну запись. Разумеется, для нормальной работы соответствующий метаобъект должен быть корректно настроен.
Если же нам требуется полная перезагрузка данных, то тут процесс схож с процедурой Open и только дополнен очищением текущих данных и ссылок, после чего также формируется запрос, формируется датасет и вызывается LoadData.
====== Примеры использования ======
Рассмотрим модуль DocsInWay.pas (журнал накладных).
На журнале расположены кнопки, выступающие в роли быстрой фильтрации журнала:
{{:develop:pasted:20220330-061904.png}}
На нажатие каждой кнопки повешен обработчик, в процессе работы которого формируется некоторая строка sWhere, содержащая в себе необходимое условие для запроса, после чего вызывается
ULF.Refresh(null, sWhere, True);
* null - у нас здесь нет никакой "головной" записи;
* sWhere - сформированое условие для запроса;
* True - выполнение полной перезагрузки данных.
{{:develop:pasted:20220330-062728.png}}
====== Сохранение данных ======
UniLines позволяет легко сохранять имеющиеся данные в базу. Для этого используется процедура Save. Она не принимает в себя никаких параметров. Разберём, как именно она работает:
procedure TUniLinesFrame.Save;
var Node: PVirtualNode;
Dat: PLinesData;
oldCur: TCursor;
prevCurLine: PLinesData;
begin
{... предварительные обработки}
try
//начинаем работать непосредственно с нодами VST, начиная с первой и до последней по очереди
Node:=VST.GetFirst;
while Node<>nil do
begin
//получаем данные, хранящиеся в этой ноде
Dat:=VST.GetNodeData(Node);
curLine:=Dat;
//если состояние LineState имеет значения lnInsert, lsUpdate или lsDelete, то вызываем процедуру OnSaveLine, если она имеется;
if (Dat^.LineState<>lsNone) and (Dat^.LineState<>lsEmpty) then
begin
if Assigned(FOnSaveLine) then
FOnSaveLine(Dat^.LineState);
end;
//Отработка OnSaveLine может поменять состояние текущей строки (например, там может быть запрет на дальнейшие операции путём выставления lsNone).
if (Dat^.LineState<>lsNone) and (Dat^.LineState<>lsEmpty) then
begin
//если состояние не поменялось, то запускается SaveLine
SaveLine;
//после него запускается обработчик OnAfterSaveLine, если он имеется.
if Assigned(FOnAfterSaveLine) then
FOnAfterSaveLine(Dat^.LineState);
end;
Node:=VST.GetNext(Node);
end;
// Помечаем все изменные строки как неизмененные
Node:=VST.GetFirst;
while Node<>nil do
begin
Dat:=VST.GetNodeData(Node);
curLine:=Dat;
if (Dat^.LineState<>lsNone) and not ((Dat^.LineState = lsDelete) or (Dat^.LineState = lsEmpty)) then
begin
Dat^.LineState:=lsNone;
end;
Node:=VST.GetNext(Node);
end;
finally
Screen.Cursor:=oldCur;
curLine:=prevCurLine;
end;
end;
Здесь появляется ещё один важный параметр - LineState, который указывается для данных ноды. В этой статье подробно мы не будем останавливаться, отметим про него, что это - условно "состояние" строки, была ли она добавлена, обновлена или удалена (значение LineState будет принимать соответственно lsInsert, lsUpdate, lnDelete). В остальных случаях она будет приниимать значение lsNone (стандартное значение при загрузке данных) или lsEmpty.
OnSaveLine и OnAfterSaveLine - базово этих обработчиков нет, но они могут быть заданы в рантайме для тех или иных целей. Примеры рассмотрим позже, а пока обратим внимание на самую интересную часть, процедуру SaveLine:
procedure TUniLinesFrame.SaveLine;
var i: integer;
PrimTbl, PrimFld: string;
PrimVal, CurVal: variant;
MetaCol: TMetaColumn;
begin
//если lineState стал lsNone, то ничего не делается;
if LineState=lsNone then
Exit;
{...прочие очистки}
//перебираем список колонок
for i:=0 to Cls.Count-1 do
begin
MetaCol:=GetMetaColumn(i);
//выбираем только те колонки, у которых указана опция Is_Field и не указана Is_LineNumber
if (MetaCol.Is_Field) and (not MetaCol.Is_LineNumber) then
begin
//получаем значение, содержащееся в определенной колонке текущей записи
curVal:=curLine.NodeColList.Items[i];
//если колонка с опцией Is_Primary, текущее значение Null, то генерируем значение ID на основе той процедуры, которую мы передали при Open у текущего UniLines;
if MetaCol.Is_Primary then
begin
if (CurVal=null) then
begin
if (FProcGenID<>'') then
begin
CurVal:=FastSQLVal('select * from '+FProcGenID, []);
curLine.NodeColList.Items[i]:=CurVal;
if curLine.LineState=lsUpdate then
curLine.LineState:=lsInsert;
end;
end;
end;
{...доп. обработки в зависимости от типа значения колонки}
//заполняем массив полей и их значений
sqlFlds.Add(Fields[i]);
sqlVals.Add(CurVal);
end;
end;
{...}
//получаем название таблицы, в которую будем сохранять данные, и название поля с первичным ключом
MetaCol:=MetaQuery.PrimaryColumn;
if MetaCol<>nil then
begin
PrimTbl:=MetaCol.TableName;
PrimFld:=MetaCol.FieldName;
i:=GetMetaColumnIndex(MetaCol);
PrimVal:=curLine.NodeColList.Items[i];
end;
//в зависимости от текущего состояния строки, делаем нужную операцию с базой данных
if (PrimTbl<>'') and (PrimFld<>'') then
case curLine.LineState of
lsInsert:
begin
InsertFromControls(PrimTbl, sqlFlds, sqlVals, RdTr);
end;
lsUpdate:
begin
UpdateFromControls(PrimTbl, PrimFld, PrimVal, sqlFlds, sqlVals, RdTr);
end;
lsDelete:
begin
DeleteFromControls(PrimTbl, PrimFld, PrimVal, RdTr);
end;
end
else
raise Exception.Create('Primary key for '+MetaObject.ObjName+' not found');
end;
Обратим внимание, что коммита здесь не происходит, его надо выполнять отдельно. Также, если не нашли Primary-колонку, будет выдан эксэпшн.
====== Примеры работы ======
Рассмотрим простой пример сохранения, в котором опишем полную последовательность шагов, что за чем происходит. Обратим внимание на модуль PlanMobileItem.pas - карточка выезда в журнале выездов. В качестве сохраняемого UniLines здесь используется таблица с перечнем заказов, по которому должен быть выезд.
Открытие Unilines:
ULF.Open(5758, -1, 'SP_GEN_MOBILE_PLAN_ORDERS_ID', UpTr, False);
{{:develop:pasted:20220330-065208.png}}
{{:develop:pasted:20220330-065244.png}}
Здесь есть колонка "ВнНомер", с опциями Primary, Is_Field. В качестве поля используется ID из таблицы MOBILE_PLAN_ORDERS.
Заполнение этой таблицы данными делается в процессе работы пользователя, механизмы заполнения рассмотрим в другой статье.
При закрытии карточки выезда вызывается ULF.Save, и далее по шагам:
- Мы начинаем проверять каждую ноду данных;
- Для каждой ноды, которая была добавлена, изменена или удалена, мы проверяем обработчик OnSaveLine. Но он у нас не указан, так что мы ничего мы не делаем;
- Повторно проверяем состояние ноды, и вызываем SaveLine, если она также добавлена, изменена или удалена;
- В SaveLine мы проверяем все колонки, имеющие признак Is_Field и не имеющие Is_LineNumber. У нас это будут ВнНомер, ВнНомерПлана, ВнНомерЗаказа;
- Для колонки ВнНомер т.к. она является Primary, проводим доп. проверку - если в текущей ноде она значение null, то сгенерируем ей значение на основе процедуры SP_GEN_MOBILE_PLAN_ORDERS_ID, которая была указана при открытии метаобъекта;
- Все перечисленные колонки добавляются в список названий полей sqlFlds, а их значения - в массив sqlVals;
- На основе данных из колонки ВнНомер мы получаем, что нам нужно добавить/изменить/удалить запись в таблице mobile_plan_orders, в качестве ключа используем ID.
- После операций с БД проверяется наличие обработчика OnAfterSaveLine, так что ничего больше не происходит;
- При закрытии формы происходит коммит в БД.
[[https://doc.agb.is/internal/work_whith_meta|Полезно - работа с Метой]]
[[https://forms.gle/F8XqnX6MdGa7ynqZ7|Пройти тест]]
[[https://doc.agb.is/develop/unilines|Назад]]