Entity Framework
Techniques d'optimisation dans EF
Recherche…
Utiliser AsNoTracking
Mauvais exemple:
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location;
Comme le code ci-dessus renvoie simplement une entité sans modification ni ajout, nous pouvons éviter le suivi des coûts.
Bon exemple:
var location = dbContext.Location.AsNoTracking()
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location;
Lorsque nous utilisons la fonction AsNoTracking()
nous indiquons explicitement à Entity Framework que les entités ne sont pas suivies par le contexte. Cela peut être particulièrement utile lors de la récupération de grandes quantités de données à partir de votre magasin de données. Si vous souhaitez apporter des modifications aux entités non suivies, n'oubliez pas de les joindre avant d'appeler SaveChanges
.
Chargement des données requises uniquement
Un problème souvent rencontré dans le code est le chargement de toutes les données. Cela augmentera considérablement la charge sur le serveur.
Disons que j'ai un modèle appelé "location" qui contient 10 champs, mais tous les champs ne sont pas requis en même temps. Disons que je ne veux que le paramètre 'LocationName' de ce modèle.
Mauvais exemple
var location = dbContext.Location.AsNoTracking()
.Where(l => l.Location.ID == location_ID)
.SingleOrDefault();
return location.Name;
Bon exemple
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.Select(l => l.LocationName);
.SingleOrDefault();
return location;
Le code dans le "bon exemple" ne récupérera que "LocationName" et rien d'autre.
Notez que puisque aucune entité n'est matérialisée dans cet exemple, AsNoTracking()
n'est pas nécessaire. Il n'y a rien à suivre de toute façon.
Récupérer plus de champs avec les types anonymes
var location = dbContext.Location
.Where(l => l.Location.ID == location_ID)
.Select(l => new { Name = l.LocationName, Area = l.LocationArea })
.SingleOrDefault();
return location.Name + " has an area of " + location.Area;
Identique à l'exemple précédent, seuls les champs 'LocationName' et 'LocationArea' seront extraits de la base de données, le type anonyme peut contenir autant de valeurs que vous le souhaitez.
Exécutez les requêtes dans la base de données lorsque cela est possible, pas en mémoire.
Supposons que nous voulons compter le nombre de comtés au Texas:
var counties = dbContext.States.Single(s => s.Code == "tx").Counties.Count();
La requête est correcte mais inefficace. States.Single(…)
charge un état depuis la base de données. Ensuite, Counties
charge les 254 comtés avec tous leurs champs dans une seconde requête. .Count()
est ensuite effectué en mémoire sur la collection Counties
chargée.
Nous avons chargé beaucoup de données dont nous n'avons pas besoin et nous pouvons faire mieux:
var counties = dbContext.Counties.Count(c => c.State.Code == "tx");
Ici, nous ne faisons qu'une seule requête, qui en SQL se traduit par un compte et une jointure. Nous ne renvoyons que le compte de la base de données - nous avons enregistré les lignes, les champs et la création d’objets.
Il est facile de voir où la requête est faite en examinant le type de collection: IQueryable<T>
vs IEnumerable<T>
.
Exécuter plusieurs requêtes asynchrones et parallèles
Lorsque vous utilisez des requêtes asynchrones, vous pouvez exécuter plusieurs requêtes en même temps, mais pas dans le même contexte. Si le temps d'exécution d'une requête est de 10 secondes, l'heure du mauvais exemple sera de 20 secondes, alors que le bon exemple sera de 10 secondes.
Mauvais exemple
IEnumerable<TResult1> result1;
IEnumerable<TResult2> result2;
using(var context = new Context())
{
result1 = await context.Set<TResult1>().ToListAsync().ConfigureAwait(false);
result2 = await context.Set<TResult1>().ToListAsync().ConfigureAwait(false);
}
Bon exemple
public async Task<IEnumerable<TResult>> GetResult<TResult>()
{
using(var context = new Context())
{
return await context.Set<TResult1>().ToListAsync().ConfigureAwait(false);
}
}
IEnumerable<TResult1> result1;
IEnumerable<TResult2> result2;
var result1Task = GetResult<TResult1>();
var result2Task = GetResult<TResult2>();
await Task.WhenAll(result1Task, result2Task).ConfigureAwait(false);
var result1 = result1Task.Result;
var result2 = result2Task.Result;
Désactiver le suivi des modifications et la génération de proxy
Si vous voulez simplement obtenir des données sans rien modifier, vous pouvez désactiver le suivi des modifications et la création de proxy. Cela améliorera vos performances et empêchera également le chargement paresseux.
Mauvais exemple:
using(var context = new Context())
{
return await context.Set<MyEntity>().ToListAsync().ConfigureAwait(false);
}
Bon exemple:
using(var context = new Context())
{
context.Configuration.AutoDetectChangesEnabled = false;
context.Configuration.ProxyCreationEnabled = false;
return await context.Set<MyEntity>().ToListAsync().ConfigureAwait(false);
}
Il est particulièrement courant de les désactiver dans le constructeur de votre contexte, surtout si vous souhaitez les définir dans votre solution:
public class MyContext : DbContext
{
public MyContext()
: base("MyContext")
{
Configuration.AutoDetectChangesEnabled = false;
Configuration.ProxyCreationEnabled = false;
}
//snip
}
Travailler avec des entités de stub
Disons que nous avons des Product
et des Category
dans une relation plusieurs-à-plusieurs:
public class Product
{
public Product()
{
Categories = new HashSet<Category>();
}
public int ProductId { get; set; }
public string ProductName { get; set; }
public virtual ICollection<Category> Categories { get; private set; }
}
public class Category
{
public Category()
{
Products = new HashSet<Product>();
}
public int CategoryId { get; set; }
public string CategoryName { get; set; }
public virtual ICollection<Product> Products { get; set; }
}
Si nous voulons ajouter une Category
à un Product
, nous devons charger le produit et ajouter la catégorie à ses Categories
, par exemple:
Mauvais exemple:
var product = db.Products.Find(1);
var category = db.Categories.Find(2);
product.Categories.Add(category);
db.SaveChanges();
(où db
est une sous-classe DbContext
).
Cela crée un enregistrement dans la table de jonction entre le Product
et la Category
. Cependant, cette table ne contient que deux valeurs Id
. Charger deux entités complètes pour créer un enregistrement minuscule est un gaspillage de ressources.
Un moyen plus efficace consiste à utiliser des entités stub , c'est-à-dire des objets entité créés en mémoire, contenant uniquement le minimum de données, généralement une valeur Id
. Voici à quoi ça ressemble:
Bon exemple:
// Create two stub entities
var product = new Product { ProductId = 1 };
var category = new Category { CategoryId = 2 };
// Attach the stub entities to the context
db.Entry(product).State = System.Data.Entity.EntityState.Unchanged;
db.Entry(category).State = System.Data.Entity.EntityState.Unchanged;
product.Categories.Add(category);
db.SaveChanges();
Le résultat final est le même, mais cela évite deux allers-retours à la base de données.
Prévenir les doublons
Si vous souhaitez vérifier si l'association existe déjà, une requête bon marché suffit. Par exemple:
var exists = db.Categories.Any(c => c.Id == 1 && c.Products.Any(p => p.Id == 14));
Encore une fois, cela ne chargera pas les entités complètes en mémoire. Il interroge efficacement la table de jonction et ne renvoie qu'un booléen.