====== Взаимодействие 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|Назад]]