From ba913db60eb42cd7f7aaa464def903e63ba53b0d Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 22 Oct 2018 17:19:14 +0200 Subject: [PATCH] Fix to/from (in)variant changes --- .../Implement/ContentTypeRepositoryBase.cs | 294 ++++++++++-------- .../NuCache/PublishedSnapshotService.cs | 16 +- 2 files changed, 171 insertions(+), 139 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 3f1ea3116e..adf02a52f3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -270,8 +270,8 @@ AND umbracoNode.id <> @id", // 1. Find content based on the current ContentType: entity.Id // 2. Find all PropertyTypes on the ContentType that was removed - tracked id (key) // 3. Remove properties based on property types from the removed content type where the content ids correspond to those found in step one - var compositionBase = entity as ContentTypeCompositionBase; - if (compositionBase != null && compositionBase.RemovedContentTypeKeyTracker != null && + if (entity is ContentTypeCompositionBase compositionBase && + compositionBase.RemovedContentTypeKeyTracker != null && compositionBase.RemovedContentTypeKeyTracker.Any()) { //TODO: Could we do the below with bulk SQL statements instead of looking everything up and then manipulating? @@ -406,40 +406,33 @@ AND umbracoNode.id <> @id", } //check if the content type variation has been changed - var ctVariationChanging = entity.IsPropertyDirty("Variations"); - if (ctVariationChanging) + var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); + var oldContentTypeVariation = (ContentVariation) dtoPk.Variations; + var newContentTypeVariation = entity.Variations; + var contentTypeVariationChanging = contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; + if (contentTypeVariationChanging) { - //we've already looked up the previous version of the content type so we know it's previous variation state - MoveVariantData(entity, (ContentVariation)dtoPk.Variations, entity.Variations); + MoveContentTypeVariantData(entity, oldContentTypeVariation, newContentTypeVariation); Clear301Redirects(entity); ClearScheduledPublishing(entity); - } + } - //track any content type/property types that are changing variation which will require content updates - var propertyTypeVariationChanges = new Dictionary(); + // collect property types that have a dirty variation + List propertyTypeVariationDirty = null; - // insert or update properties - // all of them, no-group and in-groups foreach (var propertyType in entity.PropertyTypes) { - //if the content type variation isn't changing track if any property type is changing - if (!ctVariationChanging) + if (contentTypeVariationChanging) { - if (propertyType.IsPropertyDirty("Variations")) + // content type is changing + switch (newContentTypeVariation) { - propertyTypeVariationChanges[propertyType.Id] = propertyType.Variations; - } - } - else - { - switch(entity.Variations) - { - case ContentVariation.Nothing: - //if the content type is changing to Nothing, then all property type's must change to nothing + case ContentVariation.Nothing: // changing to Nothing + // all property types must change to Nothing propertyType.Variations = ContentVariation.Nothing; break; - case ContentVariation.Culture: - //we don't need to modify the property type in this case + case ContentVariation.Culture: // changing to Culture + // all property types can remain Nothing break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -448,15 +441,36 @@ AND umbracoNode.id <> @id", } } - var groupId = propertyType.PropertyGroupId?.Value ?? default(int); + // then, track each property individually + if (propertyType.IsPropertyDirty("Variations")) + { + // allocate the list only when needed + if (propertyTypeVariationDirty == null) + propertyTypeVariationDirty = new List(); + + propertyTypeVariationDirty.Add(propertyType); + } + } + + // figure out dirty property types that have actually changed + // before we insert or update properties, so we can read the old variations + var propertyTypeVariationChanges = propertyTypeVariationDirty != null + ? GetPropertyVariationChanges(propertyTypeVariationDirty) + : null; + + // insert or update properties + // all of them, no-group and in-groups + foreach (var propertyType in entity.PropertyTypes) + { // if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias - if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default(int)) + if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default) AssignDataTypeFromPropertyEditor(propertyType); // validate the alias ValidateAlias(propertyType); // insert or update property + var groupId = propertyType.PropertyGroupId?.Value ?? default; var propertyTypeDto = PropertyGroupFactory.BuildPropertyTypeDto(groupId, propertyType, entity.Id); var typeId = propertyType.HasIdentity ? Database.Update(propertyTypeDto) @@ -467,31 +481,12 @@ AND umbracoNode.id <> @id", typeId = propertyType.Id; // not an orphan anymore - if (orphanPropertyTypeIds != null) - orphanPropertyTypeIds.Remove(typeId); - } - - //check if any property types were changing variation - if (propertyTypeVariationChanges.Count > 0) - { - var changes = new Dictionary(); - - //now get the current property type variations for the changed ones so that we know which variation they - //are going from and to - var from = Database.Dictionary(Sql() - .Select(x => x.Id, x => x.Variations) - .From() - .WhereIn(x => x.Id, propertyTypeVariationChanges.Keys)); - - foreach (var f in from) - { - changes[f.Key] = (propertyTypeVariationChanges[f.Key], (ContentVariation)f.Value); - } - - //perform the move - MoveVariantData(changes); + orphanPropertyTypeIds?.Remove(typeId); } + // if some property types have actually changed, move their variant data + if (propertyTypeVariationChanges != null) + MovePropertyTypeVariantData(propertyTypeVariationChanges); // deal with orphan properties: those that were in a deleted tab, // and have not been re-mapped to another tab or to 'generic properties' @@ -500,6 +495,45 @@ AND umbracoNode.id <> @id", DeletePropertyType(entity.Id, id); } + // gets property types that have actually changed, and the corresponding changes + // returns null if no property type has actually changed + private Dictionary GetPropertyVariationChanges(IEnumerable propertyTypes) + { + var propertyTypesL = propertyTypes.ToList(); + + // select the current variations (before the change) from database + var selectCurrentVariations = Sql() + .Select(x => x.Id, x => x.Variations) + .From() + .WhereIn(x => x.Id, propertyTypesL.Select(x => x.Id)); + + var oldVariations = Database.Dictionary(selectCurrentVariations); + + // build a dictionary of actual changes + Dictionary changes = null; + + foreach (var propertyType in propertyTypesL) + { + // new property type, ignore + if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) + continue; + var oldVariation = (ContentVariation) oldVariationB; // NPoco cannot fetch directly + + // only those property types that *actually* changed + var newVariation = propertyType.Variations; + if (oldVariation == newVariation) + continue; + + // allocate the dictionary only when needed + if (changes == null) + changes = new Dictionary(); + + changes[propertyType.Id] = (oldVariation, newVariation); + } + + return changes; + } + /// /// Clear any redirects associated with content for a content type /// @@ -526,28 +560,38 @@ AND umbracoNode.id <> @id", } /// - /// Moves variant data for property type changes + /// Gets the default language identifier. /// - /// - private void MoveVariantData(IDictionary propertyTypeChanges) + private int GetDefaultLanguageId() { - var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault)); + var selectDefaultLanguageId = Sql() + .Select(x => x.Id) + .From() + .Where(x => x.IsDefault); + + return Database.First(selectDefaultLanguageId); + } + + /// + /// Moves variant data for property type variation changes. + /// + private void MovePropertyTypeVariantData(IDictionary propertyTypeChanges) + { + var defaultLanguageId = GetDefaultLanguageId(); //Group by the "To" variation so we can bulk update in the correct batches - foreach(var g in propertyTypeChanges.GroupBy(x => x.Value.Item2)) + foreach(var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation)) { - var propertyTypeIds = g.Select(s => s.Key).ToList(); + var propertyTypeIds = grouping.Select(x => x.Key).ToList(); + var toVariation = grouping.Key; - //the ContentVariation that the data is moving "To" - var toVariantType = g.Key; - - switch(toVariantType) + switch (toVariation) { case ContentVariation.Culture: - MovePropertyDataToVariantCulture(defaultLangId, propertyTypeIds: propertyTypeIds); + CopyPropertyData(null, defaultLanguageId, propertyTypeIds); break; case ContentVariation.Nothing: - MovePropertyDataToVariantNothing(defaultLangId, propertyTypeIds: propertyTypeIds); + CopyPropertyData(defaultLanguageId, null, propertyTypeIds); break; case ContentVariation.CultureAndSegment: case ContentVariation.Segment: @@ -558,24 +602,17 @@ AND umbracoNode.id <> @id", } /// - /// Moves variant data for a content type variation change + /// Moves variant data for a content type variation change. /// - /// - /// - /// - private void MoveVariantData(IContentTypeComposition contentType, ContentVariation from, ContentVariation to) + private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, ContentVariation toVariation) { - var defaultLangId = Database.First(Sql().Select(x => x.Id).From().Where(x => x.IsDefault)); + var defaultLanguageId = GetDefaultLanguageId(); - var sqlPropertyTypeIds = Sql().Select(x => x.Id).From().Where(x => x.ContentTypeId == contentType.Id); - switch (to) + switch (toVariation) { case ContentVariation.Culture: - //move the property data - MovePropertyDataToVariantCulture(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds); - - //now we need to move the names + //move the names //first clear out any existing names that might already exists under the default lang //there's 2x tables to update @@ -585,10 +622,11 @@ AND umbracoNode.id <> @id", .InnerJoin().On(x => x.Id, x => x.VersionId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLangId); + .Where(x => x.LanguageId == defaultLanguageId); var sqlDelete = Sql() .Delete() .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); //clear out the documentCultureVariation table @@ -596,10 +634,11 @@ AND umbracoNode.id <> @id", .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLangId); + .Where(x => x.LanguageId == defaultLanguageId); sqlDelete = Sql() .Delete() .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); //now we need to insert names into these 2 tables based on the invariant data @@ -607,32 +646,31 @@ AND umbracoNode.id <> @id", //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang var cols = Sql().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) - .Append($", {defaultLangId}") //default language ID + .Append($", {defaultLanguageId}") //default language ID .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id); var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); //insert rows into the documentCultureVariation table cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) .AndSelect(x => x.Text) - .Append($", 1, {defaultLangId}") //make Available + default language ID + .Append($", 1, {defaultLanguageId}") //make Available + default language ID .From() .InnerJoin().On(x => x.NodeId, x => x.NodeId) .InnerJoin().On(x => x.NodeId, x => x.NodeId) .Where(x => x.ContentTypeId == contentType.Id); sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); break; case ContentVariation.Nothing: - //move the property data - MovePropertyDataToVariantNothing(defaultLangId, sqlPropertyTypeIds: sqlPropertyTypeIds); - - //we dont need to move the names! this is because we always keep the invariant names with the name of the default language. + //we don't need to move the names! this is because we always keep the invariant names with the name of the default language. //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :( // if we want these SQL statements back, look into GIT history @@ -646,73 +684,59 @@ AND umbracoNode.id <> @id", } /// - /// This will move all property data from variant to invariant + /// Copies property data from one language to another. /// - /// - /// Optional list of property type ids of the properties to be updated - /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated - private void MovePropertyDataToVariantNothing(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null) + /// The source language (can be null ie invariant). + /// The target language (can be null ie invariant) + /// The property type identifiers. + private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection propertyTypeIds) { - //first clear out any existing property data that might already exists under the default lang + //first clear out any existing property data that might already exists under the target language var sqlDelete = Sql() - .Delete() - .Where(x => x.LanguageId == null); - if (sqlPropertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + .Delete(); + + // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it + if (targetLanguageId == null) + sqlDelete.Where(x => x.LanguageId == null); + else + sqlDelete.Where(x => x.LanguageId == targetLanguageId); + + sqlDelete + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); Database.Execute(sqlDelete); - //now insert all property data into the default language that exists under the invariant lang + //now insert all property data into the target language that exists under the source language + var targetLanguageIdS = targetLanguageId.HasValue ? targetLanguageId.ToString() : "NULL"; var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append(", NULL") //null language ID - .From() - .Where(x => x.LanguageId == defaultLangId); - if (sqlPropertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + .Append(", " + targetLanguageIdS) //default language ID + .From(); + + // NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it + if (sourceLanguageId == null) + sqlSelectData.Where(x => x.LanguageId == null); + else + sqlSelectData.Where(x => x.LanguageId == sourceLanguageId); + + sqlSelectData + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); Database.Execute(sqlInsert); - } - /// - /// This will move all property data from invariant to variant - /// - /// - /// Optional list of property type ids of the properties to be updated - /// Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated - private void MovePropertyDataToVariantCulture(int defaultLangId, IReadOnlyCollection propertyTypeIds = null, Sql sqlPropertyTypeIds = null) - { - //first clear out any existing property data that might already exists under the default lang - var sqlDelete = Sql() - .Delete() - .Where(x => x.LanguageId == defaultLangId); - if (sqlPropertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlDelete.WhereIn(x => x.PropertyTypeId, propertyTypeIds); + // when copying from Culture, keep the original values around in case we want to go back + // when copying from Nothing, kill the original values, we don't want them around + if (sourceLanguageId == null) + { + sqlDelete = Sql() + .Delete() + .Where(x => x.LanguageId == null) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); - Database.Execute(sqlDelete); - - //now insert all property data into the default language that exists under the invariant lang - var cols = Sql().Columns(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue, x => x.LanguageId); - var sqlSelectData = Sql().Select(x => x.VersionId, x => x.PropertyTypeId, x => x.Segment, x => x.IntegerValue, x => x.DecimalValue, x => x.DateValue, x => x.VarcharValue, x => x.TextValue) - .Append($", {defaultLangId}") //default language ID - .From() - .Where(x => x.LanguageId == null); - if (sqlPropertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, sqlPropertyTypeIds); - if (propertyTypeIds != null) - sqlSelectData.WhereIn(x => x.PropertyTypeId, propertyTypeIds); - - var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData); - - Database.Execute(sqlInsert); + Database.Execute(sqlDelete); + } } private void DeletePropertyType(int contentTypeId, int propertyTypeId) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a9903669b9..55f4f07bef 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1160,6 +1160,10 @@ namespace Umbraco.Web.PublishedCache.NuCache var pdatas = new List(); foreach (var pvalue in prop.Values) { + // sanitize - properties should be ok but ... never knows + if (!prop.PropertyType.SupportsVariation(pvalue.Culture, pvalue.Segment)) + continue; + // note: at service level, invariant is 'null', but here invariant becomes 'string.Empty' var value = published ? pvalue.PublishedValue : pvalue.EditedValue; if (value != null) @@ -1191,15 +1195,19 @@ namespace Umbraco.Web.PublishedCache.NuCache var cultureData = new Dictionary(); - var names = content is IContent document + // sanitize - names should be ok but ... never knows + if (content.GetContentType().VariesByCulture()) + { + var names = content is IContent document ? (published ? document.PublishNames : document.CultureNames) : content.CultureNames; - foreach (var (culture, name) in names) - { - cultureData[culture] = new CultureVariation { Name = name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; + foreach (var (culture, name) in names) + { + cultureData[culture] = new CultureVariation { Name = name, Date = content.GetUpdateDate(culture) ?? DateTime.MinValue }; + } } //the dictionary that will be serialized