Delphi и расширения ADO - Создание утилиты просмотра 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 с именами полей, равными свойству CaptionMembers, являющихся свойствами коллекции 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, который можно расширить, например путем добавления бизнес-графики или более «интеллектуального» генератора запросов.