Borland Delphi и расширения ADO
Страница 8. Создание утилиты просмотра OLAP-кубов


 

Создание утилиты просмотра OLAP-кубов

 

Выясним, как объекты ADO MD можно использовать в Delphi. В этих целях создадим приложение, с помощью которого пользователь сможет:

  • просматривать метаданные многомерной базы данных в виде иерархической структуры;
  • копировать имена объектов многомерной базы данных и ключевые слова MDX из списка, заранее заданного в редактор MDX-запросов;
  • выполнять MDX-запрос и копировать результаты в набор данных (компонент TClientDataSet) с целью представления их в компоненте TDBGrid.

Для этого создадим новый проект и поместим его главную форму будущего приложения компонента TToolBar с несколькими кнопками, TTreeView, TListBox, TDBGrid, TClientDataSet, TDataSource и TMemo. Затем установим значение свойства DataSource компонента DBGrid1 равным DataSource1, а значение свойства DataSet компонента DataSource1 равным ClientDataSet1. Компонент ListBox1 следует заполнить ключевыми словами MDX, такими как CHILDREN, MEMBERS, DESCENDANTS , и др.

Далее следует сослаться в нашем приложении на библиотеку типов ADO MD, содержащуюся в файле MSADOMD.DLL, так как ADO MD не поддерживается в Delphi 5 на уровне компонентов. Для этого следует выбрать пункт Project | Import Type Library из главного меню среды разработки, а затем выбрать Microsoft ActiveX Data Objects (Multi-dimensional) 1.0 Library из списка доступных библиотек типов. Обратите внимание на то, что если вы уже импортировали библиотеку типов ADOX и не переименовали класс Delphi для объекта ADOX Catalog, то класс Delphi TCatalog окажется уже определенным. В этом случае во избежание конфликта имен можно переименовать TCatalog в TADOMDCatalog. Желательно также убедиться, что опция Generate Component Wrapper не выбрана, так как нам нужно создать только *.pas-файл для доступа к объектам ADO MD. Далее можно нажать кнопку Create Unit, что приведет к созданию файла ADOMD_TLB.PAS, представляющего собой интерфейсный модуль к библиотеке типов ADO MD. Наконец, нам нужно включить ссылку на этот файл в предложение Uses, так же как и ссылку на модули ComObj и ADODB .

Следующий шаг в нашем приложении — соединение с многомерной базой данных. Параметр ConnectionString, используемый для этой цели, должен ссылаться на OLE DB Provider for OLAP Services (стоит убедиться, что он действительно установлен), а также на имя компьютера и имя базы данных, например:

 DS := 'Provider=MSOLAP.1;Data Source=localhost; Initial Catalog=FoodMart';

Можно также соединиться с локальными кубами, созданными с помощью Microsoft Excel и сохраненными в файлах *.cub. В этом случае параметр Connection String может выглядеть так:

 DS := 'Provider=MSOLAP.1;Data Source=C:\Data\Cubes\NW1.cub';

Код, отвечающий за соединение с базой данных, можно поместить в обработчик события OnClick одной из кнопок. Этот код, а также процедура, заполняющая компонент TreeView1 именами кубов, представлены ниже:

 procedure TForm1.Button1Click(Sender: TObject);
begin
DS := 'Provider=MSOLAP.1;Data Source=localhost;'+
          'Initial Catalog=FoodMart';
FillTreeView(DS);
end;
procedure TForm1.FillTreeView(DataSource: WideString);
var
I : Integer;
begin
//Создадим новый объект Catalog
Catalog1 := CoCatalog.Create;
TreeView1.Items.Clear;
RootNode := TreeView1.Items.Add(nil, 'Catalog');
//Соединимся с многомерной базой данных
Catalog1._Set_ActiveConnection(OleVariant(DataSource));
//Последовательно получим имена всех кубов в базе данных
for I := 0 to Catalog1.CubeDefs.Count-1 do
begin
   CubeDef1 := Catalog1.CubeDefs[I] as CubeDef;
   CubeDefNode := TreeView1.Items.AddChild(RootNode, CubeDef1.Name);
end;
end;

Здесь мы соединяемся с базой данных, создаем объект Catalog, просматриваем по очереди все элементы его коллекции CubeDefs и извлекаем имена кубов (они содержатся в свойстве Name объектов CubeDef ).

Реальная обработка метаданных куба реализована в обработчике события OnMouseDown компонента TreeView1:

 procedure TForm1.TreeView1MouseDown(Sender: TObject; Button: TMouseButton;
                                                      Shift: TShiftState; X, Y: Integer);
var   
       HitTest    : THitTests;
       CurrNode   : TTreeNode;
       I             : integer;
       NodeName   : string;
       AddString : String;
begin
HitTest := TreeView1.GetHitTestInfoAt(X,Y);
//Если пользователь щелкнул мышью на одной из ветвей TTreeView
if (htOnItem in HitTest) then
begin
   CurrNode := TreeView1.GetNodeAt(X, Y);
   //Если у ветви могут быть дочерние ветви, но они еще не добавлены
   //добавим их
   if ((CurrNode.Count=0) and (CurrNode.Level<4)) then
   begin
   case CurrNode.Level of
   //Эта ветвь представляет куб
   1: begin
         CubeDef1 := Catalog1.CubeDefs.Get_Item(CurrNode.Text);
         //Получаем имена всех размерностей куба
         for I := 0 to CubeDef1.Dimensions.Count-1 do
         begin
          Dimension1 := CubeDef1.Dimensions[I] as Dimension;
          DimNode      := TreeView1.Items.AddChild(CurrNode, Dimension1.Name);
         end;
       end;
   //Эта ветвь представляет размерность
   2: begin
         CubeDef1    := Catalog1.CubeDefs.Get_Item(CurrNode.Parent.Text);
         Dimension1 := CubeDef1.Dimensions.Get_Item(CurrNode.Text);
         //Получаем имена всех уровней иерархии данной размерности
         for I := 0 to Dimension1.Hierarchies[0].Levels.Count-1 do
         begin
          Level1      := Dimension1.Hierarchies[0].Levels[i] as Level;
          LevelNode := TreeView1.Items.AddChild(CurrNode, Level1.Name);
         end;
       end;
   //Эта ветвь представляет уровень иерархии
   3: begin
         CubeDef1    := Catalog1.CubeDefs.Get_Item(CurrNode.Parent.Parent.Text);
         Dimension1 := CubeDef1.Dimensions.Get_Item(CurrNode.Parent.Text);
         Level1       := Dimension1.Hierarchies[0].Levels.Get_Item(CurrNode.Text);
         //Получаем имена всех членов данного уровня иерархии
         for I := 0 to Level1.Members.Count-1 do
         begin
          Member1      := Level1.Members[I] as Member;
          MemberNode := TreeView1.Items.AddChild(CurrNode, Member1.Name);
         end;
       end;
      end;
    end
    else
    //Если данная ветвь уже имеет дочерние ветви (или их не должно быть),
    //скопируем имя объекта в редактор MDX-запросов
    begin
      //Если ветвь не корневая
      if Currnode.Level>0 then
      begin
       CurrNode := TreeView1.GetNodeAt(X, Y);
       NodeName := CurrNode.Text;
       //Копируем имя ветви, сформатированное в соответствии
       //с синтаксисом MDX, в редактор MDX-запросов
       if ((CurrNode.Level=1) or (CurrNode.Parent.Parent.Text='Measures'))
       then AddString:='['+NodeName +']'
       else AddString:='['+NodeName +'].';
       Memo1.SetSelTextBuf(PChar(AddString));
    end;
   end;
end;
end;
procedure TForm1.ListBox1Click(Sender: TObject);
var AddString:string;
begin
//Добавим ключевое слово MDX из списка в редактор MDX-запросов
AddString := Listbox1.Items[Listbox1.ItemIndex]+' ';
Memo1.SetSelTextBuf(PChar(AddString));
end;

Здесь мы определяем, что именно представляет ветвь, на которой пользователь щелкнул мышью (куб, размерность, уровень иерархии, член уровня), используя ее свойство Level, а также выясняем, имеются ли уже у нее дочерние ветви. Если дочерние ветви отсутствуют (свойство Count данной ветви равно нулю), мы обращаемся к базе данных и создаем соответствующие дочерние ветви, используя свойство Name соответствующего объекта ADO MD. Если же из базы данных уже нечего загружать, мы копируем имя объекта, представленного данной ветвью, в компонент Memo1, в то место, где находится курсор.

Код для копирования ключевых слов MDX в компонент Memo1 приведен в этом же фрагменте кода. Таким образом, мы получили инструмент для просмотра метаданных куба и создания текста MDX-запросов с помощью щелчков мыши на ветвях дерева объектов ADO MD на элементах списка ключевых слов.

