Автор Николай Озниев,
Опубликовано в журнале Программист, №1, 2003г.
Приложение со свойствами платформы. Типы полей базы данных
Диалог работы с типами полей

В прошлой статье речь шла об общих принципах построения приложения, обладающего свойствами платформы, т.е. имеющего средства, позволяющие адаптировать его к условиям пользователя без переработки исходного программного кода. В описанном приложении одним из основных средств подобного назначения был конструктор баз данных. Конструктор позволяет производить основные операции, необходимые для создания, модификации и изменения атрибутов, задающих структуры таблиц и полей. Хотя у конструктора баз данных есть и другие задачи, остановимся на этих важнейших. Как известно, в этих операциях играют ключевую роль типы данных.

В нашем приложении доступ к данным осуществляется через BDE, и, казалось бы, что тут мудреного, - достаточно создать механизм поддержки существующих типов данных, удобный с точки зрения работы настройщика или пользователя. Но, как оказывается при ближайшем рассмотрении этой задачи, все не так тривиально. Прежде всего, нет надобности оперировать со всеми типами данных, - любая предметная область может обойтись необходимым минимумом, причем всегда существенно меньшим, чем набор типов BDE в целом. Кроме того, некоторые задачи обеспечения целостности базы данных и адаптации ее к предметной области оказались легко решаемыми посредством ввода в конструктор специальных структур для хранения в памяти информации о типах данных, причем в них содержатся не только традиционные сведения, такие, как тип поля, ее размер и т.д., но и дополнительная информация, вводимая для повышения удобства работы с ними настройщику и пользователю. К такой информации относится, прежде всего, смысловое наименование типа, понятное для пользователя, не обладающего специальными знаниями в области программирования. Так, например, программисту все ясно, когда он видит обозначение типа как ftInteger, а для пользователя лучше, когда он увидит в качестве обозначения типа фразу «Целое число». Для ряда задач оказалось удобным создать специальный набор структур, предназначенных для хранения информации о типах полей, используемых для связей между таблицами, а также типов полей для хранения данных, выбираемых пользователем из собственных списков, по существу являющихся справочниками небольшого объема. В таких структурах дополнительной информацией являются смысловые названия полей и списков. В связи с этим были введены так называемые группы данных, каждая из которых представляет собой определенный набор типов данных, имеющих общий признак, т.е. описываемых структурой одного и того же типа. Обратим внимание, что фактически все поля, с которыми манипулирует наш конструктор, имеют типы, предусмотренные в BDE, и здесь ничего нового нет. Новое понятие групп данных связано с характером той дополнительной информации, которая сопровождает параметры типа поля в указанных структурах. Если эта информация сводится просто к названию типа и нет других особенностей, связанных с полем, имеющим этот тип (например, просто поле имеет тип «Целое число»), то мы будем такие поля относить к базовой группе данных, а во всех остальных случаях – к той или иной группе данных, в зависимости от характера дополнительной информации, которая важна с точки зрения назначения системы. В этих случаях уже неважно, как называется тип по канонам BDE, - эта информация в структуру даже не вводится, так как дополнительная информация, вводимая в структуру, косвенно содержит описание типа. Так, если поле предназначено для хранения информации из списков, то косвенно это означает, что тип поля ftString. Важно еще подчеркнуть, что групп данных априори может быть сколько угодно. В описываемой платформе пока таких групп реализовано четыре.

Итак, по поводу базовой группы данных как будто все ясно. Еще две группы данных вводятся в конструктор из соображений обеспечения связей между таблицами. Давайте, прежде всего, потребуем, чтобы в любой таблице было автоинкрементное поле id, что позволяет обеспечить внутренние механизмы управления связями, не касаясь пользовательских данных. Для пользователя это требование, по сути, не несет никакой информации, поэтому процесс формирования автоинкрементного поля можно встроить в ядро приложения, обеспечив его автоматическое формирование в момент создания таблицы. В этом случае можно ввести сразу две группы данных.

Первая из них будет представлять собой множество всех автоинкрементных полей в пользовательской базе данных, а вторая – множество всех остальных полей этой же базы данных. Практический смысл этого выглядит следующим образом. Скажем, в таблице T1 создается поле F_New с задачей обеспечить отслеживание информации из поля F_Old таблицы T2 во время работы приложения. Под таким отслеживанием может быть самый разный смысл, поэтому предположим, что производится копирование в поле F_New редактируемой записи таблицы T1 информации из поля F_Old текущей записи таблицы T2, если конечно таблица T2 открыта. Тогда поле F_New должно иметь те же атрибуты, что и поле F_Old, поэтому при его создании мы просто указываем на поле F_Old, а конструктор обеспечивает все необходимые операции по формированию типа поля. Типичный пример, когда нужна функциональность, основанная на описанном слежении: автоматический выбор информации из справочников во время редактирования данных в амбулаторной карте пациента. Для автоинкрементных полей F_Old будет особое понимание слежения, а именно, будем иметь в виду ссылку на запись таблицы в целом, т.е. при этом мы собираемся создать программный механизм связывания таблицы Т1 с таблицей Т2. Конкретное содержание такого механизма опять же может быть различным, например, такая ссылка может использоваться как традиционная, для организации связи один ко многим, т.е. в поле F_New содержится значение автоинкрементного поля таблицы F_Old таблицы T2.

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

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

