Entity Framework Core atraviesa grandes datos de blob sin desbordamiento de memoria, mejor práctica

8

Estoy escribiendo un código que atraviesa grandes cantidades de datos de imágenes, preparando un gran bloque delta que lo contiene todo comprimido para enviar.

Aquí hay una muestra de cómo podrían ser estos datos

[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);
    }
}

Cuando trabajo con esto, proceso todo en un flujo de archivos, y quiero mantener la menor cantidad posible en la memoria en cualquier momento.

¿Es suficiente hacerlo así?

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

¿Esto todavía llenará la memoria con todos los registros de blob, o serán procesados ​​uno por uno a medida que itere en el enumerador? No está utilizando ninguna lista de ToList, solo el enumerador, por lo que Entity Framework debería poder procesarlo sobre la marcha, pero no estoy seguro de si eso es lo que hace.

Cualquier experto en Entity Framework aquí que pueda dar alguna orientación sobre cómo se maneja esto correctamente.

Atle S
fuente
No estoy 100% seguro, pero creo que esto dará como resultado que se envíe una sola consulta a la base de datos, sin embargo, la procesa en el lado c # 1 por 1. (puede verificar esto con el perfilador SQL) puede cambiar su ciclo y use skip and take para asegurarse de que está obteniendo un solo artículo, sin embargo, esto no es para lo que está hecho ef, así que no estoy seguro de si va a encontrar una mejor práctica.
Joost K
Si entiendo correctamente, SqlDataReader establecerá una conexión con la base de datos y buscará partes mientras está iterando Read (). Si el enumerador funciona de la misma manera aquí, debería estar bien. Pero si lo almacena todo, y luego itera, tenemos un problema. ¿Hay alguien aquí que pueda confirmar cómo funciona esto? Quiero que ejecute una sola consulta, pero tenga una conexión continua a la base de datos y funcione a medida que avanza con los datos, procesando y liberando una entidad a la vez.
Atle S
¿Por qué no haces un perfil de memoria de tu código? No podemos hacer eso por ti. Además, la pregunta es amplia / poco clara (y se pondría en espera como tal si no fuera por la recompensa) debido a componentes desconocidos y el código circundante. (Como, ¿de dónde streamviene?). Finalmente, el manejo rápido de datos y transmisión de archivos de SQL Server requiere un enfoque diferente que va más allá de EF.
Gert Arnold

Respuestas:

1

En general, cuando crea un filtro LINQ en una entidad, es como escribir una declaración SQL en forma de código. Devuelve un archivo IQueryableque no se ha ejecutado realmente en la base de datos. Cuando itera sobre IQueryablecon una foreachllamada o ToList()se ejecuta el sql y todos los resultados se devuelven y se almacenan en la memoria.

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

Si bien EF no es la mejor opción para un rendimiento puro, hay una forma relativamente simple de manejar esto sin preocuparse demasiado por el uso de la memoria:

Considera lo siguiente

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

Ahora ha filtrado los blobs de acuerdo con sus requisitos, y ejecutó esto en la base de datos, pero solo devolvió los valores de Id en la memoria.

Entonces

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
}

El blob grande debe estar disponible para la recolección de basura una vez que haya terminado con él, y no debe quedarse sin memoria.

Obviamente, puede verificar el número de registros en la lista de identificación, o puede agregar metadatos a la primera consulta para ayudarlo a decidir cómo procesarlo si desea refinar la idea.

ste-fu
fuente
1
Esto no responde mi pregunta. Quería saber si EF maneja la recuperación de la consulta de forma secuencial al recorrer el enumerador, de la misma manera que SqlDataReader lo hace con Next. Debería ser posible, y también es la forma preferida en lugar de buscar uno por uno. Lo más cerca que he estado de una respuesta aquí es lo que Smit Patel dice en una respuesta aquí: github.com/aspnet/EntityFrameworkCore/issues/14640 Él dice "Lo que eso significa es que no necesitaríamos almacenamiento interno. Por lo tanto, en su caso, una consulta sin seguimiento no obtendría / almacenaría más datos de lo que es la fila de resultados actual ".
Atle S
Si puede confirmar al 100% que EF recupera todo antes de enumerar, eso sería parte de una respuesta, si también proporciona una forma de usar SqlDataReader para hacerlo de la manera correcta. O si EF realmente hace esto correctamente, una confirmación sobre eso sería una respuesta. De todos modos, esto está comenzando a tomar más tiempo del que me llevaría depurar EF para una confirmación;)
Atle S
Lo siento, cavé un poco pero no llegué al fondo. Sugeriría que si está preocupado por el rendimiento puro, EF no es el camino a seguir, si desea mantener el paradigma de EF, entonces mi respuesta asegura que no se quedará sin memoria. Suponiendo que Idtiene un índice agrupado, el rendimiento de muchas consultas secuenciales puede no ser tan malo como cree.
ste-fu