Сообщений 0    Оценка 60        Оценить  
Система Orphus

Конструктор для создания связей между объектами иерархической (древовидной) сущности на базе eXpress Persistent Objects (XPO) и WinForms Controls от DevExpress

Автор: Шаров Даниил
ОмГПУ,к.п.н., доцент кафедры ИВТ / ООО "ИНСОФТ" ведущий программист

Источник: RSDN Magazine #3-2009
Опубликовано: 17.03.2010
Исправлено: 10.12.2016
Версия текста: 1.0

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

Если заранее неизвестно количество уровней вложенности объектов, одной из наиболее очевидных структур будет являться таблица, которая помимо первичного ключа (ID) и наименования объекта (Name) будет содержать внешний ключ (Parent_Id), который будет ссылаться на первичный ключ этой же таблицы, но относящийся к другой записи (рисунок 1). Корневой узел в дереве представлен в виде записи, у которой значение поля Parent_Id равно NULL [1] [2] [3].


Рисунок 1. Структура таблицы для представления иерархии.

Стандартно для визуального представления иерархических структур используются компоненты типа TreeView (рисунок 2), которые позволяют наглядно увидеть вложенность объектов, сворачивать и разворачивать дочерние узлы, добавлять, редактировать и удалять объекты в иерархии.


Рисунок 2. Дерево. Представление иерархии на стороне клиента.

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

Рассмотрим задачу в целом. Создадим фрагмент базы данных (БД) и соответствующую клиентскую часть, которые позволят не только добавлять, редактировать и удалять элементы этой иерархии, но и задавать новые типы, а также описывать правила взаимодействия между этими типами. Пример будет основываться на территориальном справочнике, фрагмент БД для которого будет создаваться на базе СУБД Oracle 10g XE, а клиентская часть – на языке C# с использованием компонентов DevExpress [4].

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

Введем понятие типа объекта. Для этого в схему БД необходимо добавить таблицу (TypeObject), которая будет содержать наименования типов, а сам объект (запись в таблице TreeObject) вынужден будет ссылаться на первичный ключ (ID) таблицы TypeObject. Добавление типов и их привязку к элементам иерархии можно позволить в пользовательском интерфейсе. Но одного только введения таблицы TypeObject недостаточно, ведь в данном случае разрешено добавлять и закреплять объекты друг за другом без всяких правил, то есть, на примере территориального справочника, можно город закрепить за улицей, а страну за областью и т.д. Добавление правил взаимодействия между объектами определенных типов требует ввести еще одну таблицу – LinkType, которая будет содержать две ссылки на типы объектов (TypeObject). Наличие записи, которая содержит ссылки на значение двух типов, принимается за возможность существования подобной связи, а отсутствие такой записи – за невозможность закрепить объект одного типа за объектом другого типа (рисунок 3). При этом в поле FirstTypeObject_Id (FK) таблицы LinkType содержится ссылка на родительский тип (TypeObject.Id), а SecondTypeObject_Id (FK) содержит ссылку на тип дочернего объекта (TypeObject.Id). Таблица LinkType наряду с внешними ключами содержит первичный ключ – ID. Может возникнуть вопрос: зачем нужен ID, если можно сделать составной первичный ключ из полей FirstTypeObject_Id и SecondTypeObject_Id? И правда, зачем? Клиенту проще отправлять SQL-команды СУБД, опираясь на ключ, значение которого постоянно после создания, а ведь в нашем случае любое из полей составного ключа может измениться, и клиенту необходимо будет отправлять, например, для обновления данных, как старые значения, чтобы найти запись, так и, новые на которые необходимо произвести замену. Но это не самая важная причина. Основная причина в дальнейшем усложнении понятия связи между объектами, что приведет к добавлению в таблицу LinkType новых полей характеризующих эту связь. Учитывая, что связь может повторяться неоднократно, т.к. характеристика может зависеть от различных условий, поэтому совокупность значений полей FirstTypeObject_Id и SecondTypeObject_Id не будет уникальной, следовательно, эти поля не могут претендовать на роль первичного ключа.


