или Множественное значение
В некоторых случаях свойство объекта представляет собой множество ссылочных значений. Например, у книги может быть несколько авторов, компьютерная игра может относиться сразу к нескольким жанрам. В этом случае нас выручит подтип значения под названием “множество”.
На стороне базы данных уже все готово для хранения таких значений. Рассмотрим ещё раз таблицу oproperty.

Каждая запись в этой таблице – одно значение конкретного свойства объекта, в том числе:
- value_s – текстовое представление значения
- id_object1 – ссылка на объект
Стоит заметить, что поле value_s хранит не только само значение свойства (текст, число или дату), но и удобную форму представления ссылочного свойства – название объекта. В случае множественного значения в этом поле хранится список названий объектов, входящих в множество выбранных значений, а каждая ссылка хранится в отдельной записи с теми же атрибутами id_object и id_cproperty, что и запись с визуализацией множества. Чтобы отличить значение свойства от значений множественных ссылок, служит поле orderNum:
- NULL – запись служит для хранения отображаемого свойства
- <порядковый номер> – запись содержит элемент множественного значения.
Сейчас поле orderNum по сути используется как флаг, но в перспективе можно добавить функционал, связанный с порядком следования элементов множества.
Доработки приложения
Так как Data Keeper создается на My Visual Database, то мне всё чаще приходится сталкиваться с ограничениями и ошибками в работе данной платформы, которые не позволяют реализовать все задумки. В частности, компонент TdbComboBox задуман как компонент, который имеет возможность отображать множественные значения. Но не имеет желания корректно работать под ОС Windows 10, в результате чего мне пришлось отказаться от его использования в режиме редактирования множественных значений. Также окончилась неудачей попытка создания суррогатного компонента из-за ограничений скриптового языка MVDB. А так как я всё равно собирался создавать возможность альтернативного выбора ссылочных значений через модальную форму, то с неё и начнем, сразу добавив в неё функционал по работе с множественными значениями.
frmObject_Sel
Форма выбора значений оказалась похожей на форму со списком экземпляров классов frmObject. По крайней мере алгоритм формирования табличного представления у них одинаковый, что заставило меня оформить его как отдельную процедуру btnPrepareOject(), в которую передается кнопка для отображения результата SQL-запроса в таблице, ID класса объекта, а также флаг необходимости добавления поля с чекерами – элемента управления множественным выбором.
procedure btnPrepareOject( AButton: TdbButton; AIDClass: integer; ACheckBox:Boolean = False);
// генерация табличного представления класса
var
tmpParentList: string;
tmpChildList: string;
tmpDataSet: TDataSet;
tmpSQL: string;
tmpFields : string;
tmpJoins: string;
tmpCount: integer;
tmpCaptions: string;
tmpTableAlias: string;
begin
GetChildAndParent( AIDClass, tmpParentList, tmpChildList); // получить список ID родителей и детей
tmpSQL := 'SELECT * FROM cproperty WHERE id_class in ('+tmpParentList+') ORDER BY orderNum ';
SQLQuery(tmpSQL,tmpDataSet);
tmpFields := 'object.id';
if ACheckBox then
tmpFields := tmpFields + ',"$checkbox"';
tmpJoins := '';
tmpCaptions := 'delete_col'; // не отображать
if ACheckBox then
tmpCaptions := tmpCaptions + ',#';
tmpCount := 0;
//
while not tmpDataSet.EOF do
begin
if tmpDataSet.FieldByName('visible').asInteger = 1 then
begin
tmpTableAlias := 'OP_'+IntToStr(tmpCount);
//
if tmpFields <> '' then
tmpFields := tmpFields + ', ';
tmpFields := tmpFields + tmpTableAlias+'.value_s ';
//
tmpJoins := tmpJoins + 'LEFT JOIN oproperty '+tmpTableAlias+' ON '+tmpTableAlias+'.id_object = object.id AND '+tmpTableAlias+'.id_cproperty = '+tmpDataSet.FieldByName('id').asString+' AND '+tmpTableAlias+'.orderNum is NULL '+CR ;
//
if tmpCaptions <> '' then
tmpCaptions := tmpCaptions + ', ';
tmpCaptions := tmpCaptions + tmpDataSet.FieldByName('name').asString;
if tmpDataSet.FieldByName('is_name').asInteger = 1 then
AButton.Tag := tmpCount;
//
inc(tmpCount);
end;
tmpDataSet.Next;
end;
tmpDataSet.Free;
tmpSQL := 'SELECT '+tmpFields+CR+' FROM object '+tmpJoins+CR+' WHERE object.id_class in ('+tmpChildList+')';
AButton.dbSQL := tmpSQL;
AButton.dbListFieldsNames := tmpCaptions;
end;
Code language: JavaScript (javascript)
Как видно из первых строчек кода, формирование списка ID предков и потомков тоже реализовано как отдельная процедура с параметрами GetChildAndParent(). В дальнейшем она упразднится, так как интервальные деревья, на которые я планирую перевести таблицу object, позволяют формировать списки родителей и детей достаточно простым SQL-запросом.
procedure GetChildAndParent(AID:integer; var AParentList:string; var AChildList:string);
// формирование списка родителей и детей
var
tmpTree: TdbTreeView;
s: string;
tmpIndex: integer;
tmpForm: TForm;
tmpLevel: integer;
begin
AParentList := '-1';
AChildList := '-1';
tmpForm := GetFormByName('dtfClass_Tree');
if tmpForm <> nil then
begin
tmpTree := TdbTreeView( Form_GetDataViewer( tmpForm ) );
// собираем ID c узла и всех родительских узлов
tmpIndex := Grid_IdToIndex( tmpTree, AID);
if tmpIndex <> -1 then
begin
s := '';
repeat
if s <> '' then
s := s+',';
s := s + IntToStr( tmpTree.dbIndexToID( tmpIndex ) );
tmpIndex := tmpTree.GetParent( tmpIndex );
until tmpIndex < 0;
AParentList := s;
// собираем дочерние ID
tmpIndex := Grid_IdToIndex( tmpTree, AID);
tmpLevel := tmpTree.GetLevel( tmpIndex );
s := '';
repeat
if s <> '' then
s := s+',';
s := s + IntToStr( tmpTree.dbIndexToID( tmpIndex ) );
inc(tmpIndex);
until (tmpIndex = tmpTree.RowCount) or ( tmpTree.GetLevel( tmpIndex ) <= tmpLevel );
AChildList := s;
end;
end;
end;
Code language: JavaScript (javascript)
Процедуры frmObject_tgrMain_OnChange и frmObject_tgrMain_OnColumnResize были переписаны таким образом, чтобы их можно было использовать на разных формах, с чекерами и без них. Для упрощения алгоритма используется соглашение, что колонка с чекерами имеет заголовок “#”.
procedure frmObject_tgrMain_OnChange (Sender: TObject);
// обновление данных в таблице
var
tmpIDClass: integer;
tmpSQL: string;
tmpColumn: integer;
s: string;
tmpColWidth: array of string;
tmpForm:TForm;
tmpLabel:TdbLabel;
tmpGrid: TdbStringGridEx;
tmpFirstColumn: integer;
tmpIDs: array of string;
i: integer;
tmpIndex: integer;
begin
tmpGrid := TdbStringGridEx(Sender);
CForm(Sender,tmpForm);
FindC(tmpForm,'labIDClass',tmpLabel);
// получаем ID
tmpIDClass := tmpLabel.Tag;
// считать ширины колонок из базы
tmpSQL := 'SELECT COALESCE(col_widths,"") FROM class WHERE id = '+IntToStr(tmpIDClass);
s := SQLExecute(tmpSQL);
tmpColWidth := SplitString(s,',');
tmpFirstColumn := 0;
if (tmpGrid.Columns.Count > 0) and (tmpGrid.Columns[tmpFirstColumn].Header.Caption = '#') then
begin
tmpGrid.Columns[tmpFirstColumn].Width := 20;
tmpFirstColumn := 1;
end;
for tmpColumn := tmpFirstColumn to tmpGrid.Columns.Count - 1 do
begin
if tmpColumn < length(tmpColWidth)+tmpFirstColumn then
tmpGrid.Columns[tmpColumn].Width := StrToInt(tmpColWidth[tmpColumn-tmpFirstColumn] )
else
tmpGrid.Columns[tmpColumn].Width := 200;
end;
tmpLabel.Caption := ''; // разблокируем
// позиционируем
FindC(tmpForm,'labID',tmpLabel,False);
if (tmpLabel<>nil) and (tmpLabel.Caption <> '') then
begin
if tmpGrid.Columns[0].Header.Caption = '#' then
begin
tmpIDs := SplitString(tmpLabel.Caption,',');
for i:=0 to length(tmpIDs) - 1 do
begin
tmpIndex := Grid_IdToIndex(tmpGrid, StrToInt( tmpIDs[i] ) );
tmpGrid.Cell[0,tmpIndex].asBoolean := True;
end;
end
else
tmpGrid.dbItemID := StrToInt(tmpLabel.Caption);
end;
end;
procedure frmObject_tgrMain_OnColumnResize (Sender: TObject; ACol: Integer);
// изменение ширины колонок
var
tmpIDClass: integer;
tmpSQL : string;
i: integer;
s: string;
tmpForm:TForm;
tmpLabel:TdbLabel;
tmpGrid: TdbStringGridEx;
tmpFirstColumn: integer;
begin
tmpGrid := TdbStringGridEx(Sender);
if (tmpGrid.Columns.Count > 0) then
begin
CForm(Sender,tmpForm);
FindC(tmpForm,'labIDClass',tmpLabel);
//
if tmpLabel.Caption = '' then
begin
s := '';
tmpFirstColumn := 0;
if tmpGrid.Columns[tmpFirstColumn].Header.Caption = '#' then
tmpFirstColumn := 1;
for i:=tmpFirstColumn to tmpGrid.Columns.Count - 1 do
s := s + IntToStr(tmpGrid.Columns[i].Width) + ',';
delete(s,length(s),1);
// запоминаем ширины всех колонок в БД
tmpIDClass := tmpLabel.Tag; // класс
tmpSQL := 'UPDATE class SET col_widths = "'+s+'" WHERE id = '+IntToStr(tmpIDClass);
SQLExecute(tmpSQL);
end;
end;
end;
Code language: PHP (php)