Во всем этом пока еще нет изюминки. Она появляется, когда мы отдадим работу с группами данных пользователю, т.е. дадим ему возможность создавать поля самостоятельно, в том числе поля, реализующие ссылки на другие поля и таблицы удобным, простым и понятным ему образом. Интерфейс, реализующий такую возможность, показан на рис. 1. Здесь пользователь вводит в таблицу Врачи новое поле базовой группы данных, т.е. его тип выбирается из подмножества «обычных» типов полей, отобранных для использования в платформе из перечислимого типа TFieldType. Как было описано выше, каждый тип снабжен смысловым описанием, что непосредственно нашло отражение в представленном диалоге. Пользователю предоставлена возможность задать как идентификатор поля, так и его наименование и описание, если предложенные системой названия неприемлемы. Система в качестве наименования всегда предлагает имя идентификатора, но пользователь как правило, заменяет наименование на понятное ему, например, в ситуации на рис.1 это может быть «дата рождения». В качестве описания пользователь обычно вводит некий краткий комментарий, например, «дата рождения врача».

Рисунок 1. Диалог создания поля базовой группы

Случай, когда пользователь решает ввести в таблицу поле, реализующее ссылку на другую таблицу, представлен на рис. 2. В этом случае система предлагает выбрать из имеющегося списка таблиц ту, на которую нужно реализовать ссылку. Напомним, что реализация ссылки может быть различной, например, как связь один ко многим. Созданное при этом поле типа ftInteger будет иметь имя, которое пользователь самостоятельно не может изменить, но зато может изменить наименование и описание нового поля.

Рисунок 2. Диалог создания поля ссылочной группы

Если нужно реализовать ссылку на поле в какой-либо таблице, то поведение системы будет несколько иным. На рис. 3 пользователю предлагается выбрать нужное поле нужной таблицы. Выше приводился пример, когда такая ссылка означала копирование в данное поле активной записи информации из того поля, на которое создана ссылка, в том случае, когда таблица открыта. Пользователь лишен возможности изменить имя поля, а атрибуты его типа определяются полем, на которое оно будет содержать ссылку.

Рисунок 3. Диалог создания поля следящей группы

Наконец, если пользователю нужно иметь поле, в которое будет вноситься значение из заранее фиксированного списка, то он выбирает списочную группу данных. Диалог выбора типа поля в этом случае приобретает вид, показанный на рис. 4. В этом случае будет образовано поле типа ftString и его размер будет определяться содержанием списка. Пользователь может выбирать имя поля, наименование и описание.

Рисунок 4. Диалог создания поля списочной группы

Базовая группа данных

Прежде всего, разработчику следует решить, какие типы данных, реализуемые BDE, он собирается использовать в своей платформе. Вряд ли целесообразно организовывать поддержку почти сорока типов, перечисленных в BDE. Следует поддерживать часто используемые типы, такие как ftString, ftInteger, ftFloat, ftDate, ftTime, ftDateTime. Для автоинкрементных полей понадобится тип ftAutoInc. Для работы с текстами, графикой и вообще с бинарными данными нужны ftBlob, ftMemo, и, возможно, ftGraphic. Дальнейшее расширение базовой группы скорее носит узкоспециализированный характер. Чтобы настройщик и пользователь могли свободно манипулировать выбранным подмножеством типов данных, целесообразно ввести структуру:

  // Структура базового типа данных
  TFbBaseType  = record
    sType: TFieldType;    // идентификация типа BDE
    sBytes,               // количество байтов под данный тип
    sSize,                // размер типа, аналог из BDE
    sInc: Integer;        // признак включения типа в платформу
    sDescr: ShortString;  // краткое описание типа
  end;

Она служит хранилищем сведений о типе поля. В диалогах, приведенных выше, списки TComboBox (Тип данных) содержат указатели на структуры данного типа. Поля sBytes и sSize приведенной структуры содержат соответственно число байтов, занимаемых данным типом в памяти и стандартное значение размера типа по соглашениям BDE. Ввод в структуру поля sBytes обусловлен желанием явно указать размер отводимой памяти в тех случаях, когда в BDE параметр Size равен 0. Для строковых и аналогичных ему типов этот параметр конкретизируется в момент формирования поля базы данных. Создав массив базовых типов

TFbFieldArray = Array[TFieldType] of TFbBaseType

