Pull to refresh

Fluent NHibernate и Oracle

Reading time12 min
Views16K
В этом топике я хотел бы осветить библиотеку Fluent NHibernate в связке с Oracle и привести небольшой пример. Статей по этой теме сейчас немного (упоминание на хабре), еще меньше описаний взаимодействия с Oracle. Естественно, большинство не на русском языке. Однако, библиотека заслуживает внимания.

Авторы поставили себе задачу избавить NHibernate от XML (так сказать xmlless). Поэтому, в нашем проекте его не будет. Совсем. Вместо этого предоставляется fluent («текучий» или «гибкий») интерфейс, использующий лямбда-выражения и цепочки методов для настройки базы данных и маппинга сущностей.

Реквизиты


Нам понадобится:
  • Oracle Server (в моем случае 11.2.0.1.0)
  • Oracle DAC (Data Access Components, в моем случае 11.2.0.2.1), Oracle Client или любой другой Ваш провайдер доступа к БД Oracle.

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

%Oracle DAC/Client%\product\11.2.0\client_1\Network\Admin\tnsnames.ora

В моем случае файл имеет вид:
ORCL =
 (DESCRIPTION =
  (ADDRESS = (PROTOCOL = TCP)(HOST = orasrvxp)(PORT = 1521))
  (CONNECT_DATA =
   (SERVER = DEDICATED)
   (SERVICE_NAME = orcl)
  )
 )

Создаем проект


Создадим новый консольный проект в Visual Studio 2010 и назовем его FluentExample. Я использую NuGet Package Manager чтобы быстро найти, получить и установить необходимые пакеты. Инсталляцию библиотеки можно провести и вручную.

Щелкаем правой кнопкой на файле проекта и нажимаем «Add Library Package Reference…». Во вкладке Online в поле поиска введем «fluent»:

Добавляем ссылки

Помимо самого Fluent NHibernate будут загружены и все зависимости (Castle.Core, Iesi.Collections, NHibernate, NHibernate.Castle).

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

В проекте создайте две подпапки: Entities и Mappings, в которых будут находиться сущности и, соответственно, отображения.

Описываем сущности


Первая сущность, работник. У работника есть имя, фамилия и место работы — магазин. В папку Entities добавим новый класс со следующим кодом:
public class Employee
{
  public virtual int Id { get; private set; }
  public virtual string FirstName { get; set; }
  public virtual string LastName { get; set; }
  public virtual Store Store { get; set; }
}

* This source code was highlighted with Source Code Highlighter.

Обращаю Ваше внимание на две вещи:
  • Здесь и далее свойство Id и свойства списков (one-to-many) объявляются с приватным сеттером. Только NHibernate будет устанавливать значения для Id (через рефлексию), а списки мы инициализируем в конструкторе.
  • Все свойства объявлены с модификатором virtual, так как NHibernate создаст прокси-классы для наших сущностей, и необходимо дать возможность перегрузки свойств.

Далее на очереди товар, у которого есть название, цена и список магазинов, им торгующих:
public class Product
{
  public virtual int Id { get; private set; }
  public virtual string Name { get; set; }
  public virtual double Price { get; set; }
  public virtual IList<Store> StoresStockedIn { get; private set; }

  public Product()
  {
    StoresStockedIn = new List<Store>();
  }
}

* This source code was highlighted with Source Code Highlighter.

Список имеет приватный сеттер и инициализация происходит в конструкторе, как описано выше.

Класс магазина, имеющего название, а так же списки товаров и персонала:
public class Store
{
  public virtual int Id { get; private set; }
  public virtual string Name { get; set; }
  public virtual IList<Product> Products { get; private set; }
  public virtual IList<Employee> Staff { get; private set; }

  public Store()
  {
    Products = new List<Product>();
    Staff = new List<Employee>();
  }

  public virtual void AddProduct(Product product)
  {
    product.StoresStockedIn.Add(this);
    Products.Add(product);
  }

  public virtual void AddEmployee(Employee employee)
  {
    employee.Store = this;
    Staff.Add(employee);
  }
}

* This source code was highlighted with Source Code Highlighter.

В этом классе мы описываем немного логики, так как NHibernate перед тем как сохранять связанные сущности требует чтобы обе стороны отношения были заданы.

На этом описание наших сущностей заканчивается. Связывающую таблицу для реализации отношения many-to-many между товаром и магазином мы рассмотрим ниже.

Маппим сущности


Далее, при использовании классического NHibernate, Вам надлежало бы начать писать xml файлы маппинга и конфигурации базы. Но именно тут вступает в силу Fluent NHibernate.

Все описываемые далее классы маппинга находятся в папке Mappings. Начнем с отображения класса Employee.
using FluentExample.Entities;
using FluentNHibernate.Mapping;

namespace FluentExample.Mappings
{
  public class EmployeeMap : ClassMap<Employee>
  {
    public EmployeeMap()
    {
      
    }
  }
}

* This source code was highlighted with Source Code Highlighter.

