Showing posts with label EF. Show all posts
Showing posts with label EF. Show all posts

Tuesday, May 5, 2015

Use Full-Text Search in SQL Server 2008 with Entity Framework

This is my first article about Full-Text search in SQL Server 2008. I had a task connected with discovering how can we implement 'google-like' search in our desktop solution. We use Entity Framework 6.0 in our project and the main aim I tried to reach was to find full-text search engine compatible with EF. I found this engine and wrote article about this engine: "Используем Lucene.Net для высокоточного полнотекстового поиска" in Russian. However, I also want to try using Full-Text Search engine in SQL Server 2008 with EF. In addition, this article is just about this. As a bonus in the end of the article, I will compare FTS in SQL Server with my choice Lucene.Net. I hope this article will be helpful to embed FTS in SQL Server in your applications. Let's see the full circle from creating catalog in database to implementing some logic for searching combined with EF.
1. The first step is to create a new catalog for saving index.
2. The second step is to create a new catalog.
3.  See catalog in DB:
4.  Create index for some table 'Define Full-Text Index…'
5.  Welcome page:
6. Select unique index.
7.  Select available column for index.
8.    Select change tracking.
9.    Select catalog, stop list of words and filegroup index.
        10.  Define population schedulers.
In this window you can create scheduler for updating index.
11. Final wizard page:
12. Create index operation performed.
13. Start Full Population for selected table for index created before.
14. Result of population:
15.  Using Full Text search from SQL Management Studio:
16. Using search in EF:
public IQueryable<Order> SearchData(string search)
{
    try
    {

        object[] parameters = { search };

            var query = "SELECT * FROM [OrderCustomFields] where FREETEXT(*, '{0}')";

            var result = (Db as System.Data.Entity.Infrastructure.IObjectContextAdapter)
                .ObjectContext.ExecuteStoreQuery<Order>(query, search);
Full documentation you can find in MSDN. A big problem with using this way is caused by a lot of words with direct selection data from database and manual query for search data.
The main problem I encountered with was the FTS in MS SQL Server that could not create searching index for multiple table. Only one table – one index. It’s not actual problem for Lucene.Net. Lucene can create complex search index with multiple tables for search. MS FTS is a part of SQL Server so you will have to write SQL queries like CONTAINS, FULLTEXT, etc. Lucene.Net provides a special API for solving the same issue. Both of them support work with stop-words, synonyms, stemming and multiple columns querying.  MS FTS support automation recreate and update index, but Lucene.Net does not support this functionality.

Conclusion
This article is not about how to choose Lucene.Net or MS FTS. It is supposed to show you how to implement FTS for vast amount of text data in MS SQL Server. If your search engine requirements are not so strong, maybe using SQL Server Full Text Search will be the best option – it is very simple to maintain and fast enough with simple queries. On the other hand, if you need more complicated search engine for searching in multiple documents or creation different queries with powerful API, I think the best choice for you is to search engine like Lucene.Net or Solr. Or if money is not a big problem for you and if you prefere using private search engine (not open-source), so the best choice could be Sphinx Full-Text Search Engine because its API is similar to EF syntax. 

Tuesday, April 7, 2015

Entity Framework DateTime and UTC/Local

Сегодня мы снова немного поиграем с Entity Framework. Поговорим о том, как сохраняются поля DateTime в базу данных.
Если вы работаете с EF через OData, например, или WCF сервис, то у вас может возникнуть потребность в сохранении даты, введенной пользователем в БД. Проблем не должно возникнуть, если вся ваша система крутится вокруг одной Time Zone; если же вокруг разных, и другие пользователи могут видеть дату и время ваших изменений, – вот здесь можно столкнуться с проблемой. Ваша задача – выдавать клиенту даты в его часовом поясе; при этом нужно реализовать хранение дат на стороне сервера так, чтобы избежать дополнительных конвертаций. 
Проблема заключается в том, что по умолчанию DateTime тип данных в EF установлен Kind как Unspecified. Есть несколько вариантов решения этой проблемы, начиная с решения "в лоб"  до решений с небольшим шаманством.
Используем DateTimeOffset
Первое и самое простое решение  это использовать для хранения данных в базе данных DateTimeOffset. Но в этом подходе есть большое "но": WCF Ria Services не поддерживает тип DateTimeOffset. В итоге вам придется либо не использовать DateTimeOffset, либо написать workaround и все-таки использовать DateTimeOffset на стороне сервера, но тогда вам нужно будет написать конвертацию в DateTime, как, например, это сделано в статье 'WCF Ria Services и DateTimeOffset', в которой ее автор, пытаясь решить одну проблему, все равно пришел к решению с конвертацией и простановкой Kind в структуре DateTime. Это самое простое решение, но если у вас уже готовая система, в которой заменить DateTime на DateTimeOffset не так уж просто (например, с такой проблемой мы столкнулись в своем проекте недавно), нужно искать другой подход, который позволит все-таки играть со свойством Kind и проставлять его в Local или UTC, в зависимости от ваших потребностей. Для наших решений было достаточно использования Local, чтобы корректно отобразить на клиенте дату в часовом поясе клиента. Поэтому следующие примеры построены на установке свойства Kind в Local. Ниже мы рассмотрим, как это все работает, и напишем тест, который покроет нам всю необходимую логику.
Forcing EF to mark DateTime fields at Local or UTC with T4 generator
Следующий подход основан на написании темплейта, который для ваших DateTime полей будет проставлять поле Kind в UTC или Local, в зависимости от вашего решения.
CreateDate = DateTime.SpecifyKind(CreateDate, DateTimeKind.Local);
О томкак это сделать, есть целая статья 'Forcing Entity Framework to mark DateTime fields at UTC'Это делается очень просто, но поскольку я не люблю использовать T4 генератор в своих приложениях, то рассмотрю другие подходы, которые позволят вам сделать то же самое, только без T4 генератора.
DateTimeKingAttribute
На этом подходе остановимся детальнее. Основная задача – добавить атрибут, с помощью которого можно будет проставить для полей DateTime свойство Kind. Ниже приведен пример такой реализации, взятый со stackoverflow.
[AttributeUsage(AttributeTargets.Property)]
public class DateTimeKindAttribute : Attribute
{
    private readonly DateTimeKind _kind;

    public DateTimeKindAttribute(DateTimeKind kind)
    {
        _kind = kind;
    }

    public DateTimeKind Kind
    {
        get { return _kind; }
    }

    public static void Apply(object entity)
    {
        if (entity == null)
            return;

        var properties = entity.GetType().GetProperties()
            .Where(x => x.PropertyType == typeof(DateTime) || x.PropertyType == typeof(DateTime?));

        foreach (var property in properties)
        {
            var attr = property.GetCustomAttribute<DateTimeKindAttribute>();
            if (attr == null)
                continue;

            var dt = property.PropertyType == typeof(DateTime?)
                ? (DateTime?)property.GetValue(entity)
                : (DateTime)property.GetValue(entity);

            if (dt == null)
                continue;

            property.SetValue(entity, DateTime.SpecifyKind(dt.Value, attr.Kind));
        }
    }
}
Как это использовать, можно посмотреть ниже.
public class Build
{
    public int Id { getset; }

    [DateTimeKind(DateTimeKind.Utc)]
    public DateTime CreateDate { getset; }

    [StringLength(255)]
    public string Name { getset; }
}
И теперь осталось самое малое: добавить трансформацию для нашего DbContext, иначе этот подход попросту не взлетит.
public class BuildContext : DbContext
{
    public BuildContext()
        : base() 
    {
        ((IObjectContextAdapter)this).ObjectContext.ObjectMaterialized +=
            (sender, e) => DateTimeKindAttribute.Apply(e.Entity);
    } 

    public virtual DbSet<Build> Builds { getset; } 

    protected override void OnModelCreating(DbModelBuilder modelBuilder) 
    { 
        Database.SetInitializer(new DropCreateDatabaseIfModelChanges<BuildContext>());
        base.OnModelCreating(modelBuilder);
    } 
}

Мы не использовали данный подход в своем проекте, хотя он довольно-таки интересный и, возможно, для вашего решения его будет достаточно.
Использование EmitMapper с разделением модели и DTO (Data Transfer Object)
Этот подход используется в том случае, когда вы используете на клиенте не напрямую объекты модели БД, а вместо них используете DTO классы, которые хранят информацию только о необходимых данных для клиентской части. Для данной реализации не обязателен какой-то мапер на подобии EmitMapperAutoMapper и другие. Приведу пример реализации подхода, который был актуальный для нужд компании, в которой я работаю.
Для этого нам понадобится вспомогательный класс, который будет проставлять для наших DateTime полей свойство Kind.
public class DateKindHelper
{
    public static DateTime DefaultToLocal(DateTime date)
    {
        return date.Kind != DateTimeKind.Local ? DateTime.SpecifyKind(date, DateTimeKind.Local) : date;
    }

    public static DateTime? DefaultToLocal(DateTime? date)
    {
        return date.HasValue && date.Value.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(date.Value, DateTimeKind.Local) : date;
    }

    public static DateTime DefaultToUtc(DateTime date)
    {
        return date.Kind != DateTimeKind.Utc ? DateTime.SpecifyKind(date, DateTimeKind.Utc) : date;
    }

    public static DateTime? DefaultToUtc(DateTime? date)
    {
        return date.HasValue && date.Value.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(date.Value, DateTimeKind.Utc) : date;
    }
}
Подобную реализацию я выдел когда-то на stackoverflow, так что если вам вдруг понадобится что-то подобное, вы сможете легко найти его.
Теперь перейдем к нашей модели Build.
public class Build
{
    public int Id { getset; }

    [DateTimeKind(DateTimeKind.Utc)]
    public DateTime CreateDate { getset; }

    [StringLength(255)]
    public string Name { getset; }
}
Также нам нужно реализовать Dto модель, которая будет использоваться на клиенте.
public class BuildDTO
{
    public int Id { getset; }

    public DateTime CreateDate { getset; }
        
    public string Name { getset; }
}
Для данного примера DTO объект практически одинаковый с основный классом Build, но на практике ваша модель может иметь очень много данных, а DTO класс будет хранить только самые необходимые. Теперь осталось написать сам конвертор, который будет конвертировать наши данные.
internal static class DateTimeConverter
{
    public static TDTO ToDTO<TDTO, TEntity>(TEntity entity)
    {
        return ObjectMapperManager.DefaultInstance
            .GetMapper<TEntity, TDTO>(new DefaultMapConfig()
            .ConvertUsing<DateTimeDateTime>(DateKindHelper.DefaultToLocal))
            .Map(entity);
    }
}
Как это использовать, показано в примере ниже.
var build = new Build
        {
            Id = 1,
            CreateDate = new DateTime(2014, 12, 16),
            Name = "Test"
        };


var buildDto = DateTimeConverter.ToDTO<BuildDTOBuild>(build);
Console.WriteLine(build.CreateDate.Kind);
Console.WriteLine(buildDto.CreateDate.Kind);
Результат можно посмотреть на рисунке ниже.
Ручная установка для полей свойства Kind
Это самый ленивый и "прямолобый" способ, который работает по принципу ручной установки свойства Kind для полей типа DateTime. Этот способ, конечно, больше всего подвержен возникновению непредусмотренных ситуаций, потому что вы можете забыть проставить свойство Kind для какого-то поля DateTime. Но это решается очень просто, если вы покрываете свой код с помощью юнит-тестов. Для начала приведу простой пример, как нужно проставлять свойство Kind напрямую в классе.
public class Build
{
    public int Id { getset; }
    private DateTime _createTime;

    public DateTime CreateDate
    {
        get { return _createTime; }
        set
        {
            _createTime = DateKindHelper.DefaultToLocal(value);
        }
    }

    [StringLength(255)]
    public string Name { getset; }
}
Выглядит это, возможно, не очень красиво, зато это решение самое простое. Правда, в нем больше всего есть вероятность допустить ошибки. Чтобы этого избежать, достаточно написать простой юнит-тест, который возьмет всю заботу о валидности данных на себя.
Например, тест, который проверяет возможность установки для класса в вашем DbContext свойства Kind в Local.
[TestMethodTestCategory("Unit")]
public void TestSettingLocalZonesOnServer()
{
    var sets =
        from p in typeof(BuildContext).GetProperties()
        where p.PropertyType.IsGenericType
        && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)
        select p;

    foreach (var set in sets)
    {
        Type type = set.PropertyType;
        var returnType = type.GetMethods(BindingFlags.Instance | BindingFlags.Public).First(x => x.Name == "Create").ReturnType;
        object o = Activator.CreateInstance(returnType);
        var obj = o.GetType();
        var dateTimeProperties = obj.GetProperties().Where(x => x.PropertyType == typeof(DateTime) || x.PropertyType == typeof(DateTime?)).ToList();
        if (dateTimeProperties.Count > 0)
        {
            foreach (var prop in dateTimeProperties)
            {
                var dateTime = new DateTime(2014, 12, 12);
                prop.SetValue(o, dateTime, null);

                var dt = (DateTime)prop.GetValue(o, null);
                Assert.AreEqual(dt.Kind, DateTimeKind.Local);
            }
        }
    }
}
Ну и более специфический тест, который позволяет проверить всю сборку на валидность проставления свойства Kind.
[TestMethodTestCategory("Unit")]
public void AllServerEntities_set_datetime_properties_to_local()
{
    var assembly= AppDomain.CurrentDomain.GetAssemblies().
        FirstOrDefault(assm => assm.GetName().Name == "Entities");

    Assert.IsNotNull(assembly);

    var types = assembly.GetExportedTypes().Where(x => x.IsSubclassOf(typeof (Entity))
        && !x.ContainsGenericParameters 
        && !x.IsAbstract).ToList();
    foreach (var type in types)
    {
        var count =
            type.GetProperties()
                .Count(p => p.PropertyType == typeof (DateTime) || p.PropertyType == typeof (DateTime?));

        if(count == 0)
            continue;

        object o = Activator.CreateInstance(type);
        var obj = o.GetType();

        var dateTimeProperties = obj.GetProperties().Where(x => x.PropertyType == typeof(DateTime)).ToList();
        if (dateTimeProperties.Count > 0)
        {
            foreach (var prop in dateTimeProperties)
            {
                var dateTime = new DateTime(2014, 12, 12);
                prop.SetValue(o, dateTime, null);

                var dt = (DateTime)prop.GetValue(o, null);
                Assert.AreEqual(dt.Kind, DateTimeKind.Local);
            }
        }
    }
}
В данном примере у нас есть базовый класс Entity, от которого наследуются все остальные классы. Как покрыть этот класс с помощью юнит-тестов и рефлексии, показано выше.
Итоги
В статье мы рассмотрели разные способы работы с DateTime в контексте EF, если нужно отображать дату в локали пользователя. В свое время поиск решения для конкретной задачи занял прилично времени, поэтому я решил собрать все в одном месте, чтобы вы могли использовать понравившееся вам решение в своих проектах.