Entity Framework Core atravessa grandes dados de blob sem excesso de memória, prática recomendada

8

Estou escrevendo um código que percorre grandes quantidades de dados de imagem, preparando um grande bloco delta contendo tudo compactado para envio.

Aqui está uma amostra de como esses dados podem ser

[MessagePackObject]
public class Blob : VersionEntity
{
    [Key(2)]
    public Guid Id { get; set; }
    [Key(3)]
    public DateTime CreatedAt { get; set; }
    [Key(4)]
    public string Mediatype { get; set; }
    [Key(5)]
    public string Filename { get; set; }
    [Key(6)]
    public string Comment { get; set; }
    [Key(7)]
    public byte[] Data { get; set; }
    [Key(8)]
    public bool IsTemporarySmall { get; set; }
}

public class BlobDbContext : DbContext
{
    public DbSet<Blob> Blob { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blob>().HasKey(o => o.Id);
    }
}

Ao trabalhar com isso, processo tudo em um fluxo de arquivos e quero manter o mínimo possível na memória a qualquer momento.

É o suficiente para fazer assim?

foreach(var b in context.Where(o => somefilters).AsNoTracking())
    MessagePackSerializer.Serialize(stream, b);

Isso ainda preencherá a memória com todos os registros de blob ou eles serão processados ​​um por um, conforme eu iterar no enumerador. Ele não está usando nenhum ToList, apenas o enumerador; portanto, o Entity Framework deve poder processá-lo em qualquer lugar, mas não tenho certeza se é isso que ele faz.

Qualquer especialista em Entity Framework aqui que possa dar alguma orientação sobre como isso é tratado adequadamente.

Atle S
fonte
Eu não tenho 100% de certeza, mas acho que isso resultará em uma única consulta sendo enviada ao banco de dados, mas processa-a no lado do c # 1 por 1. (você pode verificar isso com o sql profiler), você pode alterar seu loop e use skip and take para garantir que você esteja recebendo um único item; no entanto, não é para isso que se destina, então não tenho certeza se você encontrará as melhores práticas.
Joost K
Se bem entendi, o SqlDataReader fará uma conexão com o banco de dados e buscará partes enquanto você estiver repetindo Read (). Se o enumerador funcionar da mesma maneira aqui, tudo ficará bem. Mas se ele armazenar em buffer tudo e, em seguida, iterar, temos um problema. Alguém aqui pode confirmar como isso funciona? Quero que ele execute uma única consulta, mas tenha uma conexão de fluxo com o banco de dados e funcione conforme os dados, processando e liberando uma entidade por vez.
Atle S
Por que você não perfila seu código na memória? Não podemos fazer isso por você. Além disso, a questão é ampla / incerta (e seria colocada em espera como tal se não fosse a recompensa) por causa de componentes desconhecidos e código circundante. (Como, de onde streamvem?). Por fim, o manuseio rápido dos dados do fluxo de arquivos do SQL Server e o streaming requerem uma abordagem diferente que está além do EF.
Gert Arnold

Respostas:

1

Em geral, quando você cria um filtro LINQ em uma Entidade, é como escrever uma instrução SQL em formato de código. Ele retorna um IQueryableque não foi realmente executado no banco de dados. Quando você repete a chamada IQueryablecom a foreachou ToList(), o sql é executado e todos os resultados são retornados e armazenados na memória.

https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language-reference/query-execution

Embora a EF talvez não seja a melhor opção para desempenho puro, existe uma maneira relativamente simples de lidar com isso sem se preocupar muito com o uso de memória:

Considere o seguinte

var filteredIds = BlobDbContext.Blobs
                      .Where(b => b.SomeProperty == "SomeValue")
                      .Select(x => x.Id)
                      .ToList();

Agora você filtrou os Blobs de acordo com seus requisitos e executou isso no banco de dados, mas retornou apenas os valores de ID na memória.

Então

foreach (var id in filteredIds)
{
    var blob = BlobDbContext.Blobs.AsNoTracking().Single(x => x.Id == id);
    // Do your work here against a single in-memory blob
}

O blob grande deve estar disponível para coleta de lixo assim que você terminar e não ficará sem memória.

Obviamente, você pode verificar o número de registros na lista de IDs ou adicionar metadados à primeira consulta para ajudá-lo a decidir como processá-lo, se quiser refinar a ideia.

ste-fu
fonte
1
Isso não responde à minha pergunta. Eu queria saber se o EF lida com a busca da consulta em um assunto seqüencial ao atravessar o enumerador, da maneira que SqlDataReader faz com o Next. Deve ser possível, e também é a maneira preferida, em vez de buscar uma por uma. O mais próximo que estive de uma resposta aqui é o que Smit Patel diz em uma resposta aqui: github.com/aspnet/EntityFrameworkCore/issues/14640 Ele diz "O que isso significa é que não precisaríamos de buffer interno. Portanto, no seu Nesse caso, uma consulta sem rastreamento não obteria / armazenaria mais dados do que a linha de resultados atual. ".
Atle S
Se você puder confirmar 100% de que o EF busca tudo antes da enumeração, isso seria parte de uma resposta, se você também fornecer uma maneira de usar o SqlDataReader para fazê-lo da maneira correta. Ou, se a EF realmente fizer isso corretamente, uma confirmação disso seria uma resposta. De qualquer forma, isso está começando a levar mais tempo do que ele me levaria a EF de depuração para uma confirmação;)
Atle S
Desculpe - fiz uma pequena escavação, mas não cheguei ao fundo. Eu sugeriria que, se você está preocupado com o desempenho puro, a EF não é o caminho a seguir, se você deseja manter o paradigma da EF, então minha resposta garante que você não ficará sem memória. Supondo que o Idíndice tenha um cluster, o impacto no desempenho de muitas consultas seqüenciais pode não ser tão ruim quanto você pensa.
ste-fu