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

Дополнительные ключи в системах объектно-реляционного отображения

Практическое использование дополнительных ключей на примере BLToolkit

Автор: Смирнов Олег Сергеевич
Источник: RSDN Magazine #2-2010
Опубликовано: 13.03.2011
Версия текста: 1.1
Введение
Теория
Представление в базе данных
Представление в модели предметной области
Добавление поддержки в BLToolkit
Практическое применение
Заключение
Список литературы

Введение

Создание современных бизнес-приложений редко обходится без помощи систем объектно-реляционного отображения (ORM). Хотя в целом такие системы существенно упрощают работу прикладного программиста, но некоторые задачи они все же не автоматизируют. Так, многие ORM перекладывают на программиста задачу реализации методов сравнения объектов (проверки их эквивалентности). Учитывая, что количество классов-сущностей, получаемых в процессе отображения, может быть довольно большим, это порождает большой объем монотонной и однообразной работы, что чревато появлением ошибок.

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

В данной работе предлагается механизм автоматической генерации методов проверки эквивалентности объектов (Equals() и GetHashCode()) на основе естественных ключей, имеющихся в БД, и приводится пример реализации этого механизма для ORM-системы BLToolkit.

Теория

Первичный ключ таблицы БД может быть естественным или суррогатным.

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

Суррогатные ключи  — это искусственно созданные технические ключевые поля, не несущие информации об объектах.

Естественные ключи можно использовать для идентификации объектов-сущностей, даже если они не были сохранены в БД, так как, фактически, они являются частью данных самого объекта. На практике, однако, использование естественных ключей наталкивается на определённые сложности:

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

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

На основании дополнительных ключей можно автоматически генерировать код проверки объектов на эквивалентность.

Представление в базе данных

Рассмотрим пример таблицы в базе данных с обоими видами ключей:

ПРИМЕЧАНИЕ

В качестве базы данных далее рассматривается MS SQL Server 2008.

В качестве предметной области выберем Web-сайты, обрабатывающие ссылки на социальные истории. Примером таких Web-сайтов являются сайты DIGG, KIGG.

