Пример применения SQL DSO
В статье «Введение в OLAP, часть 11. Применение SQL DSO для создания серверных OLAP-кубов», которую вы сможете найти в нашем журнале, обсуждается использование Decision Support Objects для решения таких задач, как обновление данных в уже существующих OLAP-кубах, создание и удаление многомерных баз данных и содержащихся в них объектов — кубов, коллективных измерений, источников данных.
Как следует из представленного в этой статье материала, с помощью SQL DSO можно создавать приложения, реализующие всю функциональность Analysis Manager, и даже обладающие большими возможностями. Однако на практике DSO используют для создания средств управления OLAP-кубами, имеющих ограниченную функциональность по сравнению с Analysis Manager. В то время как администратор OLAP-сервера, как правило, создает, модифицирует, обновляет кубы и применяет для этой цели Analysis Manager, конечный пользователь обычно только просматривает кубы и обновляет их содержимое. Сам по себе просмотр OLAP-кубов не требует применения DSO (это можно сделать с помощью ADO или ADO MD), но при обновлении данных без DSO не обойтись.
В настоящей статье вашему вниманию предлагается один из примеров применения SQL DSO. Это будет небольшая утилита, позволяющая конечным пользователям просматривать метаданные OLAP-кубов, свойства их измерений и уровней, выполнять простейшие MDX-запросы и, при необходимости, обновлять данные кубов, но не создавать кубы и не модифицировать их метаданные. При этом пользователь не обязан знать, какие из объектов многомерной базы данных являются коллективными или виртуальными.
Как и другие приложения подобного класса, наша утилита будет содержать простое дерево, отображающее иерархию объектов DSO — баз данных, кубов, измерений, уровней, а также компонент TMemo для отображения списка свойств, список ключевых слов MDX и компонент TDBGrid для отображения результатов выполнения MDX-запросов. Кроме того, мы реализуем в ней возможность копировать имена ветвей дерева объектов DSO и ключевые слова MDX из списка в редактор MDX-запросов.
Для решения этой задачи создадим новый проект и разместим на главной форме следующие компоненты: TToolBar с несколькими кнопками, компоненты TEdit и ЕDBNavigator, компоненты TMainMenu, TTreeView, TListBox, TDBGrid, TADOConnection, TADODataSet, TDataSource, два компонента TMemo. Установим свойство DataSource компонента DBGrid1 равным DataSource1, свойство Connection компонента ADODataSet1 равным ADOConnection1, а свойство DataSet компонента DataSource1 равным ADODataSet1. В компонент ListBox1 поместим список наиболее распространенных ключевых слов языка MDX, таких как CHILDREN, AS, SET, MEMBERS, DESCENDANTS и др.
При работе с нашим приложением пользователь должен в первую очередь соединиться с OLAP-сервером, расположенным на компьютере, имя которого указано в компоненте Edit1:
dsoServer:=CreateOleObject('DSO.Server'); dsoServer.Connect(ServerNameEdit.Text); TreeView1.Items.Clear; RootNode := TreeView1.Items.Add(nil, dsoServer.Name); RootNode.ImageIndex:=9; . . .
Здесь и далее мы опускаем фрагменты кода, несущественные с точки зрения применения DSO и реализующие исключительно пользовательский интерфейс приложения, а также подробное описание пунктов меню приложения. Все эти подробности вы сможете найти в исходном тексте приложения — он находится на этом же компакт-диске.
Отображение иерархии объектов DSO реализовано в обработчике событий OnMouseDown компонента TreeView1. Здесь с помощью свойства Level мы определяем, чему соответствует ветвь, по которой щелкнул мышью пользователь (базе данных, кубу, измерению, уровню), и получены ли уже ее дочерние ветви из многомерной базы данных. Если они еще не получены (то есть свойство Count данной ветви равно нулю), мы обращаемся к базе данных, получаем сведения о дочерних объектах и создаем дочерние ветви, используя свойство Name соответствующего объекта SQL DSO:
HitTest := TreeView1.GetHitTestInfoAt(X,Y); //проверяем, выбрана ли одна из ветвей 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 //ветвь представляет сервер 0: begin for I:=1 to dsoServer.MDStores.Count do begin dsodb := dsoServer.MDStores.Item(I); MDStoreNode := TreeView1.Items.AddChild(RootNode, dsoDB.Name); MDStoreNode.ImageIndex:=1; end; end; //ветвь представляет базу данных 1: begin SetDBParams; RefreshButton.Visible:=False; //получаем имена кубов базы данных for I := 1 to dsodb.Cubes.Count do begin dsoCube :=dsodb.Cubes.Item(i) ; CubeNode := TreeView1.Items.AddChild(CurrNode, dsoCube.Name); CubeNode.ImageIndex:=3; end; end; //ветвь представляет куб 2: begin SetDBParams; SetCubeParams; for I := 1 to dsoCube.Dimensions.Count do begin dsoDim := dsoCube.Dimensions.Item(I); DimNode := TreeView1.Items.AddChild(CurrNode, dsoDim.Name); DimNode.ImageIndex:=4; end; end; //ветвь представляет измерение 3: begin SetDBParams; SetCubeParams; dsoDim:=dsoCube.Dimensions.Item(CurrNode.Text); RefreshButton.Visible:=False; //получаем уровни иерархии измерения for I := 1 to dsoDim.Levels.Count do begin dsoLevel := dsoDim.Levels.Item(i); LevelNode := TreeView1.Items.AddChild(CurrNode, dsoLevel.Name); LevelNode.ImageIndex:=8; end; end; end; end . . .
Если из базы данных уже больше нечего получить (дочерние ветви получены либо их нет), и мы работаем с MDX-запросами, копируем имя ветви в компонент Memo1, начиная с текущей позиции курсора:
//если дочерние ветви получены //и мы выполняем запросы, //копируем имя ветви в редактор MDX-запросов if Query1.Enabled then begin //если это не корневая ветвь if Currnode.Level>0 then begin SetDBParams; if Currnode.Level>1 then SetCubeParams; CurrNode:=TreeView1.GetNodeAt(X, Y); NodeName:=CurrNode.Text; //копируем в редактор MDX-запросов имя объекта в соответствии с
//синтаксисом MDX
if (CurrNode.Level<3) then AddString:='['+NodeName +']' else AddString:='['+NodeName +'].'; Memo1.SetSelTextBuf(PChar(AddString)); end; end;
Наконец, если нам нечего получить из базы данных, и мы занимаемся не выполнением запросов, а просмотром метаданных, мы просто отобразим их в компоненте Memo2. Кроме того, в этом случае мы предоставим пользователю возможность обновить содержимое физических OLAP-кубов:
//если дочерние ветви уже получены //и мы обновляем кубы и просматриваем метаданные, //отобразим свойство объекта if Action1.Enabled then begin case CurrNode.Level of //ветвь представляет сервер 0: begin Memo2.Lines.Clear; end; //ветвь представляет базу данных 1: begin SetDBParams; Memo2.Lines.Add('Data Sources:'+CHR(13)+CHR(10)); for I:=1 to dsoDB.DataSources.Count do begin dsoDS:= dsoDB.DataSources.Item(i); Memo2.Lines.Add (dsoDS.ConnectionString + CHR(13)+CHR(10)); end; end; //ветвь представляет куб 2: begin SetDBParams; SetCubeParams; Memo2.Lines.Add('Measures: '); //отображаем список измерений и другие свойства куба for i:=1 to dsoCube.Measures.Count do Memo2.Lines.Add(' ' + dsoCube.Measures.Item(i).Name); Memo2.Lines.Add(CHR(13)+CHR(10)); If dsoCube.IsVirtual then begin Memo2.Lines.Add ('This cube is virtual'); RefreshButton.Enabled:=False; RefreshCube1.Enabled:=False; end else begin Memo2.Lines.Add('Source Table: '+dsoCube.SourceTable+CHR(13)+CHR(10)); Memo2.Lines.Add('Source Table Filter: '+ dsoCube.SourceTableFilter+CHR(13)+CHR(10)); Memo2.Lines.Add('Join Clause: '+ dsoCube.JoinClause+CHR(13)+CHR(10)); Memo2.Lines.Add('From Clause: '+ dsoCube.FromClause+CHR(13)+CHR(10)); RefreshButton.Enabled:=True; end; end; 3: begin SetDBParams; SetCubeParams; SetDimParams; //отображаем список свойств измерения Memo2.Lines.Add('Source Table: '+ dsoDim.SourceTable+CHR(13)+CHR(10)); Memo2.Lines.Add('Source Table Filter: '+ dsoDim.SourceTableFilter+CHR(13)+CHR(10)); Memo2.Lines.Add('Join Clause: '+ dsoDim.JoinClause+CHR(13)+CHR(10)); If dsoDim.IsShared then Memo2.Lines.Add('This dimension is shared'+CHR(13)+CHR(10)); end; 4: begin SetDBParams; SetCubeParams; SetDimParams; end; end; end;
SetDBParams, SetCubeParams и SetDimParams представляют собой процедуры, устанавливающие текущие имена базы данных, куба и измерения, когда пользователь выбирает ветвь дерева:
procedure TForm2.SetDBParams; begin case CurrNode.Level of 1: dsodb := dsoServer.MDStores.Item(WideString(CurrNode.Text)); 2: dsodb := dsoServer.MDStores.Item(WideString(CurrNode.Parent.Text)); 3: dsodb := dsoServer.MDStores.Item(WideString(CurrNode.Parent.Parent.Text)); 4: dsodb := dsoServer.MDStores.Item(WideString(CurrNode.Parent.Parent.Parent.Text)); end; StatusBar1.Panels.Items[1].Text:='Database: '+dsodb.Name; StatusBar1.Panels.Items[2].Text:=''; CurrentDBName:=dsodb.Name; Memo2.Lines.Clear; end; procedure TForm2.SetCubeParams; begin case CurrNode.Level of 2: dsoCube:=dsodb.Cubes.Item(CurrNode.Text); 3: dsoCube:=dsodb.Cubes.Item(CurrNode.Parent.Text); 4: dsoCube:=dsodb.Cubes.Item(CurrNode.Parent.Parent.Text); end; CurrentCubeName:=dsoCube.Name; RefreshButton.Visible:=True; StatusBar1.Panels.Items[2].Text:='Cube: '+dsoCube.Name; Refreshcube1.Enabled:=True; Memo2.Lines.Clear; end; procedure TForm2.SetDimParams; begin case CurrNode.Level of 3: dsoDim:=dsoCube.Dimensions.Item(CurrNode.Text); 4: dsoDim:=dsoCube.Dimensions.Item(CurrNode.Parent.Text); end; // CurrentDimName:=dsoDim.Name; RefreshButton.Visible:=False; Refreshcube1.Enabled:=False; Memo2.Lines.Clear; end;
Процедура обработки куба выглядит примерно так же, как это описано в статье «Введение в OLAP, часть 11. Применение SQL DSO для создания серверных OLAP-кубов», поэтому мы не будем повторять этот фрагмент кода.
Результат просмотра метаданных с помощью созданного приложения изображен на рис. 1.
В обработчике события OnClick компонента ListBox1 мы должны скопировать выбранное пользователем ключевое слово языка MDX в компонент Memo1:
AddString:=Listbox1.Items[Listbox1.ItemIndex]+' '; Memo1.SetSelTextBuf(PChar(AddString));
Для выполнения MDX-запроса следует соединиться с базой данных OLAP того же самого сервера с помощью ADO, установить свойство CommandText компонента ADODataSet1 равным содержимому компонента Memo1 и выполнить его:
procedure TForm2.ExecuteButtonClick(Sender: TObject); begin try ADOCOnnection1.Connected:=False; // ADO Connection String DS:='Provider=MSOLAP.2; Data Source='+ ServerNameEdit.Text+'; Initial Catalog='+ CurrentDBName; ADOCOnnection1.ConnectionString:=DS; //Выполняем MDX-запрос ADODataSet1.CommandText:=Memo1.Text; ADOConnection1.Connected:=True; except ShowMessage('Invalid login, password, database or server name'); end; try ADODataSet1.Open; except ShowMessage('Invalid MDX query'); end; end;
Обратите внимание на то, что открытие подобных наборов данных вызывает исключение, однако это исключение не влияет на выполнение остального кода приложения. Причины такого поведения, похоже, пока никому не известны.
Чтобы получить вместо строк '(MEMO)' в компоненте DBGrid1 значения полей набора данных, получаемого в результате выполнения MDX-запроса, создадим обработчик события OnDrawColumnCell :
DBGrid1.Canvas.FillRect(Rect); DBGrid1.Canvas.TextOut(Rect.Left+2,Rect.Top+2, Column.Field.AsString);
Результат выполнения MDX-запроса с помощью созданной утилиты представлен на рис. 2.
Отметим, однако, что для выполнения MDX-запросов можно использовать не только ADO, но и ADO MD. Примеры применения ADO MD вы сможете найти в статьях «Введение в OLAP, часть 9» (КомпьютерПресс № 12’2001) и «Введение в расширения ADO» (КомпьютерПресс № 10’2000).
Таким образом, мы создали утилиту, позволяющую просматривать метаданные многомерных баз данных Microsoft SQL Server Analysis Services и обновлять содержимое кубов с помощью SQL DSO, а также выполнять MDX-запросы. И, как видите, это не слишком сложно.
КомпьютерПресс 2'2002