Статья
Версия для печати
Обсудить на форуме
Delphi и Interbase. Часть I, она же вводная.




Лирическое отступление, или "Что это будет"


Всем привет! Будет это, видимо, цикл статей о принципах построения баз на IB и его клонах. Цикл потому, что уложится в несколько листов по этой теме невозможно в принципе, а рассмотреть хотелось бы множество вещей, которых нет ни в справках, ни в литературе, причём придя к пониманию этих вещей постепенно, начиная с самых простых азов. Ну а финалом будет написание полноценной сетевой программы. Сразу оговорюсь, что "по ходу пьесы" придётся рассматривать множество вопросов, необходимых для осмысленной работы, и IB (а зачастую - и Delphi) собственно, не касающихся. Наберитесь терпения, если вы этого не знаете (потом всё равно пригодится :)) или пропустите, если вам это и так понятно.

Сначала несколько слов о том, почему же всё-таки IB. Главная причина в его доступности (в т.ч. - бесплатности), так  как всё-таки этот материал задумывается как учебный, а покупать в целях обучения лицензионный сервак типа Oracle или учиться на ворованной копии - как-то глупо. Во-вторых, при всей своей простоте, IB представляет собой полноценную реляционную СУБД, занимающую не последнее место на рынке. Переход с IB на любой другой промышленный сервер будет достаточно безболезненным, т.к. вы обнаружите там те же самые хранимые процедуры, триггера, просмотры с практически тем же синтаксисом. Есстественно, будут и отличия, но не столько отличия, сколько расширения языка, свои для каждой СУБД - основные принципы работы останутся неизменными.

Для работы потребуется собственно, Delphi и Interbase/Firebird. Я бы советовал FireBird, как бесплатный и лицензированный для коммерческого использования. К тому же по сравнению с IB, глюков в нём всё-таки было больше исправлено, чем добавлено. Настоятельно рекомендуется скачать IBExpert. Я практически не буду упоминать его в статьях, но поскольку сей продукт стал стандартом de-facto у русскоязычных разработчиков (для  которых он бесплатен), вы намного упростите себе жизнь в дальнейшем. Элементарные вещи типа "как поставить на форму TDatasource" также обсуждаться не будут. Базы данных - не та тема, с которой стоит начинать учиться программированию, и если вы не знаете Delphi вообще - читать это не рекомендуется. Но тем не менее, материал ориентирован на новичков, и все примеры будут разбираться буквально по шагам. Скачать всё необходимое можно здесь: Interbase, Firebird, IB Expert. Лучший, на мой взгляд, русскоязычный сайт по предлагаемой теме находится здесь.
По ходу работы я буду составлять что-то типа словарика, в котором будут описываться используемые компоненты. Поскольку сходу перевести всю справку по IBX невозможно, делаться это будет постепенно, поэтому не удивляйтесь, если какие-то ссылки по началу работать не будут. Если они не работают, то скорее всего это будет означать, что в статьях до этих компонентов мы попросту ещё не дошли, в своё время работать будет всё :)

И ещё одна, последняя, но важная, оговорка. Многие вещи, которые будут здесь обсуждаться, в т.ч. примеры, могут быть реализованы несколькими способами. Я буду ориентироваться исключительно на способы программные. Например, в следующем разделе речь пойдёт о создании первой базы данных. Это делается менее, чем за минуту в том же IbExpert'е, но мы всё это сделаем руками с помощью написанной специально для этого программы. Кому-то это может показаться "китайским коммунизмом", но, как показал опыт, человек, который чётко знает как, с какими параметрами создаётся база в коде, гораздо быстрее разберётся, как эту базу создать в любой оболочке. А вот обратное утверждение, как правило, не верно. Если вы всю жизнь тыкали кнопки в оболочке, а потом вдруг столкнулись с необходимостью создавать базу в runtime, скорее всего, у вас будут затруднения. Подобный принцип будет исповедоваться на протяжении всего цикла статей.


Подготовка к первому свиданию


Собственно, установка проблем представлять не должна: вы просто запускаете скаченный setup и всё, что требуется, будет установленно. Теперь где-то на диске у вас сидит каталог с СУБД, и вам нужно создать базу, подключиться к ней, и, любопытства ради, глянуть, а что же там есть. (Думаете, ничего нет? Ошибаетесь!)
Запустите Delphi, создайте новый проект, и сохраните его на диск. Начиная с версии 5.0 в состав Delphi входит уже упоминавшийся набор компонентов IBX, или Interbase Express, позволяющих работать с Interbase и его клонами через родное api, а не ODBC-подключения. Мы будем пользоваться именно ими, и в данный момент нас будет интересовать компонент
TIbDatabase. На этой ссылке вы сможете вкратце ознакомиться с тем, что он умеет. Не переживайте, если вдруг вам ничего не понятно, всё в своё время :)
На форму поместите Toolbar, ActionList и ImageList. (при желании можно обойтись просто одной кнопкой на форме, но я бы всё-таки советовал делать изначально осмысленный интерфейс, с тем, чтобы в будущем его можно было безболезненно наращивать). Свяжите между собой эти компоненты установкой свойства Images у ActionList и ToolaBar'а, и воткните в ImageList картинку, которая, по вашему мнению, соответствует процессу создания базы данных.

Я бы также советовал переименовать компоненты. Как - дело вкуса. Например, у меня все названия начинаются с одной или нескольких букв, отражающих тип компонента, а остальные указывают на предназначение этого компонента. Например, в любом своём проекте я знаю, что главная форма всегда называется fMain, главный ToolBar - tbMain и так далее. А если мне надо, например, найти кнопку, отвечающую за минимизацию дочерних окон, не глядя могу сказать, что она будет называться btnWindowMinimizeAll, соответствующий ей пункт меню - miWindowMinimizeAll, заглавный пункт меню, в котором он находится - miWindow, а обработчик всего этого будет прописан в aWindowMinimizeAllExecute (TAction). Конечно, это занимает время, но читабельность кода возрастает во много раз, а главное, намного облегчает ориентирование в  любом своём проекте, даже если вы писали его несколько лет назад.


Теперь создайте в ActionList новый пункт, укажите ему нужный рисунок, а кнопке присвойте этот новосозданный Action. На этом этапе вы должны видеть перед собой примерно следующее:


Теперь дваждый щёлкнув на свой Action в ActionList, вы получите возможность прописать для него какой-то обработчик. Этот самый обработчик, по задумке, и будет заниматься созданием базы. Если вы дали себе труд пролистать описание компонента
TIbDatabase,то у него был соответствующий метод (CreateDatabase) на создание базы данных. Сейчас мы разберёмся, как этот метод работает.


Создание базы данных


Форма для ввода минимальных параметров


Очевидно, что требуемые для создания БД параметры, нам придётся как-то вводить. т.е. от создания диалога здесь не отвертишься. Стало быть, добавляем в проект новую форму (dlgDbCreate), и тыкаем на неё нужные нам поля ввода. Получится примерно следующее:


Поскольку создание базы данных - дело отнюдь не ежедневное, имеет смысл убрать эту форму из списка создаваемых автоматически и написать процедуру, которая при вызове будет создавать эту форму динамически, получать у пользователя нужные значения, и возвращать их нам в виде параметров. Заходим в Project/Options, переносим dlgDbCreate в список доступных форм, и пишем простенький код:

Код: (Delphi)
type
        TdlgDbCreate = class(TForm)
        btnOk: TBitBtn;
        btnCancel: TBitBtn;
        lDbName: TLabel;
        lDialect: TLabel;
        lLogin: TLabel;
        lPassword: TLabel;
        eDbName: TEdit;
        cbDialect: TComboBox;
        eLogin: TEdit;
        ePassword: TEdit;
        Bevel1: TBevel;
private
        { Private declarations }
public
        { Public declarations }
        class function fDlgDbCreate: TDlgDbCreate;
end;

var
        dlgDbCreate: TdlgDbCreate;

        function GetDbParams (var DBName: string; var Dialect: integer;
        var UserName, Password: string): boolean;

implementation

        {$R *.DFM}

        function GetDbParams (var DBName: string; var Dialect: integer;
        var UserName, Password: string): boolean;
        begin
                with TDlgDbCreate.fDlgDbCreate do begin
                        Result := (ShowModal = mrOk);
                        if Result then begin
                                DbName := eDbName.Text;
                                Dialect := StrToInt (cbDialect.Text);
                                UserName := eLogin.Text;
                                Password := ePassword.Text;
                        end;
                end;
        end;

        { TdlgDbCreate }

        class function TdlgDbCreate.fDlgDbCreate: TDlgDbCreate;
        begin
                if dlgDbCreate = nil then
                        dlgDbCreate := Self.Create (nil);
                Result := dlgDbCreate;
        end;

initialization

        dlgDbCreate := nil;

finalization

        FreeAndNil (dlgDbCreate);
end.

Код, как говорится, прозрачен, и в комментариях не нуждается, за исключением, быть может, одного момента - class function. Даже искушённые программеры почему-то не часто пользуются в своих проектах забавной возможностью вызывать методы класса, не требующие создания самого экземпляра класса, т.е - объекта. Давайте прервёмся с Ib, сделаем очередное отступление, и вспомним старый добрый object pascal.


Offtopic: Использование методов класса для работы с объектами, не создаваемыми автоматически


Итак.  Вкратце напомню, что методы класса, это такие функции и процедуры, которые могут вызываться на уровне самого класса, без создания его экземпляра. Что-то типа конструкторов, с тем отличием, что в методах класса нельзя напрямую использовать поля и методы объекта (что вполне логично - ведь объекта, как такового, ещё нет!). При этом идентификатор Self указывает не на экземпляр объекта, а на его класс, т.е. может использоваться, например, для вызова конструкторов. Вот так, вкратце, об этом повествует справка. Теперь зачем это нужно нам.  Представьте, что у вас в проекте куча форм, таблиц, или любых других объектов, которые далеко не всегда должны использоваться. Логичным решением представляется создавать их динамически по мере необходимости. Но это означает, что перед каждым вызовом надо проверить, не создавался ли уже такой объект, при необходимости - создать, а потом освободить. Теперь взгляните ещё раз на вышеприведённый код. При необходимости обратиться к форме (которая может быть уже есть, а может быть ещё и нет) мы вызываем её метод класса, который проверяет, создана ли форма ранее, если нет - то создаёт её, и возвращает нам на неё указатель.  И вот с этим указателем мы можем творить всё, что нам угодно, ибо это уже не класс, а экземпляр класса, т.е. - сам объект. Ну а  при завершении программы его, есс-но, придётся освободить. Обратите внимание, что конструктор формы вызывается с параметром nil. Мы могли бы передать туда, например, Application, и не  беспокоиться об освобождении формы вообще. Просто автор сего опуса предпочитает удалять вручную всё то, что вручную создаётся, хотя object pascal позволяет переложить это дело на само приложение.
Этот простенький способ применим к любому объекту, будь то форма, таблица или, например,  модуль данных. В итоге он позволит вам спокойно обращаться к любому объекту через метод его класса, который сам позабодится о том, чтобы вызываемый объект уже существовал.  А теперь вернёмся к нашим баранам.


Передача параметров серверу


На данный момент нам осталась одна забава: скормить все полученные параметры компоненту
TIbDatabase и вызывать его метод CreateDatabase. Всё достаточно тривиально, ниже я просто приведу код, обратить внимание следует только на один момент: структуру параметров при вызове метода на создание базы. Она несколько отличается от той, которая используется при подключении. Сам обработчик прописан в aDatabaseCreate.OnExecute (TAction), который присвоен нашей единственной пока кнопке.

