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

Подытожим последовательность происходящих действий:

  1. TUniLinesFrame.Open - основная процедура, которую используем извне.
  2. Формируется датасет на основе результата запроса метаобъекта;
  3. Вызывается FillColumns;
  4. FillColumns определяет название каждой колонки метаобъекта и затем для них вызывает FillColumn;
  5. FillColumn добавляет колонку в VST, название колонки в Fields и элемент в Cls;
  6. После отработки FillColumns вызывается LoadData;
  7. LoadData вызывает AddData для каждой строки сформированного датасета;
  8. AddData добавляет ноду в VST, создаёт для неё NodeColList и заполняет его null-ами по числу колонок;
  9. Затем в цикле происходит перебор всех названий колонок и заполнение 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. Это делается за счёт наличия колонки со следующими опциями:

Другой пример, журнал заказов - модуль 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 (журнал накладных).

На журнале расположены кнопки, выступающие в роли быстрой фильтрации журнала:

На нажатие каждой кнопки повешен обработчик, в процессе работы которого формируется некоторая строка sWhere, содержащая в себе необходимое условие для запроса, после чего вызывается

ULF.Refresh(null, sWhere, True);

  • null - у нас здесь нет никакой «головной» записи;
  • sWhere - сформированое условие для запроса;
  • True - выполнение полной перезагрузки данных.

Сохранение данных

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);

Здесь есть колонка «ВнНомер», с опциями Primary, Is_Field. В качестве поля используется ID из таблицы MOBILE_PLAN_ORDERS.

Заполнение этой таблицы данными делается в процессе работы пользователя, механизмы заполнения рассмотрим в другой статье.

При закрытии карточки выезда вызывается ULF.Save, и далее по шагам:

  1. Мы начинаем проверять каждую ноду данных;
  2. Для каждой ноды, которая была добавлена, изменена или удалена, мы проверяем обработчик OnSaveLine. Но он у нас не указан, так что мы ничего мы не делаем;
  3. Повторно проверяем состояние ноды, и вызываем SaveLine, если она также добавлена, изменена или удалена;
  4. В SaveLine мы проверяем все колонки, имеющие признак Is_Field и не имеющие Is_LineNumber. У нас это будут ВнНомер, ВнНомерПлана, ВнНомерЗаказа;
  5. Для колонки ВнНомер т.к. она является Primary, проводим доп. проверку - если в текущей ноде она значение null, то сгенерируем ей значение на основе процедуры SP_GEN_MOBILE_PLAN_ORDERS_ID, которая была указана при открытии метаобъекта;
  6. Все перечисленные колонки добавляются в список названий полей sqlFlds, а их значения - в массив sqlVals;
  7. На основе данных из колонки ВнНомер мы получаем, что нам нужно добавить/изменить/удалить запись в таблице mobile_plan_orders, в качестве ключа используем ID.
  8. После операций с БД проверяется наличие обработчика OnAfterSaveLine, так что ничего больше не происходит;
  9. При закрытии формы происходит коммит в БД.

Полезно - работа с Метой

Пройти тест

Назад