Oct 22, 2009

NHibernate mappings. Или каталог маппингов

Тут буду собирать полезные статьи относящиеся к маппингу NHibernate.
  • Наследование.
  • property – подробности про маппинг свойств.
  • component – если есть желание иметь вложенные классы в сущностях. Например чтобы у пользователя был адрес в виде объекта и обращаться  к нему можно было как user.Address.Street.
  • one-to-one – связь один к одному.
  • any – для связи таблиц, которые не связаны явно. (не знаю как это описать более точно, надо читать :) ).
  • many-to-any – расширение any маппинга.
  • join – аналог one-to-one, но позволяет замапить несколько таблиц в один объект.
  • dynamic-component – позволяет вынести набор свойств объекта в Dictionary. Возможно редко используемые свойства есть смысл туда вынести, чтобы не засорять сущность.
  • set – один из наиболее часто используемых элементов для маппинга коллекций.
  • Named queries <query/> and <sql-query/> – если нужно встраивать свой sql код в маппинги.
  • map – никогда не использовал сам. Но это еще один вариант организации связей между сущностями. Когда посмотрю возможно опишу более подробно.
  • list – главное отличие list от set – list может содержать повторяющиеся записи. Это значит что если вы попытаетесь в set добавить элемент, который уже содержится в коллекции (проверяется на равенство при помощи метода Equals()), то элемент не будет добавлен. Если же последнее действие проделать с list, то элемент может быть добавлен.

Далее рассмотрим наиболее часто встречающиеся структуры:

  1. Дерево. По поводу дерева еще один пост про то, как его эффективно вытаскивать из базы

Каталог буду пополнять по мере поступления :).

Oct 21, 2009

NHibernate transactions. Почему рекомендуют все запросы выполнять внутри транзакций

Если выполнить для доменной модели (описана тут) следующий код без транзакции:

Category category = new Category
     {
          DisplayName = "Our first category"

     };
Product product = new Product
     {
         Description = "First product description",
         Name = "First product",
         Price = 1
     };
Order order = new Order(product)
     {
         Customer = "customer"

     };
Global.CurrentSession.SaveOrUpdate(category);
Global.CurrentSession.SaveOrUpdate(product);

То SqlProfiler покажет следующее:

image

Как вы видите операции логин/логаут выполняются после каждого запроса. Если же использовать код в global.asax, как описано ранее, то получим следующий результат:

image

И это только на 3х запросах, в реальных же приложениях это может существенно повлиять на производительность.

Oct 20, 2009

NHibernate для начинающих. Часть 3. Связи

Линки на остальные статьи

Изначально этот пост задумывался как описания каждого типа связи. Я собирался описать как мапить many-to-many, many-to-one, one-to-many и т.д. Но начав писать, я понял что такой информации и так достаточно в сети. Поэтому пост больше получился про то, как построить доменную модель для связанных сущностей. Данный подход наверняка можно распространить и на любое другое нормальное ORM средство.

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

  Диаграмма классов

На этой схеме изображено следующее:

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

Для хранения данной доменной модели была построена следующая база данных:

 Схема базы данных

Исходный текст классов таков:

public class Category  
{     
    private ISet<Product> products;     
    public Category()     
    {         
        products = new HashedSet<Product>();     
    }
    public virtual string DisplayName { get; set; }     
    public virtual int Id { get; protected set; }     
    public virtual ReadOnlyCollection<Product> Products     
    {        
        get { return new ReadOnlyCollection<Product>(new List<Product>(products));  }     
    }     
    public virtual void AddProduct(Product product)     
    {        
        if(product == null) throw new ArgumentNullException("product");     
        if(!products.Contains(product))         
        {
            product.AddCategory(this);            
            products.Add(product);        
        }     
    }     
    public virtual void RemoveProduct(Product product)     
    {        
        if(product == null) throw new ArgumentNullException("product");
        if(products.Contains(product))
        {            
            products.Remove(product);
            product.RemoveFrom(this);
        }
     }
}
 