а затем для тех ее членов, в которых поле sInc=1, заполнив поля sDescr, можно создать необходимые списки для работы с типами данных в конфигураторе. Инициализацию базовой группы нужно провести на стадии загрузки в память системной базы данных, о которой было рассказано в прошлой статье. Если есть желание расширить или, наоборот, сузить реализованный список используемых типов базовой группы данных, достаточно отредактировать поля sInc и sDescr. Если sInc=0 (по умолчанию), то данный тип исключается из системы, причем при этом можно не очищать поле sDescr. И наоборот, если нужно включить данный тип в систему, то нужно установить sInc=1, и, если есть необходимость, отредактировать поле sDescr. Предоставлять ли эту возможность настройщику, или ее оставить в ведении программиста ядра – зависит от конкретной технической политики. Очевидно, если эту возможность дать настройщику, то логика работы конструктора баз данных будет сложнее, а достигаемый эффект не так велик, поэтому лучше реализовать базовую группу данных в ядре системы.

Ссылочная группа данных

Так как мы ввели обязательное автоинкрементное поле в каждую таблицу базы данных, под ссылкой на таблицу можно иметь в виду поле целого типа, в котором хранится содержимое автоинкрементного поля той таблицы, на которую создается ссылка (ведущей таблицы). Тогда можно вывести из-под контроля настройщика процедуру создания ссылки на ведущую таблицу. Рассмотрим, чего мы добились, на примере. Пусть настройщик успел создать набор таблиц T1, T2 и T3. В каждой из этих таблиц автоматически были созданы автоинкрементные поля id. Например, если эти таблицы названы Пациенты, Врачи, Диагнозы, то к этому моменту в системе можно создать список:

Но пока это – только декларация. Чтобы этот список стал работоспособным, придется ввести структуру для управления ссылочной группой данных:

// Структура ссылочного типа данных
TFbReferenceType  = record
  sType: TFieldType;
  sBytes,
  sSize,
  sInc: Integer;
  sDescr: ShortString;
  spTableInfo: pTTableInfo;
end;

Она отличается от ранее введенной структуры для базовой группы наличием поля spTableInfo, куда заносится ссылка на уже существующую структуру таблицы. Остальные поля этой структуры копируют информацию из структур для автоинкрементного поля, кроме sDescr и sType. В поле sDescr вписывается наименование ведущей таблицы, например, Диагнозы. Как было указано выше, для ссылочной группы данных sType = ftInteger. Идентификаторы полей для ссылок унифицируем, назвав их T1_id, T2_id и T3_id и т.д., в какой бы новой таблице они не заводились, запретив тем самым их редактирование настройщикам и пользователям системы.

Внимательный читатель должен заметить избыточность информации при загрузке приложения в память из-за дублирования полей sType, sBytes и sInc, сначала в структурах, инициализирующих базовую группу данных, а затем в структурах, инициализирующих ссылки на таблицы, но из соображений унификации пренебрежем ею и возникающими хлопотами обеспечения стабильности программного кода (в строю никто не должен высовываться!)

Отметим также, что платформа должна содержать механизм, который обеспечивает синхронный процесс создания и уничтожения структур TFbReferenceType при создании и уничтожении структур таблиц в памяти во время конфигурирования системы. При этом еще должен существовать механизм блокировки от удаления тех структур таблиц, на которые существуют ссылки в других таблицах базы данных. Пока мы умалчиваем круг проблем, связанный с отслеживанием настроек рабочих мест, сохраненных в локальных файлах на компьютерах вычислительной сети, преследующей цель обеспечить сохранение работоспособности живущей системы при манипуляции с метаданными в пользовательской БД на сервере (вот она, не тривиальность работы с типами при их расширенном толковании!)

Следящая группа данных

Следящая группа данных особых пояснений не требует, т.к. принцип работы с ней в целом аналогичен принципу работы со ссылочной группой. Особенность состоит в том, что в структуру управления, помимо ранее рассмотренного указателя на структуру таблицы, вводится указатель на структуру того поля, на которое создается ссылка (ведущее поле), а тип sType устанавливается равным типу ведущего поля:

// Структура следящего типа данных
TFbLookupType  = record
  sType: TFieldType;
  sBytes,
  sSize,
  sInc: Integer;
  sDescr: ShortString;
  spFieldInfo: pTFieldInfo;
  spTableInfo: pTTableInfo;
end;

Приведенные три группы данных исчерпывают основные варианты. Но оказалось, что есть возможности по использованию механизма управления типами для создания полей, значения которых выбираются из определенных пользователем списков. Так появляется еще одна группа данных: списочная.

Списочная группа данных

