nHibernate3; retrieving 4xxx records out of an EAV data schema. When nHibernate, or .NET, goes to initialize those collections for the first time, we're seeing a severe penalty. Subsequent calls appear to perform more efficiently. Running the same queries in SQL Server Management Studio result in expected quick return times.
Using Fluent and runtime mapping instead of .hbm.xml; curious if serialized mapping would help here?
nHibernate Profiler and log4net logging didn't seem to give me much to go on. A total of something like 140,000 entities are hydrated in this process.
Attached a screenshot of my dotTrace performance tracing that shows the collection initialization penalty:
Have tried join and eager fetchtypes, with no apparent results, but am not 100% certain I implemented those correctly -- does just the parent need to be so designated, or do the children tables also need to be flagged?
var products = ((HandleSession)_handleSession).Session.CreateCriteria(typeof(Product))
.SetFetchMode("Product", FetchMode.Eager)
.List<Product>()
.AsEnumerable();
With reflection optimizer enabled (I think) via web.config:
This is where most time is spent:
return new ProductList(products.Select(p => p.ToProductContract()));
Which is simply an extension method doing this:
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);
}
}
Unfortunately with .Future I still appear to get similar results. Here's a new trace; I've switched to Release, and x64 for the key projects, for the moment, so the times are lower, but the proportions are still pretty much the same; as well as with .Eager:
var products = ((HandleSession) _handleSession).Session.CreateCriteria(typeof (Product))
.SetFetchMode("ProductAttribute", FetchMode.Join)
.SetFetchMode("ProductGroup", FetchMode.Join)
.SetFetchMode("ProductType", FetchMode.Join)
.Future<Product>()
.AsEnumerable();
Generated SQL with .Eager and .Future in place:
SELECT this_.ProductID as ProductID0_1_, this_.ProductNumber as ProductN2_0_1_, this_.ProductName as ProductN3_0_1_, this_.InsertedDateTime as Inserted4_0_1_, this_.UpdatedDateTime as UpdatedD5_0_1_, this_.ProductGUID as ProductG6_0_1_, this_.ProductTypeId as ProductT7_0_1_, producttyp2_.ProductTypeID as ProductT1_6_0_, producttyp2_.ProductTypeName as ProductT2_6_0_ FROM Product this_ inner join ProductType producttyp2_ on this_.ProductTypeId=producttyp2_.ProductTypeID;
SELECT productatt0_.ProductId as ProductId2_, productatt0_.ProductAttributeID as ProductA1_2_, productatt0_.ProductAttributeID as ProductA1_2_1_, productatt0_.PositionNumber as Position2_2_1_, productatt0_.ValueText as ValueText2_1_, productatt0_.ValueBinary as ValueBin4_2_1_, productatt0_.ProductID as ProductID2_1_, productatt0_.AttributeID as Attribut6_2_1_, productatt0_.ExpirationDateTime as Expirati7_2_1_, productatt0_.IsCurrentIndicator as IsCurren8_2_1_, productatt0_.OperationCode as Operatio9_2_1_, productatt0_.OperationDateTime as Operati10_2_1_, productatt0_.OperationSystemName as Operati11_2_1_, productatt0_.OperationUserName as Operati12_2_1_, productatt0_.LastUserPriority as LastUse13_2_1_, attribute1_.AttributeId as Attribut1_1_0_, attribute1_.AttributeName as Attribut2_1_0_, attribute1_.DisplayName as DisplayN3_1_0_, attribute1_.DataTypeName as DataType4_1_0_, attribute1_.ConstraintText as Constrai5_1_0_, attribute1_.ConstraintMin as Constrai6_1_0_, attribute1_.ConstraintMax as Constrai7_1_0_, attribute1_.ValuesMin as ValuesMin1_0_, attribute1_.ValuesMax as ValuesMax1_0_, attribute1_.Precision as Precision1_0_ FROM ProductAttribute productatt0_ inner join Attribute attribute1_ on productatt0_.AttributeID=attribute1_.AttributeId WHERE (productatt0_.IsCurrentIndicator=1) and productatt0_.ProductId in (select this_.ProductID FROM Product this_ inner join ProductType producttyp2_ on this_.ProductTypeId=producttyp2_.ProductTypeID)
SELECT productgro0_.ProductId as ProductId1_, productgro0_.ProductGroupId as ProductG1_1_, productgro0_.ProductGroupId as ProductG1_3_0_, productgro0_.ProductId as ProductId3_0_, productgro0_.GroupId as GroupId3_0_ FROM ProductGroup productgro0_ WHERE (productgro0_.IsCurrentIndicator=1) and productgro0_.ProductId in (select this_.ProductID FROM Product this_ inner join ProductType producttyp2_ on this_.ProductTypeId=producttyp2_.ProductTypeID)