Сам код отображения будет помещен в конструктор класса, унаследованного от ClassMap из библиотеки FluentNHibernate. Для того, чтобы Fluent NHibernate мог найти маппинги необходимо объявлять классы с модификатором public. Сразу же рассмотрим полный маппинг:
public EmployeeMap()
{
  Table("Employee");
  Id(e => e.Id)
    .GeneratedBy.Sequence("Employee_seq");
  Map(e => e.FirstName);
  Map(e => e.LastName);
  References(e => e.Store);
}

* This source code was highlighted with Source Code Highlighter.

Первая строчка, Table(), отвечает за имя таблицы, на которую выполняется отображение. Чуть ниже мы посмотрим, зачем указывать имя явно.

В методе Id() мы указываем какое поле идентифицирует объект (NHibernate: <id>). Если бы мы использовали базу с автоинкрементом, то Fluent NHibernate автоматически распознал бы тип и назначил генератор identity (NHibernate: <generator>). Но, так как автоинкремента в Oracle нету, будем использовать последовательности (sequence), что и указывается далее в цепочке методов.

Следующие две строки указывают на отображение скалярных свойств (NHibernate: <property>), где тип и имя определяются автоматически. Это реализуется с помощью соглашения об именовании свойств. Имя колонки так же можно указать вручную, используя метод Column(), но все же я рекомендую пользоваться соглашениями по умолчанию или задать свои. Подробнее о соглашениях.

Следующая строка показывает, что наш объект ссылается на класс Store в единственном числе, то есть реализует сторону «many» в отношении many-to-one. Имя внешнего ключа, опять же, следуя соглашениям, предполагается как Store_id. Колонку внешнего ключа можно переопределить методом Column(). Метод References() применяется на стороне «many», на другой стороне отношения («one») будет применятся метод HasMany() (NHibernate: <bag>), а соответствующий метод переименования поля ключа – KeyColumn().

Вернемся к методу Table(). Здесь дело в том, что Fluent NHibernate по умолчанию при отображении имен таблиц окружает их кавычками (соглашение такое), то есть, наша таблица сейчас должна была бы иметь название ”Employee”, вместе с кавычками. В Oracle являются чувствительными к регистру только имена, окруженные кавычками. В нашем примере я не хотел бы использовать кавычки и оставить имена таблиц нечувствительными к регистру и без кавычек, поэтому указал их явно.

Перейдем к маппингу магазина:
public class StoreMap : ClassMap<Store>
{
  public StoreMap()
  {
    Table("Store");
    Id(x => x.Id)
      .GeneratedBy.Sequence("Store_seq");
    Map(x => x.Name);
    HasMany(x => x.Staff)
      .Inverse()
      .Cascade.All();
    HasManyToMany(x => x.Products)
      .Cascade.All()
      .Table("StoreProduct");
  }
}

* This source code was highlighted with Source Code Highlighter.

Мы видим здесь симметричный вызов HasMany() для класса Employee, который мы рассмотрели выше. Так же используется метод HasManyToMany(), который реализует отношение many-to-many между магазином и продуктом через промежуточную таблицу, имя которой задано в цепочке методом Table().

Метод Inverse() показывает, что владельцем коллекции является другой конец отношения, то есть класс Employee, и он будет сохранен первым. Cascade.All() передает события связанным элементам. То есть, если мы, например, удалим магазин, то все связи с продуктами и весь персонал данного магазина будет так же удален. Подробнее см. документацию NHibernate.

Наконец, отобразим наш товар:
public class ProductMap : ClassMap<Product>
{
  public ProductMap()
  {
    Table("Product");
    Id(x => x.Id)
      .GeneratedBy.Sequence("Product_seq");
    Map(x => x.Name);
    Map(x => x.Price);
    HasManyToMany(x => x.StoresStockedIn)
      .Cascade.All()
      .Inverse()
      .Table("StoreProduct");
  }
}

* This source code was highlighted with Source Code Highlighter.

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

Теперь пришло время сконфигурировать подключение к Oracle.

Подключаемся к Oracle


Мы будем подключаться, используя connectionString. Добавим в проект новый файл, App.config, и впишем туда нашу строку подключения для пользователя fluent с паролем nhibernate:
Copy Source | Copy HTML
  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3.     <connectionStrings>
  4.         <add name="Oracle"
  5.              connectionString="DATA SOURCE=orcl;PASSWORD=nhibernate;PERSIST SECURITY INFO=True;USER ID=fluent"
  6.              providerName="Oracle.DataAccess.Client" />
  7.     </connectionStrings>
  8. </configuration>
  9.  

Далее надо добавить ссылку на сборку Oracle.DataAccess, поставляемую с выбранным Вами провайдером:



Диалог добавления ссылок у Вас скорее всего будет другим — я использую Productivity Power Tools.

Выставим добавленной ссылке свойство Copy Local = True, это указывает на то, что данную библиотеку нужно копировать в папку с выходными бинарными файлами нашего проекта:



Для удобства воспользуемся небольшим хелпером для открытия сессии в Fluent NHibernate:
using FluentExample.Entities;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Driver;

namespace FluentExample
{
  public static class FluentNHibernateHelper
  {
    private static ISessionFactory _sessionFactory;
    public static ISessionFactory SessionFactory
    {
      get
      {
        if(_sessionFactory == null)
        {
          var dbConfig = OracleDataClientConfiguration.Oracle10
            .ConnectionString(c => c.FromConnectionStringWithKey("Oracle"))
            .Driver<OracleDataClientDriver>()
            .ShowSql();

          _sessionFactory = Fluently.Configure()
            .Database(dbConfig)
            .Mappings(m => m.FluentMappings.AddFromAssemblyOf<Employee>())
            .BuildSessionFactory();
        }
        return _sessionFactory;
      }
    }

    public static ISession OpenSession()
    {
      return SessionFactory.OpenSession();
    }
  }
}

* This source code was highlighted with Source Code Highlighter.

В конфигурации базы данных мы используем предопределенный класс OracleDataClientConfiguration из пространства имен FluentNHibernate.Cfg.Db и настраиваем его для использования созданной нами строки подключения и драйвера NHibernate. Также можно включить опцию для показа сгенерированных SQL-запросов. Подробнее о конфигурации базы данных.

Далее выполняем уже саму настройку NHibernate. Подставляем нашу конфигурацию БД, указываем источник маппингов и запрашиваем фабрику сессий. Подробнее о fluent configuration.

На этом этапе получение сессии должно проходить успешно:
class Program
{
  static void Main()
  {
    using (FluentNHibernateHelper.OpenSession())
    {
      
    }
  }
}

* This source code was highlighted with Source Code Highlighter.

Протестируем нашу базу


Сейчас Ваш проект должен иметь структуру, подобную этой:



Пора запустить настоящий тест:
private static void Main()
{
  using (var session = FluentNHibernateHelper.OpenSession())
  {
    using (var transaction = session.BeginTransaction())
    {
      var barginBasin = new Store { Name = "Bargin Basin" };
      var superMart = new Store { Name = "SuperMart" };

      var potatoes = new Product { Name = "Potatoes", Price = 3.60 };
      var fish = new Product { Name = "Fish", Price = 4.49 };
      var milk = new Product { Name = "Milk", Price = 0.79 };
      var bread = new Product { Name = "Bread", Price = 1.29 };
      var cheese = new Product { Name = "Cheese", Price = 2.10 };
      var waffles = new Product { Name = "Waffles", Price = 2.41 };

      var daisy = new Employee { FirstName = "Daisy", LastName = "Harrison" };
      var jack = new Employee { FirstName = "Jack", LastName = "Torrance" };
      var sue = new Employee { FirstName = "Sue", LastName = "Walkters" };
      var bill = new Employee { FirstName = "Bill", LastName = "Taft" };
      var joan = new Employee { FirstName = "Joan", LastName = "Pope" };

      // Добавляем товары. Некоторые товары повторяются, так как отношение
      // между товаром и магазином установлено как many-to-many.
      AddProductsToStore(barginBasin, potatoes, fish, milk, bread, cheese);
      AddProductsToStore(superMart, bread, cheese, waffles);

      // Каждый работник может работать только в одном магазине
      AddEmployeesToStore(barginBasin, daisy, jack, sue);
      AddEmployeesToStore(superMart, bill, joan);

      // Сохранить оба магазина и обновить всё остальное через каскадирование
      session.SaveOrUpdate(barginBasin);
      session.SaveOrUpdate(superMart);

      transaction.Commit();
    }

    // Отобразить все магазины
    using (session.BeginTransaction())
    {
      var stores = session.CreateCriteria(typeof (Store)).List<Store>();
      foreach (var store in stores)
        WriteStorePretty(store);
    }

    Console.ReadKey();
  }
}

public static void AddProductsToStore(Store store, params Product[] products)
{
  foreach (var product in products)
    store.AddProduct(product);
}

public static void AddEmployeesToStore(Store store, params Employee[] employees)
{
  foreach (var employee in employees)
    store.AddEmployee(employee);
}

private static void WriteStorePretty(Store store)
{
  Console.WriteLine(store.Name);
  Console.WriteLine(" Products:");

  foreach (var product in store.Products)
    Console.WriteLine("  " + product.Name);

  Console.WriteLine(" Staff:");
  foreach (var employee in store.Staff)
    Console.WriteLine("  " + employee.FirstName + " " + employee.LastName);

  Console.WriteLine();
}

* This source code was highlighted with Source Code Highlighter.

Вот и все.
Так же привожу скрипт для генерации схемы в Oracle.

Что дальше?


Нереализованный функционал


Авторы поддерживают огромную часть возможностей родной библиотеки, однако некоторых особенностей NHibernate еще нет, например, <sql-insert>.

Автомаппинг


Fluent NHibernate позволяет делать маппинг автоматически, достаточно просто следовать введенным соглашениям или определить собственные.

Тестирование маппингов


Библиотека позволяет быстро и в том же стиле протестировать созданные маппинги.

Полная API документация

Tags:
Hubs:
Total votes 24: ↑20 and ↓4+16
Comments6

Articles