Простейший список может состоять из значений Да, Нет. Списков аналогичного свойства, содержащих от двух до десятка и более членов, из которых пользователь может выбирать значение, любая предметная область содержит во множестве. Так, очень удобно реализовывать такими списками справочники небольшого объема. Примером может быть справочник частей света из 6 членов: Европа, Азия, Америка, Африка, Австралия и Антарктида. Другой пример – набор специальностей медицинского персонала, и т.д.

Специалистов по СУБД эти слова, возможно, будут раздражать. Но поспешу объявить, что тут нет никакого смешивания системных и прикладных инструментов: списочные типы реально появляются в приложении только на стадии его адаптации к условиям заказчика. Поэтому разработчику ядра, замахнувшемуся на святая святых СУБД – его конструктор, никогда не придется иметь дело с реальными списками – его задача состоит в том, чтобы создать необходимый инструментарий для работы с ними в конструкторе платформы.

Структура для работы со списочными типами, по аналогии с остальными, имеет вид:

// Структура списочного типа данных
TFbPickType  = record
  sType: TFieldType;
  sBytes,
  sSize,
  sInc: Integer;
  sDescr: ShortString;
  sPickList: TStrings;
end;

Как видно, ключевым полем в этой структуре является список значений sPickList, из которого пользователь выбирает нужные ему значения во время работы приложения. Работа с базой данных таких «справочников», фактически начинающаяся со стадии конфигурирования, позволяет повысить гибкость настройки системы под конкретные требования функциональных задач. Разумеется, для создания списка и управления им придется создать менеджер списочных типов. Он должен обеспечивать создание списка, добавление в список нового члена, редактирование и удаление членов списка. Созданный список сохраняется в системной базе данных.

Приведенные четыре группы данных в конкретной задаче могут оказаться недостаточными, и описанный подход позволяет наращивать группы данных системы. Например, в Lotus Notes широко используются списки, из которых можно выбрать несколько значений сразу, которые к тому же могут наращиваться путем ввода пользователем нового элемента в список, ранее там отсутствовавшего. Могут быть и другие потребности расширения возможностей платформы, которые укладываются в механизм управления типами данных.

Для централизованного управления программным кодом в процессе использования групп данных целесообразно ввести обобщенную структуру. Предвидя это, мы намеренно вводили для всех групп данных схожие структуры для работы с ними в памяти. Сначала введем комбинированный тип для групп данных:

TFbTypeGroup = (FldGroup, RefGroup, PicGroup, LUpGroup, NoGroup)

соответственно идентифицирующий базовую, ссылочную, списочную и следящую группы. Для полноты в него введен тип NoGroup, не содержащий никакой группы. Сразу сказать, зачем нужен тип NoGroup, невозможно, но интуиция подсказывает, что так надежнее.

Получаем структуру обобщенного (комбинированного) типа данных в виде вариантной записи:

// Структура комбинированного типа данных
TFbCommonType  = packed record
  FbTypeGroup: TFbTypeGroup;
  case TFbTypeGroup of
    FldGroup: (FbFld: pTFbBaseType);
    RefGroup: (FbRef: pTFbReferenceType);
    PicGroup: (FbPic: pTFbPickType);
    LUpGroup: (FbLUp: pTFbLookupType);
    NoGroup: ();
end;

В ней pTFbBaseType, pTFbReferenceType, pTFbPickType и pTFbLookupType – суть ссылки на структуры TFbBaseType, TFbReferenceType, TFbPickType и TFbLookupType соответственно. Итак, мы получили открытый механизм управления группами данных в конструкторе, который позволяет вводить в систему новые группы данных. Разумеется ничто не дается даром, - каждая новая группа данных потребует собственного менеджера, который бы обеспечивал интерфейс для работы с ним в конфигураторе системы. Кроме того, необходим способ связи конкретных структур полей и только что введенных структур групп данных для использования в конструкторе, а также во всех библиотечных функциях и процедурах, обрабатывающих структуры полей в пользовательском режиме.

Модернизируем структуру поля, описанную в предыдущей статье:

// Структура поля
TFieldInfo  = record
  sFieldAttr: TStrings; 
    // атрибуты поля:
    { sFieldName    - Имя поля            }
    { sMTableName   - Имя ведущей таблицы }
    { sMFieldName   - Имя ведущего поля   }
    { sPicDescr     - Имя списочного типа }
    { sFieldCaption - Наименование        }
    { sFieldDescr   - Описание            }

  sFieldType: TFieldType;
  sFieldSize: Integer;
  sFieldMBytes: Integer;
  ...
end;

Поясним смысл новых полей в структуре:

Правила использования данных полей таковы.

Приведенные правила позволяют хранить необходимую информацию о типах полей в системной базе данных, а при загрузке приложения создавать в памяти необходимые списки структур TFbCommonType. Это – ключевая задача, которая обеспечивает возможность работать с типами полей в конструкторе приложения в режиме конфигуратора, а в пользовательском режиме – реализовать ссылки и списки значений.