Следует обратить внимание на следующие вещи:

  • Поле Id – protected. Это необходимо для защиты данных. Если нечайно поменять Id то можно обновить данные другого объекта, а не ожиемого
  • Все поля и методы должны быть виртуальными. Это необходимо для работы NHiberante
  • В конструкторе инициализируется колекция продуктов, таким образом никогда не будет NullReferenceException
  • Колекция продуктов представлена как ReadOnlyCollection. Такой подход оставляет нам возможность получить полный контроль над логикой добавления и удаления продуктов внутри категории
  • При добавлении продукта в категорию продукту автоматически добавляется эта категория. Это обеспечивает согласованность доменной модели (не может быть такого случая, что в колекции у категории продукт есть, а у продукта нет информации о том, что он содержится в категории)
  • Проверка приходящих параметров. Нельзя пробовать добавлять пустые ссылки на продукты
public class Product
{
     private ISet<Category> categories;
     public Product()
     {
        categories = new HashedSet<Category>();
        Orders = new HashedSet<Order>();
     }
     public virtual int Id { get; protected set; }
     public virtual string Name { get; set; }
     public virtual string Description { get; set; }
     public virtual double Price { get; set; }
     public virtual ReadOnlyCollection<Category> Categories
     {
        get
        {
           return new ReadOnlyCollection<Category>(new List<Category>(categories));
        }
     }
     public virtual ISet<Order> Orders { get; protected set; }
     public virtual void AddCategory(Category category)
     {
         if(category == null)
             throw new ArgumentNullException("category");
         if(!categories.Contains(category))
         {
             categories.Add(category);
             category.AddProduct(this);
         }
      }
      public virtual void RemoveFrom(Category category)
      {
         if(category == null)
             throw new ArgumentNullException("category");
         if(categories.Contains(category))
         {
             categories.Remove(category);
             category.RemoveProduct(this);
         }
       }
}  

Так же как и у категории мы обеспечиваем полный контроль над добавлением и удалением элементов из колекции. Но мы не делаем коллекцию заказов ReadOnly (но по прежнему инициализируем пустой колекцией для отсутствия NullReferenceException в нашем коде). Далее будет рассмотрено почему.

public class Order
{
      protected Order() { }
      public Order(Product product)
      {
         if(product == null)
             throw new ArgumentNullException("product");
         product.Orders.Add(this);
         Product = product;
      }
      public virtual int Id { get; protected set; }
      public virtual Product Product{get;protected set;}
      public virtual int NumberOfItems { get; set; }
      public virtual string Customer { get; set; }
}  

Важно выделить следующее

  • protected констуктор необходим для работы NHiberante
  • Тут можно увидеть почему колекция заказов осталась открытой. Чтобы добавить туда новый элемент необходимо сначала его создать. Но заказ не может существовать без продукта, что и отражено единственном открытом конструкторе
  • Product для заказа содержит protected сеттер. Это сделано для того, чтобы нельзя было у созданного заказа заменить продукт, который покупался.
  • Заказов без продукта существовать не должно. Соответственно при удалении продукта или же удалении связи между продуктом и заказом заказ должен быть удален. Как этого достичь будет показано далее

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

private ITransaction Transaction;
protected void Application_BeginRequest(object sender, EventArgs e)
{
      CurrentSession = SessionFactory.OpenSession();
      Transaction = CurrentSession.BeginTransaction();
}
protected void Application_EndRequest(object sender, EventArgs e)
{
      if (Server.GetLastError() == null)
      {
         Transaction.Commit();
      }
      else
      {
         Transaction.Rollback();
      }
      if (CurrentSession != null)
         CurrentSession.Dispose();
}   

И наконец маппинги:

public class CategoryMap: ClassMap<Category>
{
      public CategoryMap()
      {
          Table("Categories");
          Id(x => x.Id).GeneratedBy.Native();
          Map(x => x.DisplayName);
          HasManyToMany(x => x.Products)
                             .Access.CamelCaseField()
                             .Cascade.SaveUpdate()
                             .AsSet()
                             .ParentKeyColumn("CategoryId")
                             .ChildKeyColumn("ProductId")
                             .Table("[Products.Categories]");
      }
 }