Рисунок 3. Фрагмент схемы БД для представления иерархии, типов элементов иерархии и правил взаимодействия между ними.

В схему БД необходимо добавить триггер (Tgr_Treeobject_Ins_Upd) на события происходящие до вставки (insert) и обновления (update) записей в таблице TreeObject. В данном триггере необходимо осуществлять поиск добавляемой комбинации типов «родителя» и «ребенка», а если подобной комбинации не найдено, генерировать ошибку.

Прежде чем привести тело самого триггера, отметим ряд особенностей, с которыми пришлось столкнуться при его создании. Oracle различает верхний и нижний регистр в наименовании таблиц и полей, но широко распространенные программные продукты для взаимодействия с СУБД автоматически переводят все в верхний регистр, примером может служить Allround Automation PL/SQL Developer [7]. Поэтому в приведенном ниже коде триггера все названия таблиц и полей заключены в двойные кавычки, что и позволяет Oracle интерпретировать их как написано, даже если скрипт создается и запускается в PL/SQL Developer. Для получения доступа к новой записи (записи, которая добавляется или обновляется) и ее полям в теле триггера используется :new и через точку (.) наименование поля. Определить произошедшее событие можно, узнав значения inserting и updating в теле триггера:

      if (updating) then

При попытке во время выполнения получить доступ (выборка, вставки, изменение) к записям таблицы, к которой привязан триггер, можно получить ошибку, которая сообщает о мутировании самой таблицы:

ORA-04091 table string.string is mutating, trigger/function may not see it

Для обхода данной проблемы воспользуемся автономными транзакциями. Следует сказать, что, хотя в нашем случае они позволяют достаточно просто справится с возникшей проблемой, но их использование не всегда оптимально и может приводить к ряду других проблем. Так, например, код, который выполняется внутри автономной транзакции, не видит незафиксированных изменений основной транзакции. Иногда можно получить взаимную блокировку основной транзакции и автономной, если пытаться обновлять или удалять записи. Конечно, если транзакция автономна, требуется ее фиксировать (commit) в конце кода, а это говорит о том, что если произойдет откат (rollback) в основной транзакции, то действия, подтвержденные в автономной транзакции уже не отменить. После всего вышесказанного приведем код самого триггера:

      Create
      Or Replace Trigger Tgr_Treeobject_Ins_Upd
  Before InsertOrUpdateOn "TreeObject" -– до вставки или обновления в таблице --TreeObject(для каждой записи)For Each Row
Declare  -- Применяем автономную транзакцию для обхода мутирования 
  Pragma Autonomous_Transaction;
  Parent_Type_Id Integer;
  Fine           Integer;
Begin-- Найдем тип родительского узлаIf (:New."Parent_Id" IsNotNull) ThenBeginSelect "Type_Id"
        Into Parent_Type_Id
        From "TreeObject" t
       Where t."Id" = :New."Parent_Id";
    Exception –- Исключение, если нет данных в результате запросаWhen No_Data_Found Then
        Parent_Type_Id := Null;
    End;
  EndIf;
  -- Определим возможна ли связь дочернего и родительских узов (записей)BeginSelect Lt."Id"
      Into Fine
      From "LinkType" Lt
     Where (Lt."FirstTypeObject_Id" = Parent_Type_Id Or
           (Lt."FirstTypeObject_Id" IsNullAnd Parent_Type_Id IsNull))
       And (Lt."SecondTypeObject_Id" = :New."Type_Id" Or
           (Lt."SecondTypeObject_Id" IsNullAnd :New."Type_Id" IsNull));
  Exception –- Исключение, если нет данных в результате запросаWhen No_Data_Found Then
      Raise_Application_Error(-20022,
      'Невозможно добавить объект, т.к. не предполагается подобная связь родительского и дочернего элементов.');
  End;
  Commit; -- Необходимо зафиксировать изменения, т.к. транзакция автономнаяEnd Tgr_Treeobject_Ins_Upd;

В полях FirstTypeObject_Id и SecondTypeObject_Id таблицы LinkType разрешено задавать значения NULL, таким образом, можно создавать правила, которые разрешают существование корневых элементов различных типов.

