diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 74ecb23071..e6f1798f50 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -59,13 +59,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //fixme - we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently sql = sql.OrderBy("NodeId"); - var page = Database.Page(pageIndex + 1, pageSize, sql); var dtos = page.Items; var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray(); if (isContent) - BuildVariantInfo(entities); + BuildVariants(entities.Cast()); if (isMedia) BuildProperties(entities, dtos); @@ -86,12 +85,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var dtos = Database.FetchOneToMany( - ddto => ddto.VariationInfo, - ddto => ddto.VersionId, - sql); - - return dtos.Count == 0 ? null : BuildVariantInfo(BuildDocumentEntity(dtos[0]))[0]; + var cdtos = Database.Fetch(sql); + + return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0])); } var dto = Database.FirstOrDefault(sql); @@ -149,14 +145,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) { - var cdtos = Database.FetchOneToMany( - dto => dto.VariationInfo, - dto => dto.VersionId, - sql); + var cdtos = Database.Fetch(sql); return cdtos.Count == 0 ? Enumerable.Empty() - : BuildVariantInfo(cdtos.Select(BuildDocumentEntity).ToArray()).ToList(); + : BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList(); } var dtos = Database.Fetch(sql); @@ -280,54 +273,90 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.AdditionalData[pdto.PropertyTypeDto.Alias] = new EntitySlim.PropertySlim(pdto.PropertyTypeDto.DataTypeDto.EditorAlias, value); } - private void BuildVariantInfo(EntitySlim[] entities) - { - BuildVariantInfo((DocumentEntitySlim[])entities); - } + private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity) + => BuildVariants(new[] { entity }).First(); - private DocumentEntitySlim[] BuildVariantInfo(params DocumentEntitySlim[] entities) + private IEnumerable BuildVariants(IEnumerable entities) { - if (entities.Any(x => x.Variations.VariesByCulture())) + List v = null; + var entitiesList = entities.ToList(); + foreach (var e in entitiesList) { - //each EntitySlim at this stage is an DocumentEntitySlim - - var dtos = Database.FetchByGroups(entities.Select(x => x.Id), 2000, GetVariantPublishedInfo) - .GroupBy(x => x.NodeId) - .ToDictionary(x => x.Key, x => (IEnumerable)x); - - foreach (var e in entities.OfType().Where(x => x.Variations.VariesByCulture()).OrderBy(x => x.Id)) - { - //fixme: how do i get this info? Seems that requires another query since that is how I think it's done in the DocumentRepository - //e.EditedCultures = - e.PublishedCultures = dtos[e.Id].Where(x => x.VersionPublished).Select(x => x.IsoCode).Distinct().ToList(); - } + if (e.Variations.VariesByCulture()) + (v ?? (v = new List())).Add(e); } - return entities; + if (v == null) return entitiesList; + + // fetch all variant info dtos + var dtos = Database.FetchByGroups(v.Select(x => x.Id), 2000, GetVariantInfos); + + // group by node id (each group contains all languages) + var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x); + + foreach (var e in v) + { + // since we're only iterating on entities that vary, we must have something + var edtos = xdtos[e.Id]; + + e.CultureNames = edtos.Where(x => x.CultureAvailable).ToDictionary(x => x.IsoCode, x => x.Name); + e.PublishedCultures = edtos.Where(x => x.CulturePublished).Select(x => x.IsoCode); + e.EditedCultures = edtos.Where(x => x.CultureAvailable && x.CultureEdited).Select(x => x.IsoCode); + } + + return entitiesList; } #endregion #region Sql - private Sql GetVariantPublishedInfo(IEnumerable ids) + protected Sql GetVariantInfos(IEnumerable ids) { - var sql = Sql(); - sql - .Select(x => x.NodeId, x => Alias(x.Id, "versionId"), x => Alias(x.Current, "versionCurrent")) - .AndSelect(x => Alias(x.Id, "versionCultureId")) + // this ... is an interesting query - could we make it simpler? probably, by having DocumentCultureVariationDto + // handle 'available' and 'published' in addition to 'edited' - would take (a bit) more time to save a document, + // but would make querying way faster + + return Sql() + .Select(x => x.NodeId) .AndSelect(x => x.IsoCode) - .AndSelect(x => Alias(x.Published, "versionPublished")) + .AndSelect("doc", x => Alias(x.Published, "DocumentPublished"), x => Alias(x.Edited, "DocumentEdited")) + .AndSelect("ccv", x => Alias(x.Id, "CultureAvailableData"), x => Alias(x.Name, "Name")) + .AndSelect("pcv", x => Alias(x.Id, "CulturePublishedData")) + .AndSelect("dcv", x => Alias(x.Edited, "CultureEditedData")) + + // from node x language .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .InnerJoin().On(x => x.Id, x => x.VersionId) - .InnerJoin().On(x => x.Id, x => x.Id) - .InnerJoin().On(x => x.LanguageId, x => x.Id) - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document) - .WhereIn(x => x.NodeId, ids) - .Where($"{SqlSyntax.GetFieldName(x => x.Current)} = 1 OR {SqlSyntax.GetFieldName(x => x.Published)} = 1"); - return sql; + .CrossJoin() + + // join to document - always exists - indicates global document published/edited status + .InnerJoin("doc") + .On((node, doc) => node.NodeId == doc.NodeId, aliasRight: "doc") + + // left-join do document variation - matches cultures that are *available* + indicates when *edited* + .LeftJoin("dcv") + .On((node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv") + + // join to current version - always exists + // left-join to current version variations - indicates which cultures are *available* + .InnerJoin("cv") + .On((node, ev) => node.NodeId == ev.NodeId && ev.Current, aliasRight: "cv") + .LeftJoin("ccv") + .On((cv, ccv, lang) => cv.Id == ccv.VersionId && lang.Id == ccv.LanguageId, "cv", "ccv") + + // left-join to published version - exists when whole node is published + // left-join to published version variations - matches cultures that are *published* + .LeftJoin(nested => nested.InnerJoin("dv") + .On((pv, dv) => pv.Id == dv.Id && dv.Published, "pv", "dv"), + "pv") + .On((node, pv) => node.NodeId == pv.NodeId, aliasRight: "pv") + .LeftJoin("pcv") + .On((pv, pcv, lang) => pv.Id == pcv.VersionId && lang.Id == pcv.LanguageId, "pv", "pcv") + + // for selected nodes + .WhereIn(x => x.NodeId, ids); } + // gets the full sql for a given object type and a given unique id protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Guid uniqueId) { @@ -368,9 +397,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .InnerJoin().On((left, right) => left.DataTypeId == right.NodeId) .WhereIn(x => x.VersionId, versionIds) .OrderBy(x => x.VersionId); - } - - + } // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version @@ -397,12 +424,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent) { sql - .AndSelect(x => x.Published, x => x.Edited) - //This MUST come last in the select statements since we will end up with a 1:M query - .AndSelect( - x => Alias(x.Id, "versionCultureId"), - x => Alias(x.LanguageId, "versionCultureLangId"), - x => Alias(x.Name, "versionCultureName")); + .AndSelect(x => x.Published, x => x.Edited); } } @@ -428,10 +450,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { sql .LeftJoin("child").On((left, right) => left.NodeId == right.ParentId, aliasRight: "child"); - - if (isContent) - sql - .LeftJoin().On((left, right) => left.Id == right.VersionId); } @@ -493,8 +511,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isContent) { sql - .AndBy(x => x.Published, x => x.Edited) - .AndBy(x => x.Id, x => x.LanguageId, x => x.Name); + .AndBy(x => x.Published, x => x.Edited); } @@ -537,34 +554,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { public ContentVariation Variations { get; set; } - [ResultColumn, Reference(ReferenceType.Many)] - public List VariationInfo { get; set; } - public bool Published { get; set; } public bool Edited { get; set; } } - private class VariationPublishInfoDto + public class VariantInfoDto { public int NodeId { get; set; } - public int VersionId { get; set; } - public bool VersionCurrent { get; set; } public string IsoCode { get; set; } - public int VersionCultureId { get; set; } - public bool VersionPublished { get; set; } - } - - /// - /// The DTO used in the 1:M result for content variation info - /// - private class ContentEntityVariationInfoDto - { - [Column("versionCultureId")] - public int VersionCultureId { get; set; } - [Column("versionCultureLangId")] - public int LanguageId { get; set; } - [Column("versionCultureName")] public string Name { get; set; } + public bool DocumentPublished { get; set; } + public bool DocumentEdited { get; set; } + + public int? CultureAvailableData { get; set; } + public int? CulturePublishedData { get; set; } + public bool? CultureEditedData { get; set; } + + [ResultColumn] + public bool CultureAvailable => CultureAvailableData.HasValue; + [ResultColumn] + public bool CulturePublished => CulturePublishedData.HasValue; + [ResultColumn] + public bool CultureEdited => CultureEditedData ?? false; } // ReSharper disable once ClassNeverInstantiated.Local @@ -648,53 +659,18 @@ namespace Umbraco.Core.Persistence.Repositories.Implement private DocumentEntitySlim BuildDocumentEntity(BaseDto dto) { + // EntitySlim does not track changes + var entity = new DocumentEntitySlim(); + BuildContentEntity(entity, dto); + if (dto is ContentEntityDto contentDto) { - return BuildDocumentEntity(contentDto); + // fill in the invariant info + entity.Edited = contentDto.Edited; + entity.Published = contentDto.Published; + entity.Variations = contentDto.Variations; } - // EntitySlim does not track changes - var entity = new DocumentEntitySlim(); - BuildContentEntity(entity, dto); - return entity; - } - - /// - /// Builds the from a and ensures the AdditionalData is populated with variant info - /// - /// - /// - private DocumentEntitySlim BuildDocumentEntity(ContentEntityDto dto) - { - // EntitySlim does not track changes - var entity = new DocumentEntitySlim(); - BuildContentEntity(entity, dto); - - //fill in the invariant info - entity.Edited = dto.Edited; - entity.Published = dto.Published; - - var publishedCultures = new List(); - var editedCultures = new List(); - - //fill in the variant info - if (dto.Variations.VariesByCulture() && dto.VariationInfo != null && dto.VariationInfo.Count > 0) - { - //fixme: Currently require making a 2nd query to fill in publish status information for variants - - var variantInfo = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - foreach (var info in dto.VariationInfo) - { - var isoCode = _langRepository.GetIsoCodeById(info.LanguageId); - if (isoCode != null) - variantInfo[isoCode] = info.Name; - - } - entity.CultureNames = variantInfo; - entity.Variations = dto.Variations; - entity.PublishedCultures = publishedCultures; - entity.EditedCultures = editedCultures; - } return entity; }