public class ProductMap : ClassMap<Product>  
{
      public ProductMap()
      {
         Table("Products");
         Id(x => x.Id).GeneratedBy.Native();
         Map(x => x.Description);
         Map(x => x.Name);
         Map(x => x.Price);
         HasManyToMany(x => x.Categories)
                           .Access.CamelCaseField()
                           .Table("[Products.Categories]")
                           .ParentKeyColumn("ProductId")
                          .ChildKeyColumn("CategoryId")
                          .AsSet()
                          .Inverse()
                          .Cascade.SaveUpdate();
         HasMany(x => x.Orders)     
                      .KeyColumn("ProductId")     
                      .Inverse() 
                      .Cascade.AllDeleteOrphan();
      }
  }
  public class OrderMap:ClassMap<Order>
  {
   public OrderMap()
   {
    Table("Orders");
    Id(x => x.Id).GeneratedBy.Native();
    Map(x => x.NumberOfItems);
    Map(x => x.Customer);
    References(x => x.Product, "ProductId");
   }
  }

На что обратить внимание

  • У продукта и категории стоит Cascade.SaveUpdate() для колекции категорий и продукта соответственно. Это значит что сохранение продукта вызывает сохранение категории и наоборот. Чуть более подробно про каскадирование можно почитать тут
  • У колекций продукта стоит Inverse. Это означает что за сохранение связи между категорией и продуктом будет отвечать категория. Более подробно тут
  • У продукта на список заказов стоит Cascade.AllDeleteOrphan(). Это обязывает Nhibernate удалять записи, которые лишились родителя (дополнительно тут)
  • Для many-to-many установлен Access.CamelCaseField(). Это означает, что NHibernate будет использовать приватное поле для работы, это позволяет нам сделать коллекцию продуктов ReadOnlyCollection

Итак теперь можно поработать с полученной моделью:

Category category = new Category
    {
        DisplayName = "Our first category"
    };
Product product = new Product
    {
        Description = "First product description",
        Name = "First product",
        Price = 1
    };
category.AddProduct(product);
Global.CurrentSession.SaveOrUpdate(category); 

Эта часть кода заставит NHiberante выполнить следующий набор sql запросов:

INSERT INTO Categories (DisplayName) VALUES (@p0); select SCOPE_IDENTITY() @p0=N'Our first category'  
INSERT INTO Products (Description, Name, Price) VALUES (@p0, @p1, @p2); select SCOPE_IDENTITY() @p0=N'First product description',@p1=N'First product', @p2=1  
INSERT INTO [Products.Categories] (CategoryId, ProductId) VALUES (@p0, @p1) @p0=1,@p1=1  

Как можно видеть NHiberante сам позаботился о порядке сохранения объектов, и заполнении связующей таблицы.

Теперь проверим связь продуктов и заказов:

Product product = Global.CurrentSession.Get<Product>(1); 
var order = new Order(product)
 {
    NumberOfItems = 5,
    Customer = "customer"
 }; 
Global.CurrentSession.SaveOrUpdate(product); 

Это заставит NHiberante выполнить следующие запросы:

SELECT product0_.Id as Id2_0_, product0_.Description as Descript2_2_0_, product0_.Name as Name2_0_, product0_.Price as Price2_0_  
FROM Products product0_ 
WHERE product0_.Id=@p0,@p0=1  

SELECT orders0_.ProductId as ProductId1_, orders0_.Id as Id1_, orders0_.Id as Id3_0_, orders0_.NumberOfItems as NumberOf2_3_0_, orders0_.Customer as Customer3_0_, orders0_.ProductId as ProductId3_0_  
FROM Orders orders0_  
WHERE orders0_.ProductId=@p0,@p0=1  

INSERT INTO Orders (NumberOfItems, Customer, ProductId) VALUES (@p0, @p1, @p2); select SCOPE_IDENTITY(), @p0=5,@p1=N'customer',@p2=1  

(id могут быть другими).

Проверим сценарий в котором удаление связи между продуктом и заказом должно вызывать удаление заказа (это достигается при помощи all-delete-orphan). Например рассмотрим следующий код:

Product product = Global.CurrentSession.Get<Product>(1);
product.Orders.Clear(); 
Global.CurrentSession.SaveOrUpdate(product);

Он выполнит следующие запросы:

SELECT product0_.Id as Id2_0_, product0_.Description as Descript2_2_0_, product0_.Name as Name2_0_, product0_.Price as Price2_0_  
FROM Products product0_ 
WHERE product0_.Id=@p0, @p0=1  

SELECT orders0_.ProductId as ProductId1_, orders0_.Id as Id1_, orders0_.Id as Id3_0_, orders0_.NumberOfItems as NumberOf2_3_0_, orders0_.Customer as Customer3_0_, orders0_.ProductId as ProductId3_0_ 
FROM Orders orders0_  
WHERE orders0_.ProductId=@p0, @p0=1  

DELETE FROM Orders WHERE Id = @p0 @p0=2  
DELETE FROM Orders WHERE Id = @p0 @p0=3 
DELETE FROM Orders WHERE Id = @p0 @p0=4  

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

Пример кода

Oct 16, 2009

Agent smith plugin

Я думаю многие используют такой плагин для вижуал студии как Resharper. Он делает жизнь на много проще и про него писать нет смысла, но вот есть к Resharpery отличный плагин. Называется Agent Smith. Он автоматически проверяет правильность написания слов в коде. Крайне полезная штука для отлавливания опечаток в коде.

Еще одной функцией этого плагина является проверка правил именования. Например что приватные поля должны называться в стиле _myField. Но в Resharper 4.5 уже есть подобная функциональность, и чтобы они друг другу не мешали в агенте её можно выключить:

resharperOptions

Oct 15, 2009

Атрибут DebuggerDisplay

Это одна из моих любимых возможностей по улучшению процесса дебага. Рассмотрим следующий пример. Пусть есть объект Product:

public class Product 
{ 
    public virtual int Id { get; set; }
    public virtual string Name { get; set; }
    public virtual string Description { get; set; }
    public virtual double Price { get; set; } 
}

Если навести на него в debug режиме, то мы увидим следующее:

productNotExpanded C одной стороны все нормально, если навести на плюсик, то получим полную информацию:

productExpanded  А теперь посмотрим как будет выглядеть список этих объектов:

listOfProductUgly Соответственно чтобы найти в этом списке продукт с Id = 5 приходится последовательно разворачивать каждый (особенно если список не отсортирован).

Для удобочитаемости объекта в дебаге можно воспользоваться атрибутом DebuggerDisplay. Он позволяет изменять вид отображения объекта. Например если добавить следующий атрибут к классу Product:

[DebuggerDisplay("Id = {Id}, Name= {Name}, Price = {Price}")]
public class Product 
//... далее описание класса

В фигурных скобках указываются интересующие нас свойства. Результат:

listOfProductNice Этой краткой информации вполне достаточно чтобы понять что находится в коллекции и найти там нужный элемент.

Oct 7, 2009

NHibernate для начинающих. Часть 2. Варианты маппингов

Линки на остальные статьи

Любое бизнес приложение содержит набор некоторых объектов, комбинация который представляет собой доменную модель приложения. Например для электронного магазина такими доменными объектами могут быть: Product, Category, Order и т.д.

Для NHibernate необходимы знания о вашей доменной модели для сохранения и извлечения соответствующих объектов. Для этого используется подход называемый data mapper.

В этой статье будут рассмотрены доступные на данный момент варианты маппингов  NHibernate. Будут рассмотрены:

  • Xml файлы – базовый способ маппинга, все остальные способы сводятся к получению таких xml файлов.
  • Аттрибуты – каждому полю или классу добавляются специальные аттрибуты, которые в дальшем преобразовываются в xml.
  • Fluent маппинг – маппинг при помощи лямбда выражений.

Рассмотрим каждый из них подробнее.

1 Xml файлы

Базовый способ маппинга был показан в первой статье. Могу выделить лишь следущюее:

Преимущества:

  • Самый первый из разработанных в NHibernate, соответственно в интернете множество можно найти примеров.
  • Достаточно .net 2.0

