Comment résoudre la mauvaise initialisation de la collecte de nHibernate
nHibernate3; extraire des enregistrements 4xxx d'un schéma de données EAV. Quand nHibernate, ou .NET, va initialiser ces collections pour la première fois, nous voyons une pénalité sévère. Les appels subséquents semblent donner de meilleurs résultats. En exécutant les mêmes requêtes dans SQL Server Management Studio, vous obtenez des temps de retour rapides attendus.
utilisant Fluent et runtime mapping au lieu de .hbm.xml; curieux de savoir si sérialisé cartographie de l'aide ici?
nHibernate Profiler et log4net logging ne semblaient pas me donner beaucoup à faire. Un total de quelque 140 000 entités sont hydratées dans ce processus.
ci-joint une capture d'écran de ma recherche de performances dotTrace qui montre la pénalité d'initialisation de la collection:
ont essayé join et impatient fetchtypes, sans résultats apparents, mais je ne suis pas sûr à 100% que j'ai mis en œuvre ceux correctement -- est-ce que juste le parent doit être ainsi les tables pour enfants doivent-elles aussi être marquées?
var products = ((HandleSession)_handleSession).Session.CreateCriteria(typeof(Product))
.SetFetchMode("Product", FetchMode.Eager)
.List<Product>()
.AsEnumerable();
avec optimiseur de réflexion activé (je pense) via le web.config:
C'est ici que l'on passe le plus de temps:
return new ProductList(products.Select(p => p.ToProductContract()));
qui est simplement une méthode d'extension faisant ceci:
public static ProductContract ToProductContract(this Product product)
{
return new ProductContract
{
Name = product.ProductName,
ProductTypeName = product.ProductType.ProductTypeName,
UpdateTimeStamp = product.UpdateDateTime,
ProductNumber = product.ProductNumber,
Attributes = product.ProductAttributes.ToCommonAttribute().ToList(),
GroupCategories = product.ProductGroups.ToGroupCategory().ToList(),
PublicUniqueId = product.PublicUniqueId
};
}
mappings:
internal class ProductMapping : ClassMap<Product>
{
private const string _iscurrentindicator = "IsCurrentIndicator=1";
public ProductMapping()
{
Table("Product");
Id(Reveal.Member<Product>("ProductId")).GeneratedBy.Identity().Column("ProductID");
Map(x => x.ProductNumber).Column("ProductNumber").Not.Nullable();
Map(x => x.ProductName).Column("ProductName").Not.Nullable();
Map(x => x.InsertDateTime).Column("InsertedDateTime").Nullable().ReadOnly();
Map(x => x.UpdateDateTime).Column("UpdatedDateTime").Nullable();
Map(x => x.PublicUniqueId).Column("ProductGUID").Generated.Insert();
References(x => x.ProductType).Column("ProductTypeId").Not.Nullable();
HasMany(x => x.ProductAttributes)
.KeyColumn("ProductId")
.Inverse()
.Fetch
.Subselect()
.Where(_iscurrentindicator)
.Cascade
.SaveUpdate();
HasMany(x => x.ProductGroups).KeyColumn("ProductId").Fetch.Subselect().Where(_iscurrentindicator);
DynamicUpdate();
DynamicInsert();
BatchSize(500);
}
}
internal class ProductGroupMapping : ClassMap<ProductGroup>
{
public ProductGroupMapping()
{
Table("ProductGroup");
Id(x => x.ProductGroupId).Column("ProductGroupId").GeneratedBy.Identity();
References(x => x.Product).Column("ProductId").Not.Nullable();
References(x => x.Group).Column("GroupId").Not.Nullable();
//Where("IsCurrentIndicator=1");
}
}
internal class ProductAttributeMapping : ClassMap<ProductAttribute>
{
public ProductAttributeMapping()
{
Table("ProductAttribute");
LazyLoad();
Id(x => x.ProductAttributeId).GeneratedBy.Identity().Column("ProductAttributeID");
References(x => x.Product).Column("ProductID").Not.Nullable();
References(x => x.Attribute).Column("AttributeID").Not.Nullable().Fetch.Join();
Map(x => x.PositionNumber).Column("PositionNumber").Nullable();
Map(x => x.ValueText).Column("ValueText").Nullable();
Map(x => x.ValueBinary).Column("ValueBinary").Nullable();
Component(x => x.OperationalAuditHistory, m =>
{
Table("ProductAttribute");
m.Map(x => x.ExpirationDateTime).Column("ExpirationDateTime").Nullable();
m.Map(x => x.IsCurrent).Column("IsCurrentIndicator").Not.Nullable();
m.Map(x => x.OperationCode).Column("OperationCode").Nullable();
m.Map(x => x.OperationDateTime).Column("OperationDateTime").Nullable();
m.Map(x => x.OperationSystemName).Column("OperationSystemName").Nullable();
m.Map(x => x.OperationUserName).Column("OperationUserName").Nullable();
m.Map(x => x.LastUserPriority).Column("LastUserPriority").Nullable();
});
DynamicInsert();
BatchSize(50);
}
}
malheureusement avec .Futur je encore obtenir similaire résultat. Voici une nouvelle trace; je suis passé à Release, et x64 pour les projets clés, pour le moment, donc les temps sont plus bas, mais les proportions sont toujours à peu près les mêmes; ainsi que avec .Eager:
var products = ((HandleSession) _handleSession).Session.CreateCriteria(typeof (Product))
.SetFetchMode("ProductAttribute", FetchMode.Join)
.SetFetchMode("ProductGroup", FetchMode.Join)
.SetFetchMode("ProductType", FetchMode.Join)
.Future<Product>()
.AsEnumerable();
SQL généré avec .Impatient et .Avenir en place:
sélectionnez this_.ProductID comme ProductID0_1_, this_.ProductNumber comme ProductN2_0_1_, this_.ProductName comme Produit3_0_1_, this_.InsertedDateTime comme Inserted4_0_1_, ce._UpdatedDateTime as UpdatedD5_0_1_, this_.ProductGUID comme Produittg6_0_1_, this_.ProductTypeId comme ProductT7_0_1_, producttyp2_.ProductTypeID comme ProductT1_6_0_, producttyp2_.ProductTypeName comme Produit 2_6_0_ à partir du produit_ inner join ProductType producttyp2_ sur ce._ProductTypeId=producttyp2_.Type de produit;
sélectionnez productatt0_.ProductId comme ProductId2_, productatt0_.ProductAttributeID as ProductA1_2_, productatt0_.ProductAttributeID as ProductA1_2_1_, productatt0_.PositionNumber as Position2_2_1_, productatt0_.ValueText comme Valeurext2_1_, productatt0_.ValueBinary as ValueBin4_2_1_, productatt0_.ProductID comme ProductID2_1_, productatt0_.AttributeID as Attribut6_2_1_, productatt0_.ExpirationDateTime as Expirati7_2_1_, productatt0_.IsCurrentIndicator comme IsCurren8_2_1_, productatt0_.OperationCode comme Opératio9_2_1_, productatt0_.OperationDateTime as Opération10_2_1_, productatt0_.Opérationsystemname as Opération11_2_1_, productatt0_.OperationUserName comme Opération12_2_1_, productatt0_.LastUserPriority as Latuse13_2_1_, attribut1_.AttributeId as Attribut1_1_0_, attribut1_.Nom de l'attribut que Attribut2_1_0_, attribut1_.DisplayName as DisplayN3_1_0_, attribut1_.DataTypeName as DataType4_1_0_, attribut1_.ConstraintText comme Constrai5_1_0_, attribut1_.ConstraintMin comme Constrai6_1_0_, attribut1_.ConstraintMax comme Force7_1_0_, attribut1_.ValuesMin comme Valeurmin1_0_, attribut1_.ValuesMax as Valeursmax1_0_, attribut1_.Précision comme précision à partir de la contribution des produits productatt0_ intérieure Attribut de jointure attribut1_ sur productatt0_.AttributeID=attribut1_.AttributeId OÙ (productatt0_.IsCurrentIndicator=1) et productatt0_.ProductId in (select ce._ProductID DE ce Produit_ inner join ProductType producttyp2_ sur ce._ProductTypeId=producttyp2_.Type de produit)
sélectionnez productgro0_.ProductId comme ProductId1_, productgro0_.ProductGroupId comme ProductG1_1_, productgro0_.ProductGroupId comme Produittg1_3_0_, produittgro0_.ProductId comme ProductId3_0_, productgro0_.GroupId comme Grouped3_0_ du groupe de produits productgro0_ OÙ (productgro0_.IsCurrentIndicator=1) et productgro0_.ProductId in (select ce._ProductID DE ce Produit_ inner join ProductType producttyp2_ sur ce._ProductTypeId=producttyp2_.Type de produit)
3 réponses
1) un mappage sérialisé ne contribuera qu'à réduire le temps nécessaire à la construction du logiciel SessionFactory. Si la requête ci-dessus n'est pas le premier accès à la base de données, elle n'apportera rien à cet égard.
2) Set FetchMode doit être appliqué aux enfants, comme ceci:
var products = ((HandleSession)_handleSession).Session.CreateCriteria(typeof(Product))
.SetFetchMode("ProductChildren", FetchMode.Eager)
.List<Product>()
.AsEnumerable();
3) cela ressemble à un problème N+1, si j'interprète les méthodes dans les captures d'écran correctement. Transformez - vous le Products
dans votre requête suite à une liste de ProductDTOs? Si c'est le cas, il semble que les collections de l'enfant sont chargées paresseusement à partir de la base de données à l'intérieur d'une boucle.
Edit:
pour combattre le Select N+1, nous devrons dire à NHibernate de tout Charger à l'avance, de préférence avec des Futures. Voici une solution potentielle qui récupère toutes vos données de la base de données avec une poignée de Select-statements. Je n'ai pas inclus De "où-conditions". Ceux que vous aurait à ajouter en conséquence.
// any where-condition will have to be applied here and in the subsequent queries
var products = session.QueryOver<Product>()
.Future();
var products2 = session.QueryOver<Product>()
.Fetch(p => p.ProductType).Eager
.Future();
var products3 = session.QueryOver<Product>()
.Fetch(p => p.ProductAttributes).Eager
.Future();
var products4 = session.QueryOver<Product>()
.Fetch(p => p.ProductGroups).Eager
.Future();
// Here we execute all of the above queries in one roundtrip.
// Since we already have all the data we could possibly want, there is no need
// for a N+1 Select.
return new ProductList(products.Select(p => p.ToProductContract()));
une option est d'activer la taille de lot sur vos collections. Je suppose que ceux-ci sont paresseux, et avec la taille du lot activé, il essaierait de récupérer des collections pour plusieurs entités dans un seul aller-retour.
cela ne fait pas de différence si vous récupérez 1 entity avec une collection, mais peut faire une énorme différence si vous sélectionnez 1000 entities qui ont toutes une collection. L'utilisation d'une taille de lot de 1000 se traduirait par 2 requêtes au lieu de 1001.
a essayé de trouver de la documentation, mais seulement trouvé cet exemple:
NHibernate alternates lot size
utiliser des stratégies de jointure dans votre cas aboutirait à des résultats gigantesques de sorte que ce n'est pas une bonne option. Une meilleure option serait d'utiliser FetchMode.Sélectionnez ce qui forcerait explicitement vos collections à être chargées dans un aller-retour ultérieur.
une autre chose qui pourrait améliorer les performances est le réglage:
Session.FlushMode = FlushMode.Never;
qui désactive le rinçage automatique de votre portée. C'est utile si vous ne faites que lire des données et non les modifier. Cependant, vous verriez des appels à IsDirty ou tout autre chèque pour des objets sales dans votre callstack.
si vous utilisez cette session uniquement pour le reporting, vous devez utiliser les Sessions Stateless: http://nhforge.org/blogs/nhibernate/archive/2008/10/30/bulk-data-operations-with-nhibernate-s-stateless-sessions.aspx