Перейдем к рассмотрению клиентской части, которая отвечает за взаимодействие с базой данных (БД) и обеспечивает пользовательский интерфейс. Создадим объектную базу данных, воспользовавшись ORM (Object-relation mapping) [6, С. 68-71] решением от DevExpress под названием eXpress Persistent Objects (XPO) [5], которое позволяет отображать объекты базы данных на объекты объектно-ориентированного языка программирования (C#). Не забудьте подключить в ссылки проекта (References) DevExpress.Xpo и DevExpress .Data соответствующей версии.

XPO позволяет с помощью мастера создать C#-классы, соответствующие таблицам в существующей БД. Для этого нужно добавить в проект новый элемент Item – «Persistent Classes». После добавления в появившемся диалоговом окне указывается путь к БД и выбираются те таблицы и их структура (перечень полей), которые будут проецироваться в соответствующие классы. После небольшой обработки получим следующий код:

      using DevExpress.Xpo;
namespace TerritoryDictionary.PersistantObject
{
  publicclass TypeObject : XPLiteObject
  {
    System.Int32 fId;
    [Key(AutoGenerate = true)]
    public System.Int32 Id
    {
      get { return fId; }
      set { SetPropertyValue<System.Int32>("Id", ref fId, value); }
    }
    string fName;
    [Size(50)]
    publicstring Name
    {
      get { return fName; }
      set { SetPropertyValue("Name", ref fName, value); }
    }
    public TypeObject(Session session) : base(session) { }
    public TypeObject() : base(Session.DefaultSession) { }
  }

  publicclass TreeObject : XPLiteObject
  {
    System.Int32 fId;
    [Key(AutoGenerate = true)]
    public System.Int32 Id
    {
      get { return fId; }
      set { SetPropertyValue<System.Int32>("Id", ref fId, value); }
    }

    string fName;
    [Size(50)]
    publicstring Name
    {
      get { return fName; }
      set { SetPropertyValue("Name", ref fName, value); }
    }

    TreeObject fParent_Id;
    public TreeObject Parent_Id
    {
      get { return fParent_Id; }
      set { SetPropertyValue("Parent_Id", ref fParent_Id, value); }
    }

    [NonPersistent]
    publicobject KeyParent_Id
    {
      get { if (Parent_Id==null)
          returnnull;
        return Parent_Id.Id;
      }
      set
      {
        SetPropertyValue("Parent_Id", (System.Int32)value); 
      }

    }

    TypeObject fType_Id;

    public TypeObject Type_Id
    {
      get { return fType_Id; }
      set { SetPropertyValue("Type_Id", ref fType_Id, value); }
    }
    public TreeObject(Session session) : base(session) { }
    public TreeObject() : base(Session.DefaultSession) { }
  }

  publicclass LinkType : XPLiteObject
  {
    System.Int32 fId;
    [Key(AutoGenerate = true)]
    public System.Int32 Id
    {
      get { return fId; }
      set { SetPropertyValue<System.Int32>("Id", ref fId, value); }
    }

    TypeObject fFirstTypeObject_Id;
    public TypeObject FirstTypeObject_Id
    {
      get { return fFirstTypeObject_Id; }
      set 
      { 
        SetPropertyValue(
          "FirstTypeObject_Id", ref fFirstTypeObject_Id, value);
      }
    }
    TypeObject fSecondTypeObject_Id;
    public TypeObject SecondTypeObject_Id
    {
      get { return fSecondTypeObject_Id; }
      set 
      { 
        SetPropertyValue(
          "SecondTypeObject_Id", ref fSecondTypeObject_Id, value);
      }
    }
    public LinkType(Session session) : base(session) { }
    public LinkType() : base(Session.DefaultSession) { }
  }
}

После того как созданы классы, позволяющие взаимодействовать с БД, необходимо обеспечить соединение с самой БД и дальнейшее взаимодействие с ней. В данном примере используется схема tdb. Рассмотрим только основный код, который обеспечивает проецирование данных и метаданных на созданные нами объекты.

До описания кода откроем пространство имен DevExpress.Xpo:

      using DevExpress.Xpo;

После этого перейдем к рассмотрению части кода, в которой происходит взаимодействие клиента со схемой данных:

      // Создание объекта, позволяющего осуществить соединения с Oracle 
OracleConnection connect = CreateConnect(); 

// Создание контейнера для БД и метаданных
Metadata.XPDictionary dict = new Metadata.ReflectionDictionary();

// Создание полной схемы БД, соответствующей 
// имеющимся типам Persistent Objects
dict.GetDataStoreSchema(typeof(TypeObject).Assembly);
      dict.GetDataStoreSchema(typeof(TreeObject).Assembly);
      dict.GetDataStoreSchema(typeof(LinkType).Assembly);

/* Производим соединение с Oracle в соответствии с AutoCreateOption (второй параметр метода GetConnectionProvider), значение
которого – SchemaAlreadyExists, т.е. предполагается, что схема данных уже 
существует в СУБД и создавать ее нет необходимости, и возвращаем объект,
инкапсулирующий взаимодействие с установленным соединением.
*/
IDataStore store = XpoDefault.GetConnectionProvider(connect, AutoCreateOption.SchemaAlreadyExists);

// Проецируем слой данных (структуру таблиц с данными и связи между ними) 
// на клиент из схемы данных, расположенной на СУБД
XpoDefault.DataLayer = new ThreadSafeDataLayer(dict, store); 
store.UpdateSchema(false); // не обновлять схему БД с клиента// Создание заполненных коллекций из Persistent Object 
// (TypeObject, TreeObject, LinkType), используемых в сессии,
// установленной по умолчанию
tblTypeObject = new XPCollection<TypeObject>();
tblTreeObject = new XPCollection<TreeObject>();
tblLinkType = new XPCollection<LinkType>();

Перейдем к визуальной части проекта. DevExpress предоставляет множество визуальных компонентов для создания пользовательского интерфейса, которые к тому же весьма просты в использовании. Одним из наиболее функциональных и часто используемых компонентов является GridControl, который позволяет отображать данные и осуществлять операции с ними в табличном виде (GridView) или в виде карточки (CardView). Для использования визуальных компонентов достаточно разместить их на форме и задать определенные свойства. В нашем случае программа должна позволять манипулировать типами объектов. Внешний вид вызываемого из главного окна диалога для манипуляции с типами представлен на рисунке 4.


Рисунок 4. Диалоговое окно для манипулирования типами объектов (добавление, редактирование, удаление).

После добавления формы в проект и размещения на ней компонента GridControl от DevExpress нам необходимо свойство DataSource, которое приравнивается к объекту типа XPCollection:

gridControl1.DataSource = tblTypeObject;

Это позволит компоненту GridControl отображать данные в табличном виде.

Мы можем добавлять, редактировать и удалять типы элементов иерархии, нажимая соответствующие кнопки, расположенные внизу визуального компонента GridControl. Перейдем к правилам взаимоотношений между типами элементов иерархии (рисунок 5).


Рисунок 5. Диалоговое окно для создания правил связывания типов объектов.

Как видим, для отображения данных используется компонент GridControl, которому назначается источник данных, как и в предыдущем примере с типами объектов, который в свою очередь отображает записи с правилами из коллекции tblLinkType, но, как видно на рисунке 5, данные не вводятся в ячейки, а выбираются из списка, т.к. в таблице LinkType задаются внешние ключи для поля Id из таблицы TypeObject. Тем самым устанавливается возможная связь между типами.

Разберем, как обеспечивается работа выпадающего списка (LookUp) в ячейке. Необходимо задать поля, которые возвращают коллекции, чтобы в GridControl можно было настроить LookUp.

tblLinkType.DisplayableProperties =
  "Id;FirstTypeObject_Id!Key;SecondTypeObject_Id!Key";

Поля FirstTypeObject_Id!Key и SecondTypeObject_Id!Key в свойстве tblLinkType.DisplayableProperties показывают, что необходимо возвращать первичный ключ из связанных объектов TypeObject, благодаря чему и появляется возможность настроить LookUp. Указываем значения «FirstTypeObject_Id!Key» и «SecondTypeObject_Id!Key» для свойств FieldName объектов колонок, затем в свойстве ColumnEdit соответствующих колонок задаем репозиторий типа RepositoryItemLookUpEdit. Далее необходимо настроить репозиторий, назначенный для колонки, отображающей FirstTypeObject_Id, и объявленный как RepositoryItemLookUpEdit repositoryItemLookUpEdit1 = new RepositoryItemLookUpEdit():

repositoryItemLookUpEdit1.DataSource = xpCollectionTypeObject;
repositoryItemLookUpEdit1.DisplayMember = "Name";
repositoryItemLookUpEdit1.ValueMember = "Id";

Для колонки, отображающей SecondTypeObject_Id, репозиторий типа RepositoryItemLookUpEdit назначается аналогично.

Мы рассмотрели, как происходят операции по манипуляции с типами и связями между ними, теперь перейдем непосредственно к древовидному отображению элементов с помощью компонента DevExpress.XtraTreeList.TreeList, который разместим на главной форме приложения. На главной форме, помимо выше представленного компонента TreeList, будет находится меню, позволяющее вызывать диалоговые окна для взаимодействия с типами элементов и связями между ними (рисунок 6).

Настройка объекта TreeList treeList1 = new TreeList() осуществляется следующим образом:

treeList1.DataSource = tblTreeObject;
treeList1.KeyFieldName = "Id";
treeList1.ParentFieldName = "KeyParent_Id";

Здесь KeyFieldName – это наименование поля, являющегося первичным ключем таблицы, а ParentFieldName – наименование поля, содержащего значение внешнего ключа. В свойстве DataSource указывается источник данных, в нашем случае это коллекция _tblTreeObject. В коллекции tblTreeObject свойству DisplayableProperties необходимо задать значение:

tblTreeObject.DisplayableProperties = "Name;Id;Type_Id!Key;KeyParent_Id";

Колонку, отображающую тип элемента необходимо настроить способом, аналогичным настройке LookUp для GridControl. Поле Type_Id!Key в свойстве tblTreeObject.DisplayableProperties показывает, что необходимо возвращать первичный ключ из связанного объекта Type_Id, благодаря чему и появляется возможность настроить LookUp.

repositoryItemLookUpEdit1.DataSource = tblTypeObject;
repositoryItemLookUpEdit1.DisplayMember = "Name";
repositoryItemLookUpEdit1.ValueMember = "Id";


Рисунок 6. Основное диалоговое окно программы с меню и древовидным отображением элементов иерархической структуры.

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

treeList1.InvalidNodeException += treeList1_InvalidNodeException;  

а затем реализовать метод:

      void treeList1_InvalidNodeException(object sender, DevExpress.XtraTreeList.InvalidNodeExceptionEventArgs e)
{
// Отобразим текст ошибки, возможно, заранее разобрав его.// Используя значение свойства e.ExceptionMode,// зададим дальнейшее поведение компонента

}

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

Источники

  1. Земсков, Г. Иерархические справочники с линейным временем доступа. – URL: http://www.rsdn.ru/article/db/Dewey.xml?print
  2. Голованов, М. Иерархические структуры данных в реляционных БД. - URL: http://www.rsdn.ru/article/db/Hierarchy.xml?print
  3. Кашменский, Е. Навигация по иерархиям и сетям в реляционных базах данных. - URL: http://www.rsdn.ru/article/db/db_nav1.xml?print
  4. DevExpress - URL: http://www.devexpress.com/
  5. .NET Object-Relational Mapper Tool - URL: http://www.devexpress.com/Products/NET/ORM/
  6. Шаров, Д.А. Разработка и стандартизация программных средств и информационных технологий: Учебное пособие. – Омск: Изд-во ОмГПУ, 2009. – 212 с.
  7. http://allroundautomations.com/


Эта статья опубликована в журнале RSDN Magazine #3-2009. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 60        Оценить