Ниже приведен SQL-скрипт, описывающий таблицу, хранящую информацию о социальных историях.

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Stories]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Stories](
  [Id] [int] IDENTITY(1,1) NOT NULL,
  [Title] [varchar](50) NOT NULL,
  [Url] [varchar](50) NOT NULL,
  [Content] [varchar](400) NOT NULL,
 CONSTRAINT [PK_Stories_1] PRIMARY KEY CLUSTERED 
(
  [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
END

В данном примере у нас суррогатный ключ Id является первичным ключом, а поля Title и Url входят в дополнительный ключ. Уникальность обеспечивается следующим ограничением (constraint):

IF NOT EXISTS (
  SELECT * 
  FROM sys.indexes 
  WHERE object_id = OBJECT_ID(N'[dbo].[Stories]')
    AND name = N'IX_NK_TitleUrl'
)
  CREATE UNIQUE NONCLUSTERED INDEX [IX_NK_TitleUrl] ON [dbo].[Stories] 
  (
    [Title] ASC,
    [Url] ASC
  )  WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
           SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, 
           DROP_EXISTING = OFF, ONLINE = OFF, 
           ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON)
    ON [PRIMARY] 

Представление в модели предметной области

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

public class Story
{
  public int    Id      { get; set; }
  public string Title   { get; set; }
  public string Url     { get; set; }
  public string Content { get; set; }
}

Приведённый класс не содержит информации об дополнительных ключах, но при реализации методов Equals() и GetHashCode() для операций поиска и сравнения объектов программист повторяет логику работы дополнительного ключа:

public class Story
{
  // Свойства пропущены…

  // Перегрузка object.Equals
  public override bool Equals(object obj)
  {
    if (obj == null || GetType() != obj.GetType())
      return false;

    var anotherStory = (Story)obj;

    return Title == anotherStory.Title && Url == anotherStory.Url;
  }

  // Перегрузка object.GetHashCode
  public override int GetHashCode()
  {
    var hashCode = 1;

    if (Title != null)
      hashCode = (hashCode * 397) ^ Title.GetHashCode();

    if (Url != null)
      hashCode = (hashCode * 397) ^ Url.GetHashCode();

    return hashCode;
  }
}

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

Подобный код может быть автоматически сгенерирован на основании анализа дополнительных ключей в БД. В следующем разделе приведен пример практической реализации данной идеи на основе ORM BLToolkit

Добавление поддержки в BLToolkit

Так как изначально BLToolkit не имеет встроенной поддержки отображения дополнительных ключей, попробуем “научить” его этому. Это возможно, благодаря множеству механизмов генерации отображения. Мы воспользуемся способом на основе шаблонов Т4.

ПРЕДУПРЕЖДЕНИЕ

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

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

SELECT OBJECT_NAME(IX.OBJECT_ID) AS TABLE_NAME, 
       IX.NAME AS NATURAL_KEY_NAME, COL.NAME AS COLUMN_NAME
  FROM SYS.INDEXES IX
  INNER JOIN SYS.INDEX_COLUMNS IXC 
          ON IXC.OBJECT_ID = IX.OBJECT_ID AND IX.INDEX_ID = IXC.INDEX_ID
  INNER JOIN SYS.COLUMNS COL 
          ON COL.OBJECT_ID = IXC.OBJECT_ID 
         AND COL.COLUMN_ID = IXC.COLUMN_ID
  WHERE IX.NAME LIKE 'IX_NK_%'

Мы используем информацию из системных таблиц и соглашение о том, что накладываемые ограничения начинаются с префикса IX_NK_ для поиска дополнительных ключей.

ПРЕДУПРЕЖДЕНИЕ

Обратите внимание – префикс IX_NK не является стандартным и может быть изменен в зависимости от предпочтений разработчика в именовании ограничений и индексов, создаваемых для дополнительных ключей.

После выполнения этого скрипта мы будем иметь результат, подобный следующему:

TABLE_NAME NATURAL_KEY_NAME COLUMN_NAME
Stories    IX_NK_TitleUrl   Title
Stories    IX_NK_TitleUrl   Url

Следующий шаг – применить эти сведения при генерации объектов предметной области. Так как дополнительные ключи используются в методах Equals() и GetHashCode(), то логично было бы сгенерировать именно их на основе информации, полученной из базы данных. Мы реализуем базовый класс, похожий на базовый класс из такого проекта, как S#arpArch:

public abstract class Entity
{
  #region [Primary Key]

  /// <summary>
  /// Проверяем персистентность объекта.
  /// </summary>
  /// <returns>
  /// True, если объект персистентен.
  /// </returns>
  public abstract bool IsTransient();

  /// <summary>
  /// Сравнение текущего уникального идентификатора с 
  /// другим идентификатором.
  /// </summary>
  /// <param name="e">Другой объект.</param>
  /// <returns>
  /// True, если текущий уникальный идентификатор
  /// равен уникальному идентификатору другого объекта.
  /// </returns>
  protected abstract bool HasSamePrimaryKeyAs(Entity e);

  #endregion

  #region [Natural key]

  /// <summary>
  /// Сравнение текущего дополнительного ключа с 
  /// другим дополнительным ключом.
  /// </summary>
  /// <param name="e">Другой объект.</param>
  /// <returns>
  /// True, если текущий дополнительный ключ
  /// равен дополнительному ключу другого объекта.
  /// </returns>
  protected abstract bool HasSameNaturalKeyAs(Entity e);

  /// <summary>
  /// Подсчёт хеша от дополнительного ключа.
  /// </summary>
  /// <returns>Хеш от дополнительного ключа.</returns>
  protected abstract int GetHashCodeForNaturalKey();

  #endregion
  
  /// <summary>
  /// Сравнить текущий объект с другим объектом.
  /// </summary>
  /// <param name="obj">Другой объект.</param>
  /// <returns>
  /// True, если текущий объект равен другому объекты.
  /// </returns>
  public override bool Equals(object obj)
  {
    var anotherObj = (Entity)obj;

    if (ReferenceEquals(this, anotherObj))
      return true;

    // Проверяем, что другой объект не null
    // и что они одного типа с текущим объектом.
    if (anotherObj == null || !GetType().Equals(anotherObj.GetType()))
      return false;

    // Проверяем идентичность на уровне базы данных.
    if (!IsTransient()
        && !anotherObj.IsTransient() 
        && HasSamePrimaryKeyAs(anotherObj))
    {
      return true;
    }

    // Один из наших объектов не персистентен, поэтому будет 
    // выполнено сравнение на основе дополнительного ключа.
    return HasSameNaturalKeyAs(anotherObj);
  }

  /// <summary>
  /// Получить хеш объекта.
  /// </summary>
  /// <returns>Хеш объекта.</returns>
  public override int GetHashCode()
  {
    // Просто возращаем хеш от натурального ключа
    // в качестве хеша объекта.
    return GetHashCodeForNaturalKey();
  }
}

Из приведённого примера видно, что в реализации метода Equals() мы используем значение дополнительного ключа только в последнюю очередь.

Класс Entity является абстрактным, и во всех наследниках необходимо переопределить 6 методов (3 для главного ключа и 3 для дополнительного ключа). Пример класса Story, полученного после изменения и применения шаблонов для генерации модели предметной области:

[TableName(Name="Stories")]
public partial class Story : Entity
{
  [Identity, PrimaryKey(1)]
  public int    Id      { get; set; }
  public string Title   { get; set; }
  public string Url     { get; set; }
  public string Content { get; set; }

  #region Entity Members

  public override bool IsTransient()
  {
    if (Id != default(int))
      return false;

    return true;
  }

  protected override bool HasSamePrimaryKeyAs(Entity e)
  {
    var anotherEntity = (Story)e;

    if (Id != anotherEntity.Id)
      return false;

    return true;
  }

  protected override bool HasSameNaturalKeyAs(Entity e)
  {
    var anotherEntity = (Story)e;

    if (Title != anotherEntity.Title)
      return false;
    if (Url != anotherEntity.Url)
      return false;

    return true;
  }

  protected override int GetHashCodeForNaturalKey()
  {
    unchecked
    {
      var hashCode = 1;

      if (Title != default(string))
        hashCode = (hashCode * 397) ^ Title.GetHashCode();
      if (Url != default(string))
        hashCode = (hashCode * 397) ^ Url.GetHashCode();

      return hashCode;
    }
  }

  #endregion
}

В случае отсутствия дополнительных ключей шаблон сгенерирует для методов HasSameNaturalKeyAs() и GetHashCodeForNaturalKey() вызовы базовых реализаций методов Equals() и GetHashCode().

За дополнительными деталями вы можете обратиться к исходному коду, прикреплённому к статье.

Практическое применение

Мы могли бы закончить на этом, но у некоторых читателей, вероятно, возникнет вопрос: зачем такое усложнение для системы объектно-реляционного отображения, которая не управляет состоянием объектов? И они отчасти правы. Число сценариев, где можно применить изложенные концепции для BLToolkit, крайне мало по сравнению с такими системами объектно-реляционного отображения как NHibernate и Entity Framework. Однако мне хотелось бы привести пример одного из таких сценариев.

Предположим, что нам необходимо реализовать просмотр и добавление новых историй с помощью веб-сайта на платформе ASP.NET MVC. Пример:

[HandleError]
public class HomeController : Controller
{
  private readonly StoryRepository repository = new StoryRepository();
  private const int PageSize = 10;

  // Просмотр последних 10 историй.
  public ActionResult Index(int? page)
  {
    var stories = repository.GetStories(page ?? 0, PageSize);

    return View(stories);
  }

  // Предложение добавить новую историю.
  public ActionResult Add()
  {
    return View(new Story());
  }

  // Добавление новой истории.
  [HttpPost]
  public ActionResult Add(string title, string url, string content)
  {
    var story = new Story
    {
      Title   = title,
      Url     = url,
      Content = content
    };

    Validate(story);

    if (ModelState.IsValid)
    {
      try
      {
        repository.SaveStory(story);
        return RedirectToAction("Index");
      }
      catch (Exception ex)
      {
        ModelState.AddModelError("Saving error", ex);
      }
    }

    return View(story);
  }

  // Валидацию сделаем для простоты вручную
  private void Validate(Story story)
  {
    if (string.IsNullOrWhiteSpace(story.Title))
      ModelState.AddModelError("Validation error", "The title is empty");

    if (string.IsNullOrWhiteSpace(story.Url))
      ModelState.AddModelError("Validation error", "The url is empty");

    if (string.IsNullOrWhiteSpace(story.Content))
      ModelState.AddModelError("Validation error", "The content is empty");
  }
}

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

ПРИМЕЧАНИЕ

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

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

ПРИМЕЧАНИЕ

Из практики – пользователь с большей вероятностью не заметит результаты, если они расположены внизу страницы и/или ему необходимо пролистать страницу, чтобы увидеть их.

По этим причинам мы перед добавлением проанализируем результаты в кэше.

[HandleError]
public class HomeController : Controller
{
  // Показаны только изменения в объекте контроллера!

  private const string CacheId = "LastStories";

  public ActionResult Index(int? page)
  {
    var stories = (IEnumerable<Story>)HttpRuntime.Cache.Get(CacheId);

    if (stories == null)
    {
      stories = repository.GetStories(page ?? 0, PageSize);
      
      // В реальности рекомендуется устанавливать зависимость
      // объектов от таблицы в базе данных.
      HttpRuntime.Cache.Insert(CacheId, stories);
    }

    return View(stories);
  }

  [HttpPost]
  public ActionResult Add(string title, string url, string content)
  {
    var story = new Story
    {
      Title   = title,
      Url     = url,
      Content = content
    };

    Validate(story);

    if (ModelState.IsValid)
    {
      var stories = (IEnumerable<Story>)HttpRuntime.Cache.Get(CacheId);

      if (stories != null)
      {
        // Используем поддержку дополнительных ключей, т.к.
        // объект сравнения неперсистентен.
        if (!stories.Contains(story))
        {
          // Сбрасываем вручную кеш.
          HttpRuntime.Cache.Remove(CacheId);
        }
        else
        {
          ModelState.AddModelError(
            "Saving error",
            "Sorry, your story already exist on our site.");
        }
      }

      try
      {
        if (ModelState.IsValid)
        {
          repository.SaveStory(story);
          return RedirectToAction("Index");
        }
      }
      catch (Exception ex)
      {
        ModelState.AddModelError("Saving error", ex);
      }
    }

    return View(story);
  }
}

Заключение

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

Список литературы

  1. http://osmirnov.net/?p=15#more-15 ;
  2. http://osmirnov.net/?p=51#more-51 ;
  3. http://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D0%B2%D0%B8%D1%87%D0%BD%D1%8B%D0%B9_%D0%BA%D0%BB%D1%8E%D1%87 .


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