На форме также расположена кнопка подтверждения выбора (1) и дополнительный элемент-метка, который используется для передачи параметров на форму: значение ссылочного параметра и его визуальное отображение. На кнопке находится обработчик, который может формировать из чекеров строку с ID выбранных значений и строку визуального отображения:
procedure frmObject_Sel_btnSelect_OnClick (Sender: TObject; var Cancel: boolean);
var
i: integer;
tmpCaption: string;
tmpIDs: string;
begin
if frmObject_Sel.tgrMain.Columns[0].Header.Caption = '#' then
begin
tmpCaption := '';
tmpIDs := '';
for i := 0 to frmObject_Sel.tgrMain.RowCount - 1 do
begin
if frmObject_Sel.tgrMain.Cell[0,i].AsBoolean then
begin
if tmpCaption <> '' then
tmpCaption := tmpCaption + ', ';
tmpCaption := tmpCaption + frmObject_Sel.tgrMain.cells[ frmObject_Sel.btnUpdate.Tag + 1, i ];
if tmpIDs <> '' then
tmpIDs := tmpIDs + ',';
tmpIDs := tmpIDs + IntToStr(frmObject_Sel.tgrMain.dbIndexToID( i ));
end;
end;
frmObject_Sel.labID.Caption := tmpIDs;
frmObject_Sel.labID.TagString := tmpCaption;
end
else
begin
frmObject_Sel.labID.Caption := IntToStr(frmObject_Sel.tgrMain.dbItemID);
frmObject_Sel.labID.TagString := frmObject_Sel.tgrMain.cells[ frmObject_Sel.btnUpdate.Tag, frmObject_Sel.tgrMain.SelectedRow ];
end;
frmObject_Sel.ModalResult := mrOK;
end;
Code language: JavaScript (javascript)
Доработке также подверглись другие процедуры и функции.
Компоненты UI

