Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Entity Framework Code First — индексация полей и полнотекстовый поиск

Anna | 18.06.2014 | нет комментариев

По роду моей деятельности, мне Зачастую доводится делать разные небольшие планы, в основном, это сайты написанные на 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}]) WITH STOPLIST = SYSTEM;", 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 и некоторые комментарии, полная версия на GitHab’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, впрочем, простого решения на сегодняшний день не было. Данная реализация с лихвой перекрывает мои требования к индексации, если Вам чего то не хватает (обработки ошибок, настроек — скажем, список стоп-слов и т.д.), Вы можете самосильно забрать план с GitHab’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();
}
}

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB