So I was able to figure out how to do this. I don't know if its the best way, but I was able to reduce the loading time of some of my larger product sets from ~5 seconds and over 1000 queries to ~0.5 seconds and around 12 queries.
Here is how I did it for anyone that's wondering:
Entity Framework RelationshipsFirst I had to make Entity Framework aware of the relationships in the data model. In Nop.Core/Domain, I added the following to the Products class:
private ICollection<ProductGridAttributeValue> _productGridAttributeValue;
private ICollection<Product> _childGroupedProducts;
public virtual ICollection<ProductGridAttributeValue> ProductGridAttributeValues
{
get { return _productGridAttributeValue ?? (_productGridAttributeValue = new List<ProductGridAttributeValue>()); }
protected set { _productGridAttributeValue = value; }
}
public virtual Product ParentGroupedProduct
{
get;
protected set;
}
public virtual ICollection<Product> ChildGroupedProducts
{
get { return _childGroupedProducts ?? (_childGroupedProducts = new List<Product>()); }
protected set { _childGroupedProducts = value; }
}
Note: I could have and probably should have called ChildGroupedProducts AssociatedProducts.
Note 2: I had made two classes and associated database tables called ProductGridAttribute and ProductGridAttributeValue to store the extra data I needed.
Then I added the mappings in Nop.Data/Mapping:
In
ProductMap.cs:
this.HasRequired(p => p.ParentGroupedProduct).WithMany(p => p.ChildGroupedProducts);
In
ProductGridAttributeValueMap:
this.HasRequired(pgav => pgav.Product).WithMany(p => p.ProductGridAttributeValues);
Custom ProductServiceI implemented a new method in Nop.Service's
ProductService.cs to replace ProductService.GetProductById():
private const string DETAILED_PRODUCTS_BY_ID_KEY = "Nop.product.detailed.id-{0}";
public Product GetDetailedProductById(int productId)
{
if (productId == 0)
return null;
string key = string.Format(DETAILED_PRODUCTS_BY_ID_KEY, productId);
return _cacheManager.Get(key, () => _productRepository.TableNoTracking
.Where(p => p.Id == productId)
.IncludeProperties(p => p.ChildGroupedProducts
.Select(c => c.ProductGridAttributeValues
.Select(v => v.ProductGridAttribute)),
p => p.ChildGroupedProducts
.Select(c => c.ProductAttributeMappings
.Select(m => m.ProductAttributeValues)),
p => p.ChildGroupedProducts
.Select(c => c.ProductAttributeMappings
.Select(m => m.ProductAttribute))
)
).FirstOrDefault();
}
Note: You'll also have to define this method in IProductService
Custom ProductController.PrepareProductDetailsPageModel()I wrote my own custom PrepareProductDetailsPageModel method to build the ProductDetailsModel object. A lot of it was a direct copy from the original method with a bit of tweaks. For the site I'm building, we don't need all the features build into NopCommerce so some sections I left out and others have hard-coded values.
My goal here was to build the ProductDetailsModel object with all the data I needed in my view without trying to access any data that wasn't already eagerly loaded (therefore avoiding unneeded hits to the DB).
Some of the changes I made:
// Only the parent product has an SeName defined, so I moved this out of
// the new ProductDetailsModel statement so we don't do it for each child product.
if (!isAssociatedProduct)
{
model.SeName = product.GetSeName();
}
// Changed this bit at the beginning of the Attributes section so that we
// used the data already eagerly loaded instead of querying the database for
// each child product.
//
// Original looked like:
// productAttributeMapping = _productAttributeService.GetProductAttributeMappingsByProductId(product.Id);
ICollection<ProductAttributeMapping> productAttributeMapping = null;
productAttributeMapping = product.ProductAttributeMappings;
// When loading AttributeValues for each AttributeMapping, use the eagerly loaded
// data instead of querying for each mapping for each child product.
//
// Original looked like:
// var attributeValues = _productAttributeService.GetProductAttributeValues(attribute.Id);
var attributeValues = attribute.ProductAttributeValues;
// Get associated products from eagerly loaded data instead of querying the database
//
// Original looked like:
// var associatedProducts = _productService.GetAssociatedProducts(product.Id, _storeContext.CurrentStore.Id);
// Note: Eager Loading query doesn't sort associated products properly, so sort the list in code here
var associatedProducts = product.ChildGroupedProducts.OrderBy(c => c.DisplayOrder);
If anyone has a better way, or more "Proper (tm)" way of doing this, I'd love to learn.