Код: (Delphi)
procedure TfMain.aDatabaseCreateExecute(Sender: TObject);
var
        DbName: string;
        Dialect: integer;
        UserName: string;
        Password: string;

        begin
                if GetDbParams (DbName, Dialect, UserName, Password) then
                        with dbMain do begin
                                Connected := FALSE;
                                DatabaseName := DbName;
                                SQLDialect := Dialect;
                                Params.Clear;
                                Params.Add ('USER "' + UserName + '"');
                                Params.Add ('PASSWORD "' + Password + '"');

                                try
                                        CreateDatabase;
                                        MessageDlg (Format (csDatabaseCreatedMsg, [DbName]), mtInformation, [mbOk], 0);
                                except
                                        on E: Exception do
                                                MessageDlg (Format (csDatabaseCreateError, [E.Message]), mtError, [mbOk], 0);
                                end;
                        end;
        end;

По умолчанию, логином / паролем являются sysdba / masterkey. Экспериментировать можно и с ними, но вот для серьёзной работы пароль администратора следует изменить изначально (логин, sysdba, изменить нельзя).

Сообщения, выдаваемые программой вынесенны в отдельные константы, объявленные к тому же как строковые ресурсы. Сделано это для того, чтобы впоследствии эти константы можно было спокойно редактировать непосредственно в экзешнике (привет хацкерам :)). Вот, собственно, и вся недолга. Теперь нажав на кнопку, заполнив поля формы и нажав "Готово" вы окажетесь счастливым обладателем новой базы данных. При наименовании базы стоит придерживаться традиций, т.е. расширение файла указать "*.GDB". Если в указанном вами месте на диске появился соответствующий файл, то можно двигаться дальше, а именно - посмотреть, что же в нём лежит.


Подключение к базе данных


Опять таки, дело вкуса, но я пошёл по пути многооконного интефейса (MDI), чтобы на каждую открываемую базу создавалось новое дочернее окно, в котором с этой базой можно будет работать. Соответственно, главной форме выставляем аттрибут FormStyle = fsMDIForm, заводим новую форму, убираем из автоматически создаваемых, ей указываем FormStyle = fsMDIChild, и кладём на неё ещё один
TIbDatabase(dbChild) для осуществления непосредственного подключения. На главной форме заводим ещё один TAction (aDatabaseOpen), связанную с ним кнопку (btnDatabaseOpen) и добавляем ещё один рисунок в TImageList (ilMain). Ещё для открытия файла нам потребуется диалог TOpenDialog (OpenDlg) с соответствующими настройками. Само подключение лучше всего прописать в дочернюю форму, одной процедурой, которой будет передаваться путь к файлу БД, порлученный из OpenDlg. Помимо всего прочего процедура будет создавать новую форму и освобождать её в случае, если подключение не удалось. У вас получится примерно следующий код:

Код: (Delphi)
procedure OpenDatabase (DbName: string);
        begin
                with TfChild.Create (Application) do begin
                        dbChild.DatabaseName := DbName;
                        dbChild.SQLDialect := 3;
                        try
                                dbChild.Connected := TRUE;
                                LoadObjects;
                                Caption := DbName;
                                Show;
                        except
                                on E: Exception do begin
                                        MessageDlg (Format (csDatabaseOpenError, [E.Message]), mtError, [mbOk], 0);
                                        Free;
                                end;
                        end;
                end;
        end;

Единственный ньюанс - это установка диалекта, равного трём. На самом деле у базы он может быть равен и одному. Здесь дело в механизме обработки несоответствий диалектов: можно подключиться к базе с "завышенным" по сравнению с реальным диалектом (он автоматически будет понижен и возникнет событие OnDialectDowngradeWarning), но не наоборот. (См. свойство SqlDialect)

