Friday, November 1, 2013

Ошибки при использовании EF.

Классический пример использования  byte типов данных в Entity Model (SQL Type - tinyint). Классическая ошибка, которую мы допустили в проекте, заключалась в следующем коде:
const byte inProgress = (byte)SessionStatus.InProgress;
using (var context = new SystemDatabaseContext())
    return context.Entity.Session
          .Where(s => s.Status == inProgress).ToList()
          .Select(s => new Tuple<long, long>(s.Id, s.MessageId));
Основная ошибка этого кода в том, что код на выходе будет такой:

SELECT [Extent1].[Id]              AS [Id],
       [Extent1].[MessageId]       AS [MessageId],
       [Extent1].[SystemId]        AS [SystemId],
       [Extent1].[Status]          AS [Status],
       [Extent1].[CreateDate]      AS [CreateDate],
       [Extent1].[ChangeDate]      AS [ChangeDate],
       [Extent1].[TransferredSize] AS [TransferredSize],
       [Extent1].[ErrorCode]       AS [ErrorCode],
       [Extent1].[ErrorText]       AS [ErrorText]
FROM   [dbo].[Session] AS [Extent1]

WHERE  0 = CAST([Extent1].[Status] AS int)

Имеем лишнее преобразование byte в  int. Решается эта проблема следующим образом:
const byte inProgress = (byte) SessionStatus.InProgress;
            var tinyintComparison = new List<byte> { inProgress };
            using (var context = new SystemDatabaseContext())
            {
                return context.Entity.Session
                              .Where(s => tinyintComparison.Contains(s.Status)).ToList()
                              .Select(s => new Tuple<long, long>(s.Id, s.MessageId));
            }
На выходе имеем такой результат:

SELECT [Extent1].[Id]              AS [Id],
       [Extent1].[MessageId]       AS [MessageId],
       [Extent1].[SystemId]        AS [SystemId],
       [Extent1].[Status]          AS [Status],
       [Extent1].[CreateDate]      AS [CreateDate],
       [Extent1].[ChangeDate]      AS [ChangeDate],
       [Extent1].[TransferredSize] AS [TransferredSize],
       [Extent1].[ErrorCode]       AS [ErrorCode],
       [Extent1].[ErrorText]       AS [ErrorText]
FROM   [dbo].[Session] AS [Extent1]

WHERE  0 = [Extent1].[Status]

Данный код работает в несколько раз быстрее, чем предыдущая версия. Очень ощутимые результаты, когда данных много и поле индексированное.  Индексация получается в первом примере не участвует . Во втором можно увидеть преимущество выборки, когда используем индексацию. Не используется лишнее преобразование типов, и выборка данных происходит в 10 раз быстрее.  http://blog.hompus.nl/2013/01/21/filtering-on-a-tinyint-with-entity-framework/
Вторая ошибка была скрыта в приведенном коде:
var selectData = (from organization in Entity.Organization
                              join executive in Entity.SystemExecutive
                                  on organization.Id equals executive.OrgId
                              where !executive.Disabled
                              select new { organization.Id, organization.Edrpou })
                    .ToList();.
По времени выполнение данного запроса занимает 4ms. Из-за того что запрос приводится к  такому виду:

SELECT [Extent1].[Id]     AS [Id],
       [Extent1].[Edrpou] AS [Edrpou]
FROM   [dbo].[Organization] AS [Extent1]
       INNER JOIN [dbo].[SystemExecutive] AS [Extent2]
         ON [Extent1].[Id] = [Extent2].[OrgId]

WHERE  [Extent2].[Disabled] <> cast(1 as bit)

Заменяем знак != на == и вот что имеем:
var selectData = (from organization in Entity.Organization
                              join executive in Entity.SystemExecutive
                                  on organization.Id equals executive.OrgId
                              where executive.Disabled == false
                              select new { organization.Id, organization.Edrpou })
                    .ToList();

Результат выполнения 2ms.  Данные не совсем корректны, потому используемая нами таблица была небольшой. На больших таблицах прирост производительности будет существеннее. Ниже представлен результат преобразования приведенного выше кода в SQL.

SELECT [Extent1].[Id]     AS [Id],
       [Extent1].[Edrpou] AS [Edrpou]
FROM   [dbo].[Organization] AS [Extent1]
       INNER JOIN [dbo].[SystemExecutive] AS [Extent2]
         ON [Extent1].[Id] = [Extent2].[OrgId]

WHERE  0 = [Extent2].[Disabled]

Следующая проблема была в использовании N+1 антипаттерна.
Select N+1
Select N + 1 is a data access anti-pattern where the database is accessed in a suboptimal way. Take a look at this code sample, then we'll discuss what is going on. Say you want to show the user all comments from all posts so that they can delete all of the nasty comments. The naive implementation would be something like:
var posts = Dao.GetRecentPosts();
foreach (var post in posts)
{
    post.Comments = Dao.GetCommentsForPost(post);
}

public IEnumerable<Post> GetRecentPosts()
{
    using(var ctx= new MyBlogContext())
    {
        return (from post in ctx.Posts
            orderby post.PublishedDate descending
            select post)
            .Take(50)
            .ToList();
    }
}
public IEnumerable<Comment> GetCommentsForPost(Post post)
{
    using(var ctx= new MyBlogContext())
    {
        return (from comment in ctx.Comments
            where comment.Post.Id = post.Id
            select comment)
            .ToList();
    }
}
Пример решения может выглядеть следующим образом:
// SELECT * FROM Posts JOIN Comments ...
var postsQuery = (from post in blogDataContext.Posts.Include("Comments")
                    select post);
foreach (Post post in postsQuery)
{
    // no lazy loading of comments list causes  
    foreach (Comment comment in post.Comments)
    {
        //print comment... 
    }

}
Мы указываем в запросе, какие части объектной модели мы хотим включить в первоначальный запрос. Более подробно об решении этой проблемы можно посмотреть здесь: http://www.hibernatingrhinos.com/products/efprof/learn/alert/SelectNPlusOne

No comments:

Post a Comment