
По роду моей деятельности, мне часто приходится делать различные небольшие проекты, в основном, это сайты написанные на ASP.NET MVC. В любом современном проекте присутствуют данные, а значит и база данных, а значит с ней нужно как то работать.
Если отбросить все дискуссии про «за и против», то спешу сообщить, что мой выбор пал на Entity Framework Code First. Во время разработки проекта, я уделяю внимание исключительно бизнес-логике и не трачу время на проектирование базы данных и прочие шаблонные действия. Неприятным сюрпризом при использовании такого подхода для меня стало отсутствие возможности «из коробки» у Entity Framework возможности строить индекс по полям, а так же пользоваться удобным и современным механизмом полнотекстового поиска.
После многочасового гугления, опробовав десятки различных методов со StackOverflow и прочих подобных сайтов, я пришел к выводу, что очевидного и простого решения проблемы нет, поэтому решил сделать собственное, об этом и пойдет речь далее.
Реализация
Основным требованием к решению проблемы, является простота интеграции в любой новый (существующий) проект. В Code First принято все настраивать атрибутами, поэтому хорошо было бы сделать так:
public class SomeClass { public int Id { get; set; } [Index] public string Name { get; set; } [FullTextIndex] public string Description { get; set; } }
при этом, не хотелось бы переопределять DatabaseInitializer и делать прочие нетривиальные действия.
В своей работе я использую Visual Studio 2013 Ultimate. Создадим новый проект типа Class Library, сразу добавим в него Entity Framework 6 Beta 1 с помощью NuGet консоли (Package Manager Console):
PM> Install-Package EntityFramework -Pre
Создадим атрибуты Index и FullTextSearch, а так же перечисление для FullTextSearch:
public class IndexAttribute : Attribute { } public class FullTextIndexAttribute : Attribute { } public class FullTextIndex { public enum SearchAlgorithm { Contains, FreeText } }
Если Вы ранее работали с полнотекстовым поиском, то Вы наверняка поняли зачем нужен Contains и FreeText, если нет, то Вам сюда.
Далее, создадим абстрактный класс, унаследованный от DbContext:
public abstract class DbContextIndexed : DbContext { private static bool Complete; private int? language; public int Language { get { return language.HasValue ? language.Value : 1049; //1049 - русский язык } set { language = value; } } protected override void Dispose(bool disposing) { if (!Complete) { Complete = true; CalculateIndexes(); } base.Dispose(disposing); } private void CalculateIndexes() { if (GetCompleteFlag()) return; //Получаем все сущности текущего DbContext foreach (var property in this.GetType() .GetProperties() .Where(f => f.PropertyType.BaseType != null && f.PropertyType.BaseType.Name == "DbQuery`1")) { var currentEntityType = property.PropertyType.GetGenericArguments().FirstOrDefault(); if (currentEntityType == null || currentEntityType.BaseType.FullName != "System.Object") continue; //Получаем название таблицы в БД var tableAttribute = currentEntityType .GetCustomAttributes(typeof(TableAttribute), false).FirstOrDefault() as TableAttribute; var tableName = tableAttribute != null ? tableAttribute.Name : property.Name; //Получаем у сущности свойства помеченые аттрибутом Index, создаем по ним индекс BuildingIndexes(tableName, currentEntityType.GetProperties() .Where(f => f.GetCustomAttributes(typeof(IndexAttribute), false) .Any())); //Получаем у сущности свойства помеченые атрибутом FullTextIndex, создаем по ним индекс BuildingFullTextIndexes(tableName, currentEntityType.GetProperties() .Where(f => f.GetCustomAttributes(typeof(FullTextIndexAttribute), false) .Any())); } CreateCompleteFlag(); } private void BuildingIndexes(string tableName, IEnumerable<PropertyInfo> propertyes) { foreach (var property in propertyes) Database.ExecuteSqlCommand(String.Format("CREATE INDEX IX_{0} ON {1} ({0})", property.Name, tableName)); } private void BuildingFullTextIndexes(string tableName, IEnumerable<PropertyInfo> propertyes) { var fullTextColumns = string.Empty; foreach (var property in propertyes) fullTextColumns += String.Format("{0}{1} language {2}", (string.IsNullOrWhiteSpace(fullTextColumns) ? null : ","), property.Name, Language); //Создаем полнотекстовый индекс Database.ExecuteSqlCommand(System.Data.Entity.TransactionalBehavior.DoNotEnsureTransaction, String.Format("IF NOT EXISTS (SELECT * FROM sysindexes WHERE id=object_id('{1}') and name='IX_{2}') CREATE UNIQUE INDEX IX_{2} ON {1} ({2}); CREATE FULLTEXT CATALOG FTXC_{1} AS DEFAULT; CREATE FULLTEXT INDEX ON {1}({0}) KEY INDEX [IX_{2}] ON [FTXC_{1}]", fullTextColumns, tableName, "Id")); } private void CreateCompleteFlag() { Database.ExecuteSqlCommand(System.Data.Entity.TransactionalBehavior.DoNotEnsureTransaction, "CREATE TABLE [dbo].[__IndexBuildingHistory]( [DataContext] [nvarchar](255) NOT NULL, [Complete] [bit] NOT NULL, CONSTRAINT [PK___IndexBuildingHistory] PRIMARY KEY CLUSTERED ([DataContext] ASC))"); } private bool GetCompleteFlag() { var queryResult = Database.SqlQuery(typeof(string), "IF OBJECT_ID('__IndexBuildingHistory', 'U') IS NOT NULL SELECT 'True' AS 'Result' ELSE SELECT 'False' AS 'Result'") .GetEnumerator(); queryResult.MoveNext(); return bool.Parse(queryResult.Current as string); } }
чтобы не раздувать пост, здесь намеренно убраны summary и некоторые комментарии, полная версия на GitHub'e. Если кратко пояснить, то EF создает модель при первичном обращении к DbContext'у, соответственно строить индексы на конструкторе мы не можем, остается самый просто�� вариант построить их после создания модели, при попытке уничтожить экземпляр DbContext. Далее, чтобы не нагружать БД каждый раз несколькими запросами и попыткой создания, в лучших традициях EF создадим в базе служебную таблицу __IndexBuildingHistory, наличие которой, будет свидетельствовать о наличии индексов. Остальное очевидно.
В целом, если уже сейчас создать модель, пометить ее атрибутами и запустить проект, то индексы будут успешно созданы, однако, нам еще нужно удобное использование полнотекстового индекса, для это создадим класс расширение (extension class):
public static class IQueryableExtension { public static IQueryable<T> FullTextSearch<T>(this DbSet<T> queryable, Expression<Func<T, bool>> func, FullTextIndex.SearchAlgorithm algorithm = FullTextIndex.SearchAlgorithm.FreeText) where T : class { var internalSet = queryable.AsQueryable() .GetType() .GetProperty("System.Data.Entity.Internal.Linq.IInternalSetAdapter.InternalSet", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(queryable.AsQueryable()); var entitySet = (EntitySet)internalSet.GetType() .GetProperty("EntitySet") .GetValue(internalSet); var searchType = algorithm == FullTextIndex.SearchAlgorithm.Contains ? "CONTAINS" : "FREETEXT"; var columnName = ((MemberExpression)((BinaryExpression)func.Body).Left).Member.Name; var searchPattern = ((ConstantExpression)((BinaryExpression)func.Body).Right).Value; return queryable.SqlQuery( String.Format("SELECT * FROM {0} WHERE {1};", entitySet.Name, String.Format("{0}({1},'{2}')", searchType, columnName, searchPattern))) .AsQueryable(); } }
Вот и все, казалось бы, такая популярная проблема как индексы и полнотекстовый поиск требует особого внимания со стороны создателей Entity Framework, однако, простого решения на сегодняшний день не было. Данная реализация с лихвой перекрывает мои требования к индексации, если Вам чего то не хватает (обработки ошибок, настроек — например, список стоп-слов и т.д.), Вы можете самостоятельно забрать проект с GitHub'a и доработать, либо написать мне. Статья была бы совсем скучной, если бы мы не попробовали как все это работает, поэтому переходим к использованию.
Использование
1. Создадим проект Console application
2. Добавим Entity Framework 6 beta через NuGet
3. Добавим ссылку на библиотеку (если Вы не читали про реализацию, то Вы можете скачать готовую библиотеку, ссылки в конце статьи)
4. Создадим простую сущность, без вложеностей и связей, для примера этого достаточно:
public class Animal { public int Id { get; set; } [Index] [StringLength(200)] public string Name { get; set; } [FullTextIndex] public string Description { get; set; } public int Family { get; set; } [FullTextIndex] public string AdditionalDescription { get; set; } }
Сущность животное, с названием (Name), по которому мы построим обычный индекс, описанием (Description) — построим полнотекстовый индекс и прочими полями для вида, мы не будем их использовать. Обратите внимание на строку [StringLength(200)], при создании индекса по строковым полям она обязательна, т.к. MSSQL позволяет строить индексы по полям, размер которых не превышает 900 байт — сколько это в символах, зависит от выбранной Вами кодировки базы данных.
5. Создадим контекст базы данных:
public class DataContext : DbContextIndexed { public DbSet<Animal> Animals { get; set; } }
единственная разница здесь в наследовании, обычно Вы наследуетесь от DbContext, а теперь от нашей DbContextIndexed
6. В Programm.cs добавим обращение к контексту, чтобы спровоцировать создание базы данных:
static void Main(string[] args) { using (var context = new DataContext()) { var temp = context.Animals.ToList(); } }
7. В config файле проекта пропишите строку подключения к базе данных с названием DataContext:
<configuration> <connectionStrings> <add name="DataContext" connectionString="Data Source=(local)\SQL; Initial Catalog=EFCF; Integrated Security=true;" providerName="System.Data.SqlClient"/> </connectionStrings> </configuration>
8. Нажимаем F5, чтобы создать базу данных, когда программа завершится, с помощью Managment Studio можно убедится, что все работает, как мы запланировали:

9. Теперь, давайте попробуем добавить данные, чтобы опробовать поиск:
using (var context = new DataContext()) { context.Animals.Add(new Animal { Name = "Кот", Description = "Относится к семейству кошачьих, очень любят вискас." }); context.Animals.Add(new Animal { Name = "Лев", Description = "Лев по праву считается королем зверей, самый известный среди котов." }); context.Animals.Add(new Animal { Name = "Пантера", Description = "Пантера - это маленькая черная кошка." }); context.Animals.Add(new Animal { Name = "Тигр", Description = "Большой полосатый кот." }); context.Animals.Add(new Animal { Name = "Питбуль", Description = "Хорошая бойцовая собака." }); context.Animals.Add(new Animal { Name = "Американский стафардширский терьер", Description = "Произошел от питбуля, является примером отличной бойцовой собаки." }); context.SaveChanges(); }
запустим, чтобы данные записались в БД, теперь попробуем поискать:
using (var context = new DataContext()) { foreach (var pet in context.Animals.FullTextSearch(f => f.Description == "коты")) Console.WriteLine("{0} - {1}", pet.Name, pet.Description); }
результат следующий:

У меня установлена версия MSSQL 2008R2, поэтому результат хороший, но не идеальный. Насколько я знаю в 2013-ой версии мы бы еще получили значение пантера, т.к. «кошка», тоже бы учлось.
Я считаю, что довольно простым, и самое главное, «стандартным» способом можно пользоваться полнотекстовым поиском и строить индексы по полям. Данной реализации достаточно для 95% маленьких проектов, но я искренне надеюсь, что создатели Entity Framework все таки реализуют данный функционал «в коробке».
Источники
Скачать готовую библиотеку:
в формате zip
в формате dll
Проект выложен на GitHub
Entity Framework 6 beta на сайте Nuget