Недостатки:

  • Отсутствие валидации во время компиляции
  • Невозможность переименовать поля при помощи Refactor (в вижуал студии есть отличная возможность для удобного переименовывания полей и методов. Для этого на нужном методе клацаем правой кнопкой, далее в меню выбираем Refactor->Rename. Эта опция переименует метод и обновит все вывозы этого метода или свойства в проекте)
  • Отсутствеие полноценного intellisense (есть xsd схема для маппингов, но имена полей вашей сущности показывать все равно не будет)

2 Аттрибуты

Рассмотрим пример из первой части, только воспользуемся маппингами с помощью аттрибутов. Скачать нужную для этого библиотеку можно тут. После того, как добавлена ссылка на NHibernate.Mapping.Attributes класс Product (описан в первой части) должен выглядеть следующим образом:

[Class(Table = "Products", Name = "InheritanceCore.Product")]
public class Product
{
 [Id(0, Column = "Id", Type = "int", Name="Id")]
 [Generator(1, Class = "native")]
 public virtual int Id { get; protected set; }

 [Property(Name = "Name", Column = "Name", Type = "String")]
 public virtual string Name { get; set; }

 [Property(Name = "Description", Column = "Description", Type = "String")]
 public virtual string Description { get; set; }

 [Property(Name = "Price", Column = "Price", Type = "Double")]
 public virtual double Price { get; set; }
}

Также надо изменить Global.asax следующим образом:

protected static ISessionFactory CreateSessionFactory()
{
 var config = new Configuration().Configure(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "hibernate.config"));
 HbmSerializer.Default.Validate = true;
 var assembly = Assembly.GetAssembly(typeof (Product));
 HbmSerializer.Default.HbmAssembly = assembly.FullName;
 HbmSerializer.Default.HbmNamespace = typeof(Product).Namespace;
 var stream = HbmSerializer.Default.Serialize(assembly);
 config.AddInputStream(stream);
 return config.BuildSessionFactory();
}

Вот и все, остальное остается с первой части без изменений. Таким образом:

Преимущества:

  • Сущности и маппинги находятся рядом
  • Наличие некоторого intellisense

Недостатки

  • Наличие строковых литералов. (Свойства Name, Type). Из-за этого невозможно использовать Refactor и полноценно переименовывать свойства классов
  • Отсутствие валидации во время компиляции. На самом деле можно все атрибуты повесить на одно свойство, результат будет такой же
  • Доменные объекты засоряются огромным количеством аттрибутов, которые усложняют читабельность кода


3 Fluent маппинг

Это наиболее новый способ маппинга.
Рассмотрим как с его помощью замапить тот же самый класс Product.

В этом случае надо изменить метод в global.asax следующим образом:


protected static ISessionFactory CreateSessionFactory()
{
return Fluently.Configure()
               .Database(MsSqlConfiguration.MsSql2005.ConnectionString(@"your connection string"))
               .Mappings(m => m.FluentMappings.AddFromAssemblyOf<product>())
               .BuildSessionFactory();
}

Для маппинга в проект добавляется еще один класс. Как правило его называют <ClassName>Map, т.е. у для примера Product это будет класс ProductMap. Его содержимое:

public class ProductMap : ClassMap<Product>
{
 public ProductMap()
 {
    Table("Products");
    Id(x => x.Id).GeneratedBy.Native();
    Map(x => x.Description).Column("Description");
    Map(x => x.Name).Column("Name");
    Map(x => x.Price).Column("Price");
 }
}

Тут мы имеем полный intellisense, и все прелести проверки время компиляции.

Jquery date picker и форматы дат

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

1. Добавляем календарь следующим образом:

$(".datePick").datepicker({ 
    dateFormat: '<%= ConstantHelper.JQUERY_DATE_FORMAT %>'
});

2. ConstantHelper

CultureInfo.CurrentCulture.DateTimeFormat
 .ShortDatePattern.Replace("M", "m")
    .Replace("yy", "y");

Добавленный таким образом календарь будет выдавать даты в формате текущей культуры, и они будут совпадать с .net форматами.