Алгоритм Hi/Lo полезен, когда вам нужны уникальные ключи. Если коротко, то алгоритм Hi/Lo описывает механизм генерации безопасных идентификаторов на стороне клиента, а не в базе данных (безопасных в этом контексте означает отсутствие коллизий). Он задает уникальные идентификаторы строкам таблицы, не в зависимости от того, будет ли сразу храниться строка в базе данных или нет. Это позволяет сразу же использовать идентификаторы, как обычные последовательные идентификаторы базы данных.

Последовательности (sequences) баз данных кэшируются, масштабируются и решают проблемы параллелизма. Но для каждого нового значения последовательности нам постоянно нужно обращаться в базу данных. И когда у нас есть большое количества инсертов то это становится немного накладно. Поэтому, для оптимизации работы с последовательностями используют алгоритм Hi/Lo. EntityFramework Core поддерживает Hi/Lo «из коробки» с помощью метода ForSqlServerUseSequenceHiLo

Как работает Hi/Lo


Начнем с того что такое Hi/Lo. Основная идея состоит в том, что у вас есть два числа, чтобы составить первичный ключ — «высокий»(high) номер и «низкий»(low) номер. Клиент может в основном увеличивать «высокую» последовательность, зная, что он может безопасно генерировать ключи из всего диапазона предыдущего «высокого» значения с множеством «низких» значений.

Например, предположим, что у вас есть «высокая» последовательность с текущим значением 35, а «низкий» номер находится в диапазоне 0-1023. Затем клиент может увеличить последовательность до 36 (для других клиентов, чтобы иметь возможность генерировать ключи при использовании 35) и знать, что ключи 35/0, 35/1, 35/2, 35/3… 35/1023 являются все доступные.

Может быть очень полезно (особенно с ORM), чтобы иметь возможность устанавливать первичные ключи на стороне клиента вместо того, чтобы вставлять значения без первичных ключей, а затем извлекать их обратно на клиент. Помимо всего прочего, это означает, что вы можете легко создавать отношения между родителями и дочерними элементами и иметь все ключи на месте, прежде чем делать какие-либо вставки, что упрощает их батчинг(batching).

Используем Hi/Lo для генерации ключей в Entity Framework Core


Давайте посмотрим, как использовать Hi/Lo для генерации ключей с помощью Entity Framework Core. Допустим у нас есть простая модель Категорий:

public class Category
{
   public int Id { get; set; }
   public string Name { get; set; }
}

Помним что EF конфигурирует свойство Id или <имя типа>Id в качестве ключа. Теперь нам нужно создать DBContext:

public class SampleDBContext : DbContext
{
   public SampleDBContext()
   {
      Database.EnsureDeleted();
      Database.EnsureCreated();
   }
  
   protected override void OnConfiguring(DbContextOptionsBuilder optionBuilder)
   {
      string сonnString = @"Server=localhost\SQLEXPRESS01;Database=EFSampleDB;Trusted_Connection=true;";
      optionBuilder.UseSqlServer(сonnString);
   }
  
   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
      var entityTypeBuilder = modelBuilder.Entity<Category>();
      entityTypeBuilder.ToTable("Category");
      entityTypeBuilder.HasKey(t => t.Id);
      entityTypeBuilder.Property(t => t.Id).ForSqlServerUseSequenceHiLo();
      entityTypeBuilder.Property(t => t.Name).HasMaxLength(128).IsRequired();
   }
  
   public DbSet<Category> Category { get; set; }
}

DbContext состоит из:

  • конструктора SampleDBContext который представляет собой реализацию инициализатора базы данных DropCreateDatabaseAlways;
  • метода OnConfiguring для настройки DBContext;
  • метода OnModelCreating — это место, где можно определить конфигурацию модели. Чтобы определить последовательность Hi/Lo, используйте метод расширения ForSqlServerUseSequenceHiLo.

Запускаем приложение. В результате должна создаться база «EFSampleDB» с таблицей «Category» и с последовательностью «EntityFrameworkHiLoSequence». Имя последовательности и схему в которой она будет создана можно передать как параметр:

ForSqlServerUseSequenceHiLo(string name, string schema).



Скрипт создания «EntityFrameworkHiLoSequence» виглядит следюющим образом

CREATE SEQUENCE [dbo].[EntityFrameworkHiLoSequence] 
 AS [bigint]
 START WITH 1
 INCREMENT BY 10
 MINVALUE -9223372036854775808
 MAXVALUE 9223372036854775807
 CACHE 
GO

Данная последовательность начинается с 1 и увеличивается на 10. Существует разница между Sequence и Hi/Lo Sequence относительно опции INCREMENT BY. В Sequence INCREMENT BY добавляет значение к предыдущему значению последовательности, чтобы сгенерировать новое значение. Таким образом, в этом случае, если ваше предыдущее значение последовательности равно 11, тогда следующее значение последовательности будет равно 11 + 10 = 21. В случае Hi/Lo Sequence параметр INCREMENT BY обозначает значение блока, это означает, что следующее значение последовательности будет выбрано после использования первих 10.

Давайте добавим некоторые данные в базу:

using (var dataContext = new SampleDBContext())
{
   dataContext.Category.Add(new Category { Name = "Name-1" });
   dataContext.Category.Add(new Category { Name = "Name-2" });
   dataContext.Category.Add(new Category { Name = "Name-3" });
   dataContext.SaveChanges();
}

Как только этот код попадает в строку, где добавляется категория в DBContext, выполняется вызов базы данных, чтобы получить значение последовательности. Это можно проверить его с помощью SQL Server Profiler.



Когда вызывается dataContext.SaveChanges(); все 3 категории будут сохранены с значениями первичного ключа которые уже сгенерированные и выбираются только один раз.



Значение последовательности не будет выбрано из базы данных пока не исчерпана часть Lo (10 записей в нашем случае), тольки при добавлении 11-ой записи будет выполняться вызов базы данных, чтобы получить следующее значение последовательности (Hi).

Можно создать одну последовательность нескольких таблиц:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.ForSqlServerUseSequenceHiLo("DBSequenceHiLo");
}

Настройка последовательности Hi/Lo


В метода ForSqlServerHasSequence нет никаких параметров для изменения начального значения и значения инкременты. Но можно указать эти значения следующим образом, сначала определяем последовательность с параметрами StartAt и IncrementBy а дальше используем тот же метод расширения ForSqlServerUseSequenceHiLo.

modelBuilder.HasSequence<int>("DBSequenceHiLo").StartsAt(1000).IncrementsBy(5);
modelBuilder.ForSqlServerUseSequenceHiLo("DBSequenceHiLo");

В этом случае sql-скрипт для DBSequenceHiLo будет иметь следующий вид

CREATE SEQUENCE [dbo].[DBSequenceHiLo] 
 AS [int]
 START WITH 1000
 INCREMENT BY 5
 MINVALUE -2147483648
 MAXVALUE 2147483647
 CACHE 
GO

Теперь, когда мы добавляем три категорий, значение ключа начинается с 1000. А так как параметр IncrementBy установлен на «5», то, когда будет добавлена шестая запись в контекст, будет сделан запрос в базу данных, чтобы получить следующее значение последовательности. Если поменять код, и добавить еще 3 категории то на скрине Вы можете увидеть второй вызов базы данных.



Спасибо за внимание!

Комментарии (3)


  1. JohnLemon
    19.02.2018 18:28

    А чем подход использование Hi-Lo лучше Guid? поискал сам но ничего кроме «красивее» айди не нашел


    1. Deosis
      20.02.2018 08:00

      Guid очень случаен и плохо подходит для вставки большого количества элементов.
      Поэтому и возникают схемы, в которых клиенту отдается диапазон идентификаторов.
      Либо часть ключа берется из метки времени, что позволяет хранить записи в порядке добавления.


  1. vladimirkolyada
    20.02.2018 08:34

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