Следующий шаг в создании OLAP-клиента заключается в выполнении MDX-запроса, содержащегося в компоненте Memo1, и в заполнении компонента TClientDataSet его результатами. Эта функциональность реализована в процедуре CDSFill, приведенной ниже:

 procedure TForm1.Button2Click(Sender: TObject);
begin
CDSFill(DS);
end;
procedure TForm1.CDSFill(DataSource: WideString);
var   
I,J : Integer;
V    : OleVariant;
begin
//Создадим новый объект CellSet
CellSet1 := CoCellSet.Create;
try
//Выполним MDX-запрос, содержащийся в компоненте Memo1,
//и откроем объект CellSet
CellSet1.Open(Memo1.Text,DataSource);
with ClientDataSet1 do
begin
   Close;
   with FieldDefs do
   begin
    //Уничтожим все определения полей в ClientDataset
    Clear;
    //Добавим новые определения полей
    //Первое поле нужно для ранения имен строк
    with AddFieldDef do
    begin
      Name := 'Rows';
      DataType := ftString;
    end;
    //Перебираем коллекцию Positions первой оси
    for I := 1 to CellSet1.Axes[0].Positions.Count do
    begin
      with AddFieldDef do
      begin
      //Значение поля исходной базы данных станет именем колонки
       Name :=CellSet1.Axes[0].Positions[I-1].Members[0].Caption+
      //Имена колонок в наборах данных должны быть уникальны, поэтому
      //добавим уникальное число, содержащееся в свойстве Ordinal
      //объекта Position, к значению поля
      ' ('+IntToStr(CellSet1.Axes[0].Positions[I-1].Ordinal) +')';
      DataType := ftFloat;
      end;
    end;
   end;
   //Создаем и открываем ClientDataSet
   CreateDataSet;
   Open;
   //Добавляем к нему записи
   for J:=1 to CellSet1.Axes[1].Positions.Count   do
   begin
    //Добавляем запись
    Append;
    //Добавляем имя строки, используя коллекцию Position второй оси
    Fields[0].Value :=   CellSet1.Axes[1].Positions[J-1].Members[0].Caption;
    //Перебираем ячейки в строке, извлекая из них данные
    for I := 1 to CellSet1.Axes[0].Positions.Count do
    begin
      //Создаем массив координат ячеек
      V:=VarArrayCreate([0,1], varVariant);
      V[0] := I-1;
      V[1] := J-1;
      //Если соответствующая ячейка в CellSet не пуста,
      if CellSet1.Item[PSafeArray(TVarData(V).VArray)].FormattedValue <> ''
      then
       //Значение поля будет равно значению в ячейке
       Fields[I].Value := Cellset1.Item[PSafeArray(TVarData(V).VArray)].Value
      else
       //иначе поместим в поле нулевое значение
       ClientDataSet1.Fields[I].Value:=0;
    end;
   end;
   //Закрываем Cellset и высвобождаем ресурсы
   CellSet1.Close;
   CellSet1 := nil;
end;
except
   ShowMessage('Invalid MDX Query');
end;
end;

В данном фрагменте кода мы создаем объект CellSet и используем его метод Open. Если MDX-запрос корректен, будет создан пустой набор данных типа TClientDataset с именами полей, равными свойству Caption первых элементов коллекций Members, являющихся свойствами коллекции Positions первой оси (Axis[0]) .

Обратите внимание на то, что имена полей в наборах данных должны быть уникальны. Однако в реальных многомерных базах данных свойство Caption членов коллекции Members таковым не является. Например, в иерархии Year/Month свойство Caption для членов коллекции Members January 1999 и January 2000 будет равно одному и тому же значению January. Существует много способов избежать дублирования имен полей, и в данном примере мы использовали самый простой — добавление уникального числа, содержащегося в свойстве Ordinal объекта Member .

После того как имена полей компонента ClientDataset1 определены, выполняется цикл перебора строк объекта CellSet, и для каждого ряда мы устанавливаем значение первого поля равным свойству Caption первого элемента коллекции Members соответствующего члена коллекции Positions второй оси (Axis[1]), а затем помещаем значения из соответствующих объектов Cell (доступных с помощью коллекции Item объекта CellSet) в оставшиеся поля. В результате получается набор данных, заполненный двухмерным сечением куба и отображенный в компоненте DBGrid1.

Итак, мы создали простейшее приложение для просмотра OLAP-кубов и выполнения MDX-запросов, используя ADO MD. Это лишь элементарный пример для иллюстрации возможностей ADO MD, который можно расширить, например путем добавления бизнес-графики или более «интеллектуального» генератора запросов.

 
« Предыдущая статья   Следующая статья »