Работа с иерархическими данными в ASP.Net MVC. Часть 1

В этой статье я хочу описать проблемы, которые встречаются при работе с иерархическими данными в ASP.Net MVC с использованием связки нескольких популярных технологий: NHibernate, Fluent NHibernate, AutoMapper ну и конечно самой ASP.Net MVC. ASP.Net MVC способствует созданию приложений, которые будет приятно разрабатывать, отлаживать и сопровождать, принося больше value для бизнеса. Но для того, чтобы реализовать эффективный и полезный проект, необходимо использовать совокупность технологий для решения поставленных задач. Вот почему я не буду ограничиваться лишь отображением иерархических данных в ASP.Net MVC. Я расскажу о работе с ними в приложении целиком: от модели предметной области и сохранении в базе данных до создания модели представления (view model) и отображения ее в UI. Под иерархическими данными я подразумеваю сущности, для которых тем или иным образом определен порядок родительский элемент-дочерний элемент.

Рассмотрим достаточно распространенный случай в приложениях для электронной коммерции: предположим, что у нас есть интернет-магазин, в котором пользователи могут просматривать и выбирать товары из различных категорий. Категории могут быть вложенными, например категория “Книги” может иметь в свою очередь несколько под категорий со специализированной литературой “Техническая литература”, “Детские книги”, “Художественная литература” и т.п. В нашем случае вложенные категории являются примером иерархических данных, которые необходимо обрабатывать.

Определим технологии, которые используются для интернет-магазина. Как я уже сказал выше, это ASP.Net MVC приложение. Архитектура приложения будет создана на основе принципов DDD. Данные хранятся в базе данных Sql Server. Для отображения данных из объектной среды в реляционную используется NHibernate. Для описания конфигураций отображений используем Fluent NHibernate, т.к. он позволяет использовать code-first подход и не требует написания xml файлов, что более чревато проблемами с сопровождением и отладкой, т.к. нет compile-time проверки. Для отображения объектов доменной модели (domain model) в модель отображения (view model) используем AutoMapper, созданный Jimmy Bogard-ом (координатором другого популярного проекта NBehave для использования BDD подхода в .Net приложениях).

Начнем с самого главного – с модели. Итак, у нас есть класс Category, описанный следующим образом:

public class Category : PersistentObject<int>
{
    /*nhibernate requires all properties be virtual*/
    public virtual string Name { get; set; }
    public virtual Category ParentCategory { get; set; }
    public virtual IList<Category> ChildCategories { get; set; }
    public virtual IList<Product> Products { get; set; }
    public virtual bool IsActive { get; set; }
}

У каждой категории есть название, список дочерних категорий, список товаров, входящих в эту категорию (свойство IsActive введено для реализации так называемого мягкого удаления (soft delete) – см. например Soft Deletes. Эта тема, хотя и очень интересная, вне скопа данной статьи). Кроме того, каждая категория имеет ссылку на родительскую категорию. При этом orphan-категории, у которых нет предка, являются категориями самого высокого порядка в иерархии. PersistenObject<T> – это супер-класс слоя (layer superclass) для всех persistent-объектов (объектов, чье состояние  хранится в базе). В нем переопределены методы Equals() и GetHashCode() для реализации семантики тождества объектов по их идентификаторам (два persistent-объекта с одинаковыми идентификаторами являются тождественными):

public abstract class PersistentObject<T>
{
    public virtual T Id { get; set; }
 
    public virtual bool IsPersistent
    {
        get { return (!Id.Equals(default(T))); }
    }
 
    public override bool Equals(object obj)
    {
        if (IsPersistent)
        {
            if (obj == null)
            {
                return false;
            }
 
            if (GetType() != obj.GetType())
            {
                return false;
            }
 
            var persistentObject = obj as PersistentObject<T>;
            return (persistentObject != null) && (this.Id.Equals(persistentObject.Id));
        }
 
        return base.Equals(obj);
    }
 
    public override int GetHashCode()
    {
        return IsPersistent ? Id.GetHashCode() : base.GetHashCode();
    }
}

Для категорий выбран подход родительская сущность-дочерняя сущность (parent-child) для представления их в виде иерархии. Этот подход более прост в реализации и используется во многих приложениях. Но с большими объемами данных вы можете столкнуться с проблемами производительности с этим подходом (из-за особенностей представления в Sql базе данных – см. ниже). Для решения подобных проблем существуют другие способы представления иерархий – например вложенные множества (nested sets). За примерами я отсылаю вас на codeproject: Improve hierarchy performance using nested sets.

Следующий шаг – это отображение категории в реляционную модель. Заметьте тонкий момент: до сих пор я говорил только о модели в коде, не слова не сказав про DDL описание таблицы для категорий, хотя множество примеров по ORM начинают именно с описания схемы БД и лишь затем переходят к непосредственной модели. При этом отображение данных преподносится как решение принципиальной проблемы несоответствия реляционной и объектной среды. Я же начал с модели – т.к. именно вокруг нее строятся все сервисы приложения, она является нашим пониманием процессов бизнеса и от четкости и правильности модели зависит успешность всего приложения в целом. Вы можете использовать самые передовые технологии, лучшие средства разработки, какие угодно методологии – без правильной модели ваш проект вряд ли станет успешным. В то же время с четкой моделью (которую может понять даже нетехнический специалист предметной области бизнеса) вы будете уверены, что делаете именно то, что нужно заказчику, причем делаете это эффективно, так что ваша работа будет приносить вам удовольствие, а не pain-in-ass из-за очередного специального случая, ломающего всю архитектуру. Эта концепция является ключевой в DDD. За более подробной информацией я отсылаю вас к книге Эванса Domain-Driven Design by Eric Evans.