В основной форме вызов обработчика прост до безобразия:

Код: (Delphi)
procedure TfMain.aDatabaseOpenExecute(Sender: TObject);
        begin
                if OpenDlg.Execute then
                        OpenDatabase (OpenDlg.FileName);
        end;

Ещё в объявление констант добавилась одно сообщение, об ошибке открытия базы:
... csDatabaseOpenError = 'Ошибка при открытии базы данных. Сообщение от сервера: '#13#13'"%s"';
Теперь, когда мы умеем создавать базу данных и подключаться к ней, было бы интересно взглянуть, что, собственно, в этой "пустой" базе находится.


Получение списка таблиц и полей


В этом деле нам помогут две процедуры всё того же компонента
TIbDatabase: GetTableNames и GetFieldNames. Для удобства я поместил в дочернюю форму TTreeView (tvObject), в котором мы выведем это всё в виде дерева, примерно как это делает тот же IbExpert. Сам процедура загрузки ничего из ряда вон выходящего из себя не представляет, ниже приведён её код:

Код: (Delphi)
procedure TfChild.LoadObjects;
        var
                Node,
                TableNode: TTreeNode;
                slTables,
                slFields: TStrings;
                i, j: integer;
        begin
                with tvObjects.Items do begin
                        BeginUpdate;
                        try
                                Clear;
                                Node := Add (nil, 'Oaaeeou');
                                slTables := TStringList.Create;
                                slFields := TStringList.Create;
                                try
                                        dbChild.GetTableNames (slTables, TRUE);
                                        for i := 0 to slTables.Count - 1 do begin
                                                TableNode := AddChild (Node, slTables [i]);
                                                dbChild.GetFieldNames (slTables [i], slFields);
                                                for j := 0 to slFields.Count - 1 do
                                                        AddChild (TableNode, slFields [j]);
                                        end;
                                finally
                                        slFields.Free;
                                        slTables.Free;
                                end;
                        finally
                                EndUpdate;
                        end;
                end;
        end;

Т.е. для каждой таблицы мы получаем список её полей. Вызывается эта процедура из написанной ранее OpenDatabase непосредственно после установки Connected в TRUE. На экране результат будет выглядет примерно так:


Как видим, в "пустой" базе данных оказалось просто куча всяких таблиц! Объясняется это тем, что при вызове процедуры GetTableNames мы передали второй параметр [System: boolean] равным TRUE. И всё, что мы видим, это не что иное, как системные таблицы. Забегая вперёд скажу, что именно они отвечают за хранение метаданных, управление целостностью и многое другое.

В следующей статье мы научимся создавать таблицы самостоятельно, просматривать их, и более подробно остановимся на внутренней архитектуре СУБД Interbase.

В заключение - домашнее задание :). Есстественно, никто не заставит вас его делать, но если кто-то всё-таки решится, все ответы будут рассмотрены и прокомментированы. Создайте базу с размером страницы равным 4096 и кодировкой WIN1251. Почитать о том, что это такое можно в справке по Ib в pdf-формате, входящей в поставку, или на уже упоминавшемся ibase.ru. Небольшая подсказка: решение сведётся к вызову метода
TIbDatabase.CreateDatabase с нужными параметрами, т.е. по большом счёту, требуется просто выяснить, как эти параметры задаются и предусмотреть механизм для их ввода. Ещё одна подсказка: это далеко не единственные параметры, которые могут задаваться.

Вопросы и решения принимаются как на мыло,так и в форум. Исходник слегка причёсанной проги для Delphi5 можно взять здесь.

Огромное спасибо Oldy, взявшему на себя труд просмотреть материал и высказать ряд замечаний, способных сэкономить кучу нервов тем, кто взял на себя труд пробовать приведённые примеры на практике.

За сим - до встреч, всем удачи.

best regards,
x77.
Версия для печати
Обсудить на форуме