
По роду моей деятельности, мне часто приходится делать различные небольшие проекты, в основном, это сайты написанные на 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
