Una práctica común en la programación es utilizar un bucle "foreach" para iterar sobre una colección de elementos. Sin embargo, cuando se trata de trabajar con Entity Framework, es importante tener en cuenta que no es recomendable colocar código de Entity Framework directamente dentro de un bucle "foreach". ❌
En este artículo veremos el porqué tenés que dejar de usar Entity Framework dentro de bucles y te voy a mostrar ejemplos de situaciones reales y mejorar tu código. 🙌
El problema de usar EFCore en bucles
La razón principal radica en el rendimiento y la eficiencia de nuestras consultas a la base de datos. Cada vez que se ejecuta una consulta de Entity Framework, se establece una conexión con la base de datos y se realiza una solicitud para recuperar los datos. Si colocamos ese código dentro de un bucle "foreach", se generará una nueva consulta para cada iteración del bucle. Esto puede tener un impacto significativo en el rendimiento, especialmente cuando se trabaja con grandes conjuntos de datos.
En lugar de eso, se recomienda realizar una consulta única para recuperar todos los datos necesarios antes de entrar al bucle "foreach". De esta manera, minimizamos el número de consultas a la base de datos y aprovechamos la capacidad de Entity Framework para recuperar datos de manera eficiente. Además, podemos almacenar los datos en una estructura de datos local, como una lista o un diccionario, y luego iterar sobre ellos con el bucle "foreach". Esto reduce la carga en la base de datos y mejora el rendimiento general de nuestra aplicación.
Situaciones Reales
A continuación voy a mostrar distintas situaciones que pueden evitar y les muestro como hacerlo. 👌
Utilizaré los métodos Async para hacer referencia a métodos de EFCore y aquellos métodos Sync para hacer referencia a los métodos comunes de Linq.
Usar AddRange en vez de Add
Antes ❌
El código recorre una lista de objetos personDto
y para cada elemento, se convierte a un objeto person
utilizando el método ConvertToEntity()
. Luego, se agrega cada objeto person
al contexto de la base de datos utilizando context.People.Add(person)
. Finalmente, se guardan los cambios en la base de datos utilizando context.SaveChanges()
.
foreach (var personDto in peopleDto)
{
var person = personDto.ConvertToEntity();
await context.People.AddAsync(person);
}
await context.SaveChangesAsync();
Ahora ✔
Creamos una lista auxiliar de personas (people
) y recorremos la otra lista de objetos personDto
. En cada iteración, se convierte cada personDto
a un objeto person
, que luego se agrega a la lista people
. Luego, se agrega toda la lista people
a la base de datos utilizando Entity Framework Core.
List<Person> people = new();
foreach (var personDto in peopleDto)
{
var person = personDto.ConvertToEntity();
people.Add(person);
}
await context.AddRangeAsync(people);
await context.SaveChangesAsync();
Este primer caso es muy básico y haciendo este cambio no ganamos casi nada de rendimiento. Pero lo que sí ganamos es un montón de calidad. Ahora nuestro foreach
no está acoplado a código de EFCore ganando así mucha más escalabilidad y mantenibilidad.
FirstOrDefault dentro de bucles
Antes ❌
Este código busca productos en la base de datos utilizando una lista de IDs proporcionada. Para cada ID, se realiza una búsqueda asincrónica utilizando Entity Framework.
int[] ids = { 1, 2, 3, 4, 5 }; // Array de IDs
foreach (int id in ids)
{
Product product = await context.Products.FirstOrDefaultAsync(p => p.Id == id);
if (producto != null)
{
Console.WriteLine($"Producto encontrado - ID: {producto.Id}, Nombre: {producto.Nombre}");
}
}
Ahora ✔
Este código busca productos en la base de datos utilizando una lista de IDs proporcionada. Primero, se realiza una consulta asincrónica para obtener, con una única consulta, los productos con los IDs deseados. Luego, se recorre la lista de IDs y se busca cada producto correspondiente en la lista obtenida previamente.
int[] ids = { 1, 2, 3, 4, 5 }; // Array de IDs
List<Product> products = await context.Products.Where(p => ids.Contains(p.Id)).ToListAsync();
foreach (int id in ids)
{
Product product = products.FirstOrDefault(p => p.Id == id);
if (producto != null)
{
Console.WriteLine($"Producto encontrado - ID: {producto.Id}, Nombre: {producto.Nombre}");
}
}
A continuación, les muestro el SQL que genera EFCore con esta nueva estrategia.
SELECT *
FROM Products
WHERE Id IN (1, 2, 3, 4, 5)
Validaciones dentro de bucles
Antes ❌
Este código valida la existencia de los productos en la base de datos utilizando una lista de IDs proporcionada. Para cada ID, se realiza una búsqueda asincrónica utilizando Entity Framework.
int[] ids = { 1, 2, 3, 4, 5 }; // Array de IDs
foreach (int id in ids)
{
var productExists = await context.Products.AnyAsync(p => p.Id == id);
if (productExists)
{
Console.WriteLine($"Producto encontrado - ID: {id}");
}
}
Ahora ✔
Este código busca productos en la base de datos utilizando una lista de IDs proporcionada. Primero, se realiza una consulta asincrónica para obtener, con una única consulta, solamente los IDs de los productos deseados. Luego, se recorre la lista de IDs original y se válida cada ID con la lista existingProductIds
obtenida previamente.
int[] ids = { 1, 2, 3, 4, 5 }; // Array de IDs
List<int> existingProductIds = await context.Products.Where(p => ids.Contains(p.Id)).Select(p => p.Id).ToListAsync();
foreach (int id in ids)
{
bool productExists = existingProductId.Any(x => x == id);
if (productExists)
{
Console.WriteLine($"Producto encontrado - ID: {id}");
}
}
A continuación, les muestro el SQL que genera EFCore con esta nueva estrategia.
SELECT Id
FROM Products
WHERE Id IN (1, 2, 3, 4, 5)
Bonus Track: Evitar usar metodos Async dentro de bucles
Así como venimos explicando el porqué hay que evitar usar Entity Framework dentro de bucles, pues las razones son las mismas si fuera usar HttpClient.
Así que también te dejo una situación (hoy será una sola 😉) de cómo evitar usar métodos Async dentro de bucles.
Antes ❌
Este código obtiene una lista de productos. Cada producto tiene un precio en una moneda específica (ARS, USD, EUR). Por cada producto, hay que obtener cuanto vale actualmente la moneda establecida.
El problema está en que el foreach puede llegar a hacer 10 iteraciones en las cuales las 10 veces va a consultar a un método async el valor de USD. ¿Es necesario buscar 10 veces el mismo valor? No lo creo.
List<Product> products = await context.Products.ToListAsync(); // Podemos llegar a tener 10 productos
foreach (product in products)
{
Currency currency = await _currencyService.GetByIdAsync(product.CurrencyId); // CurrencyId puede llegar a ser ARS, USD o EUR
if (currency != null)
{
Console.WriteLine($"Moneda encontrada - ID: {currency.Id}, Valor: {currency.Rate}");
}
}
Ahora ✔
Lamentablemente, CurrencyService internamente usa HttpClient para comunicarse con una API externa del cual no tenemos control. Por lo tanto, no podemos evitar que se llame dentro de un foreach, pero si pudimos lograr que en vez de llamar a esa API 10 veces, se llame solamente la cantidad necesaria juntando los IDs de monedas con Distinct()
.
Este código, por más que sea más complejo, escala para que en la lista de productos haya 10, 100 o 1000 ítems y aun así se llame máximo 3 veces a CurrencyService.GetByAsync().
List<Product> products = await context.Products.ToListAsync(); // Podemos llegar a tener 10 productos
List<string> currencyIds = products.Select(p => p.CurrencyId).Distinct();
List<Currency> currencies = new();
foreach(currencyId in currencyIds)
{
Currency currency = await _currencyService.GetByIdAsync(product.CurrencyId); // CurrencyId puede llegar a ser ARS, USD o EUR
currencies.Add(currency);
}
foreach (product in products)
{
Currency currency = currencies.First(c => c.Id == product.CurrencyId);
if (currency != null)
{
Console.WriteLine($"Moneda encontrada - ID: {currency.Id}, Valor: {currency.Rate}");
}
}
Conclusión
En resumen, evitar colocar código de Entity Framework dentro de un bucle "foreach" es una práctica recomendada para optimizar el rendimiento de nuestras consultas y minimizar la carga en la base de datos. Al realizar una consulta única y almacenar los datos localmente antes de iterar sobre ellos, podemos lograr una ejecución más eficiente y aprovechar al máximo las capacidades de Entity Framework.
Si encuentras útil este trabajo y deseas mostrar tu apoyo, siempre puedes invitarme a un buen café ☕.