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

Практическое использование дополнительных ключей на примере 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'))
BEGINCREATETABLE [dbo].[Stories](
  [Id] [int] IDENTITY(1,1) NOTNULL,
  [Title] [varchar](50) NOTNULL,
  [Url] [varchar](50) NOTNULL,
  [Content] [varchar](400) NOTNULL,
 CONSTRAINT [PK_Stories_1] PRIMARYKEYCLUSTERED 
(
  [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'
)
  CREATEUNIQUENONCLUSTEREDINDEX [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
{
  publicint    Id      { get; set; }
  publicstring Title   { get; set; }
  publicstring Url     { get; set; }
  publicstring Content { get; set; }
}

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

      public
      class Story
{
  // Свойства пропущены…// Перегрузка object.Equalspublicoverridebool Equals(object obj)
  {
    if (obj == null || GetType() != obj.GetType())
      returnfalse;

    var anotherStory = (Story)obj;

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

  // Перегрузка object.GetHashCodepublicoverrideint 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
  INNERJOIN SYS.INDEX_COLUMNS IXC 
          ON IXC.OBJECT_ID = IX.OBJECT_ID AND IX.INDEX_ID = IXC.INDEX_ID
  INNERJOIN 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>publicabstractbool IsTransient();

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

  #endregion#region [Natural key]

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

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

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

    if (ReferenceEquals(this, anotherObj))
      returntrue;

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

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

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

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

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

[TableName(Name="Stories")]
publicpartialclass Story : Entity
{
  [Identity, PrimaryKey(1)]
  publicint    Id      { get; set; }
  publicstring Title   { get; set; }
  publicstring Url     { get; set; }
  publicstring Content { get; set; }

  #region Entity Members

  publicoverridebool IsTransient()
  {
    if (Id != default(int))
returnfalse;

    returntrue;
  }

  protectedoverridebool HasSamePrimaryKeyAs(Entity e)
  {
    var anotherEntity = (Story)e;

    if (Id != anotherEntity.Id)
returnfalse;

    returntrue;
  }

  protectedoverridebool HasSameNaturalKeyAs(Entity e)
  {
    var anotherEntity = (Story)e;

    if (Title != anotherEntity.Title)
returnfalse;
    if (Url != anotherEntity.Url)
returnfalse;

    returntrue;
  }

  protectedoverrideint 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]
publicclass HomeController : Controller
{
  privatereadonly StoryRepository repository = new StoryRepository();
  privateconstint 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);
  }

  // Валидацию сделаем для простоты вручнуюprivatevoid 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]
publicclass HomeController : Controller
{
  // Показаны только изменения в объекте контроллера!privateconststring 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. Информацию о журнале можно найти здесь