Регистрируем в базе новый компонент. Он реализован как кнопка, которая появляется рядом с полем отображения текстового представления:

Чтобы всё сработало, настраиваем тип компонента в дереве классов:

Затем устанавливаем тип значения “Автор” как “Множество”.

В результате при редактировании книги можно выбрать несколько авторов, что соответствует реальной картине мира.
Результат

Появилась возможность редактирования множественных значений, которые отображаются как список (1), а редактируются чекерами (2) на отдельной форме.
Не следует путать множественное значение со списком объектов, входящих в состав другого объекта, например, рецепт блюда или детализация накладной. Главное отличие состоит в том, что элемент такого списка не является простым объектом, а содержит несколько уникальных свойств (ингредиенты – информацию о количестве, строка в накладной – количество и стоимость товара). Вот только удобней отображать детализацию накладной вместе с самой накладной. Задача отображения зависимых объектов стоит в списке приоритетных доработок:
- Интервальное дерево классов
- Поиск объектов по заданному значению свойства (нескольких свойств)
- Отображение зависимых объектов (объектов, у которых данный объект является значением какого-либо свойства)
- Создание произвольных представлений (использования аналога SQL для ООП).
- Фильтрация (автоматическое построение панели фильтрации объектов заданного класса на основании списка свойств)
- Флаг обязательности заполнения свойства объекта (аналог обязательного поля)
- Флаг уникальности свойства объекта класса (аналог уникальности значения поля)
- Флаг глобальной уникальности свойства объекта (или свойство с генерацией GUID)
- Расширение базовых типов данных (время, да/нет, изображения/файлы и др.)
- Добавление контроля для типов данных (аналог доменов) – диапазон значений для чисел и дат, длина текста для строк и т.д.
- Ручная настройка положения и размера компонентов на форме редактирования классов.
- Добавление новых ссылочных значений непосредственно из формы редактирования объекта.
Заглядывая на перспективу реализации данной системы для web, размышляю над тем, что web-интерфейс не предполагает наличия модальных форм, создаваемых сервером. Придется много мудрить с клиентскими скриптами, чтобы реализовать такую простую и нужную концепцию, как модальность и вложенность отображения форм.
Ссылки
- Data Keeper 1.2 – исходники проекта