1
votes

Is it possible to eager load a field when querying content using the ContentManager?

I'm using the ContentManager to retrieve all content items of a specific content type. The content type has a MediaLibraryPickerField on it which is creating a select n+1 issue when I iterate over the results of the query. I'd like to force this data to be loaded upfront (join on initial query). This seems straightforward for a ContentPart but I can't get it to work for a ContentField. Is this possible or is there another way to avoid the select n+1 issue with fields?

Here's what I've tried but it has not effect:

var myQuery = _contentManager.Query(new[] { "MyContentType" })
        .WithQueryHints(new QueryHints().ExpandParts<MediaPart>());

I've also tried expanding the record:

var myQuery = _contentManager.Query(new[] { "MyContentType" })
        .WithQueryHints(new QueryHints().ExpandRecords<MediaPartRecord>());
1
The problem is not so much that it's a field (field data is stored on the content item record itself), but that the media content item linked by the field is not part of the original query.Bertrand Le Roy
@BertrandLeRoy - Makes sense. Any suggestions on how I should be going about it?joshb
Not really, but if you can gather the ids for the media items up front, then you can make a GetMany ans stitch things back together. I haven't really thought this through however, sorry.Bertrand Le Roy
I had to do this on my own site to speed up my home page. I added an answer to explain how I did it.Bertrand Le Roy

1 Answers

3
votes

Here's how I fixed the problem for a projection page, but the same method, or something simpler, could be applied in your case.

In an alternate template for the Content shape of the projection page, Content-ProjectionPage.cshtml, I did the following, which creates a lookup for media that items will be able to use later:

// Pre-fetch images
var projectionItems =  ((IEnumerable<dynamic>)
    ((IEnumerable<dynamic>)Model.Content.Items)
    .First(i => i.Metadata.Type == "List").Items)
    .Select(s => (ContentItem)s.ContentItem);
var mediaLibraryFields = projectionItems
    .SelectMany(i => i.Parts.SelectMany(p => p.Fields.Where(f => f is MediaLibraryPickerField)))
    .Cast<MediaLibraryPickerField>();
var firstMediaIds = mediaLibraryFields
    .Select(f => f.Ids.FirstOrDefault())
    .Where(id => id != default(int))
    .Distinct()
    .ToArray();
var firstMedia = WorkContext.Resolve<IContentManager>()
    .GetMany<MediaPart>(firstMediaIds, VersionOptions.Published, QueryHints.Empty);
var mediaCache = Layout.MediaCache == null 
    ? Layout.MediaCache = new Dictionary<int, MediaPart>() 
    : (Dictionary<int, MediaPart>) Layout.MediaCache;
foreach (var media in firstMedia) {
    mediaCache.Add(media.Id, media);
}

In your case, you don't have to do the complicated drilling into shapes to dig out the fields, as you have access to them directly. I had to do that because the view or a shape table provider is unfortunately the easiest place for me to do that.

Then, when I want to display an image, all I have to do is access my lookup and try to get it from there. In my alternate template MediaLibraryPicker.Summary.cshtml, I do this:

var field = (MediaLibraryPickerField)Model.ContentField;
var imageIds = field.Ids;
if (imageIds.Any()) {
    var cm = Model.ContentPart.ContentItem.ContentManager as IContentManager;
    var title = cm == null || Model.ContentPart == null
        ? "" : cm.GetItemMetadata(Model.ContentPart).DisplayText;

    var mediaCache = Layout.MediaCache as Dictionary<int, MediaPart>;
    var firstImage = mediaCache != null
        ? mediaCache[imageIds.First()]
        : cm.Get(imageIds.First()).As<MediaPart>();
    <div class="gallery">
        <a href="@Url.ItemDisplayUrl((IContent)Model.ContentPart)"><img src="@Display.ResizeMediaUrl(Path: firstImage.MediaUrl, Width: 132)" class="main" alt="@title"/></a>
    </div>
}

I'm only displaying the first image in the field, here, but you could change that where it does f.Ids.FirstOrDefault(). Just do f.Ids instead and replace the Select with a SelectMany. Also change the summary template so it displays all images after looking them up in the same dictionary.

Once I did that, I had no select N+1, and instead got a single SQL query for all the images on the page.