Итак, модель предметной области, выраженная в коде, первична. На основе модели мы будем создавать базу данных (в NHibernate есть инструменты для экспорта схемы базы из отображаемой модели). Но для этого нам нужно описать, как наша модель будет представлена в базе данных. Для этого мы используем Fluent NHibernate (я не буду повторятся о полезности code-first-а):

public class CategoryMap : ClassMap<Category>
{
    public CategoryMap()
    {
        Table("[Category]");
        Id(x => x.Id, "CategoryId");
        
        Map(x => x.Name).Length(256)
            .Not.Nullable()
            .Unique();
 
        Map(x => x.IsActive).Not.Nullable().Default("1");
        
        References(x => x.ParentCategory)
            .Column("ParentCategoryId");
        
        HasMany(x => x.ChildCategories)
            .KeyColumn("ParentCategoryId").Inverse()
            .AsBag();
 
        HasManyToMany(x => x.Products)
            .Table("[ProductCategory]")
            .ParentKeyColumn("CategoryId")
            .ChildKeyColumn("ProductId")
            .AsBag();
    }
}

Описание достаточно self-descriptive. Мы указали название таблицы, название primary key и его соответствие свойству Id, указали соответствия между другими свойствами и колонками таблицы. Обратите внимание, как мы сконфигурировали self-reference категорий:

References(x => x.ParentCategory)
    .Column("ParentCategoryId");
 
HasMany(x => x.ChildCategories)
    .KeyColumn("ParentCategoryId").Inverse()
    .AsBag();

Как результат в таблице Category будет внешний ключ (foreign key) на саму себя. Вот какой sql получится если мы экспортируем схему с использованием класса SchemaExport из NHibernate:

CREATE TABLE [dbo].[Category](
    [CategoryId] [int] IDENTITY(1,1) NOT NULL,
    [Name] [nvarchar](256) NOT NULL,
    [IsActive] [bit] NOT NULL,
    [ParentCategoryId] [int] NULL,
 CONSTRAINT [PK_Category] PRIMARY KEY CLUSTERED 
(
    [CategoryId] ASC
)WITH (PAD_INDEX  = OFF, ...) ON [PRIMARY],
 CONSTRAINT [UQ_Category_Name] UNIQUE NONCLUSTERED 
(
    [Name] ASC
)WITH (PAD_INDEX  = OFF, ...) ON [PRIMARY]
) ON [PRIMARY]
 
GO
 
ALTER TABLE [dbo].[Category]  WITH CHECK
ADD CONSTRAINT [FK_Category_ParentCategoryId_Category_CategoryId] FOREIGN KEY([ParentCategoryId])
REFERENCES [dbo].[Category] ([CategoryId])
GO
 
ALTER TABLE [dbo].[Category] CHECK CONSTRAINT [FK_Category_ParentCategoryId_Category_CategoryId]
GO
 
ALTER TABLE [dbo].[Category] ADD  CONSTRAINT [DF_Category_IsActive]  DEFAULT ((1)) FOR [IsActive]
GO

Заметьте, что мы сами не написали ни строчки Sql-кода. Он был сгенерирован автоматически на основе модели. Единственный момент, что для большей удобочитаемости был применен скрипт, который изменяет названия constraints на более привлекательные и понятные. Для примеры я отсылаю вас к open source проекту CodeCampServer, который использовался в книге Jeffrey Palermo ASP.NET MVC in Action. В нем, в частности, реализована настройка создания и миграции схемы БД на основе текущей модели для CI (continuous integration) с использованием проекта Tarantino.Net. Недавно я выложил для их скрипта патч, который также фиксит названия Unique и Default constraints: http://codecampserver.codeplex.com/Thread/View.aspx?ThreadId=231871.

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

Реклама

Об авторе sadomovalex

Старший инженер, team lead, консультант. Работаю в стеке .Net. Последние несколько лет занимаюсь разработкой enterprise приложений под Sharepoint, чему и будет в основном посвящена тематика этого блога. Также активно использую и интересуюсь ASP.Net MVC, DDD, TDD, Agile. Активно участвую в жизни многих профессиональных сообществ, SPb .Net UG, SPb ALT.Net, rsdn, Finland SP UG и др.
Запись опубликована в рубрике ASP.Net MVC, DDD, IoC. Добавьте в закладки постоянную ссылку.

3 комментария на «Работа с иерархическими данными в ASP.Net MVC. Часть 1»

  1. annchousinka:

    Здравствуйте!а куда этот код вставлять надо?

  2. annchousinka:

    можно по подробней описать, как и где создать. т.к. это ASP то в Visual Studio можно?

  3. annchousinka,
    по умолчанию все примеры в этом блоге для Visual Studio. Примеры из этого поста написаны на C# и T-SQL.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s