Fix to/from (in)variant changes

This commit is contained in:
Stephan
2018-10-22 17:19:14 +02:00
parent 3468351de8
commit ba913db60e
2 changed files with 171 additions and 139 deletions

View File

@@ -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<int, ContentVariation>();
// collect property types that have a dirty variation
List<PropertyType> 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<PropertyType>();
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<int, (ContentVariation, ContentVariation)>();
//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<int, byte>(Sql()
.Select<PropertyTypeDto>(x => x.Id, x => x.Variations)
.From<PropertyTypeDto>()
.WhereIn<PropertyTypeDto>(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<int, (ContentVariation FromVariation, ContentVariation ToVariation)> GetPropertyVariationChanges(IEnumerable<PropertyType> propertyTypes)
{
var propertyTypesL = propertyTypes.ToList();
// select the current variations (before the change) from database
var selectCurrentVariations = Sql()
.Select<PropertyTypeDto>(x => x.Id, x => x.Variations)
.From<PropertyTypeDto>()
.WhereIn<PropertyTypeDto>(x => x.Id, propertyTypesL.Select(x => x.Id));
var oldVariations = Database.Dictionary<int, byte>(selectCurrentVariations);
// build a dictionary of actual changes
Dictionary<int, (ContentVariation, ContentVariation)> 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<int, (ContentVariation, ContentVariation)>();
changes[propertyType.Id] = (oldVariation, newVariation);
}
return changes;
}
/// <summary>
/// Clear any redirects associated with content for a content type
/// </summary>
@@ -526,28 +560,38 @@ AND umbracoNode.id <> @id",
}
/// <summary>
/// Moves variant data for property type changes
/// Gets the default language identifier.
/// </summary>
/// <param name="propertyTypeChanges"></param>
private void MoveVariantData(IDictionary<int, (ContentVariation, ContentVariation)> propertyTypeChanges)
private int GetDefaultLanguageId()
{
var defaultLangId = Database.First<int>(Sql().Select<LanguageDto>(x => x.Id).From<LanguageDto>().Where<LanguageDto>(x => x.IsDefault));
var selectDefaultLanguageId = Sql()
.Select<LanguageDto>(x => x.Id)
.From<LanguageDto>()
.Where<LanguageDto>(x => x.IsDefault);
return Database.First<int>(selectDefaultLanguageId);
}
/// <summary>
/// Moves variant data for property type variation changes.
/// </summary>
private void MovePropertyTypeVariantData(IDictionary<int, (ContentVariation FromVariation, ContentVariation ToVariation)> 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",
}
/// <summary>
/// Moves variant data for a content type variation change
/// Moves variant data for a content type variation change.
/// </summary>
/// <param name="contentType"></param>
/// <param name="from"></param>
/// <param name="to"></param>
private void MoveVariantData(IContentTypeComposition contentType, ContentVariation from, ContentVariation to)
private void MoveContentTypeVariantData(IContentTypeComposition contentType, ContentVariation fromVariation, ContentVariation toVariation)
{
var defaultLangId = Database.First<int>(Sql().Select<LanguageDto>(x => x.Id).From<LanguageDto>().Where<LanguageDto>(x => x.IsDefault));
var defaultLanguageId = GetDefaultLanguageId();
var sqlPropertyTypeIds = Sql().Select<PropertyTypeDto>(x => x.Id).From<PropertyTypeDto>().Where<PropertyTypeDto>(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<ContentVersionDto>().On<ContentVersionDto, ContentVersionCultureVariationDto>(x => x.Id, x => x.VersionId)
.InnerJoin<ContentDto>().On<ContentDto, ContentVersionDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(x => x.ContentTypeId == contentType.Id)
.Where<ContentVersionCultureVariationDto>(x => x.LanguageId == defaultLangId);
.Where<ContentVersionCultureVariationDto>(x => x.LanguageId == defaultLanguageId);
var sqlDelete = Sql()
.Delete<ContentVersionCultureVariationDto>()
.WhereIn<ContentVersionCultureVariationDto>(x => x.Id, sqlSelect);
Database.Execute(sqlDelete);
//clear out the documentCultureVariation table
@@ -596,10 +634,11 @@ AND umbracoNode.id <> @id",
.From<DocumentCultureVariationDto>()
.InnerJoin<ContentDto>().On<ContentDto, DocumentCultureVariationDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(x => x.ContentTypeId == contentType.Id)
.Where<DocumentCultureVariationDto>(x => x.LanguageId == defaultLangId);
.Where<DocumentCultureVariationDto>(x => x.LanguageId == defaultLanguageId);
sqlDelete = Sql()
.Delete<DocumentCultureVariationDto>()
.WhereIn<DocumentCultureVariationDto>(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<ContentVersionCultureVariationDto>(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId);
sqlSelect = Sql().Select<ContentVersionDto>(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate)
.Append($", {defaultLangId}") //default language ID
.Append($", {defaultLanguageId}") //default language ID
.From<ContentVersionDto>()
.InnerJoin<ContentDto>().On<ContentDto, ContentVersionDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(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<DocumentCultureVariationDto>(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId);
sqlSelect = Sql().Select<DocumentDto>(x => x.NodeId, x => x.Edited, x => x.Published)
.AndSelect<NodeDto>(x => x.Text)
.Append($", 1, {defaultLangId}") //make Available + default language ID
.Append($", 1, {defaultLanguageId}") //make Available + default language ID
.From<DocumentDto>()
.InnerJoin<NodeDto>().On<NodeDto, DocumentDto>(x => x.NodeId, x => x.NodeId)
.InnerJoin<ContentDto>().On<ContentDto, NodeDto>(x => x.NodeId, x => x.NodeId)
.Where<ContentDto>(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",
}
/// <summary>
/// This will move all property data from variant to invariant
/// Copies property data from one language to another.
/// </summary>
/// <param name="defaultLangId"></param>
/// <param name="propertyTypeIds">Optional list of property type ids of the properties to be updated</param>
/// <param name="sqlPropertyTypeIds">Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated</param>
private void MovePropertyDataToVariantNothing(int defaultLangId, IReadOnlyCollection<int> propertyTypeIds = null, Sql<ISqlContext> sqlPropertyTypeIds = null)
/// <param name="sourceLanguageId">The source language (can be null ie invariant).</param>
/// <param name="targetLanguageId">The target language (can be null ie invariant)</param>
/// <param name="propertyTypeIds">The property type identifiers.</param>
private void CopyPropertyData(int? sourceLanguageId, int? targetLanguageId, IReadOnlyCollection<int> 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<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == null);
if (sqlPropertyTypeIds != null)
sqlDelete.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, sqlPropertyTypeIds);
if (propertyTypeIds != null)
sqlDelete.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, propertyTypeIds);
.Delete<PropertyDataDto>();
// NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it
if (targetLanguageId == null)
sqlDelete.Where<PropertyDataDto>(x => x.LanguageId == null);
else
sqlDelete.Where<PropertyDataDto>(x => x.LanguageId == targetLanguageId);
sqlDelete
.WhereIn<PropertyDataDto>(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<PropertyDataDto>(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<PropertyDataDto>(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<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == defaultLangId);
if (sqlPropertyTypeIds != null)
sqlSelectData.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, sqlPropertyTypeIds);
if (propertyTypeIds != null)
sqlSelectData.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, propertyTypeIds);
.Append(", " + targetLanguageIdS) //default language ID
.From<PropertyDataDto>();
// NPoco cannot turn the clause into IS NULL with a nullable parameter - deal with it
if (sourceLanguageId == null)
sqlSelectData.Where<PropertyDataDto>(x => x.LanguageId == null);
else
sqlSelectData.Where<PropertyDataDto>(x => x.LanguageId == sourceLanguageId);
sqlSelectData
.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, propertyTypeIds);
var sqlInsert = Sql($"INSERT INTO {PropertyDataDto.TableName} ({cols})").Append(sqlSelectData);
Database.Execute(sqlInsert);
}
/// <summary>
/// This will move all property data from invariant to variant
/// </summary>
/// <param name="defaultLangId"></param>
/// <param name="propertyTypeIds">Optional list of property type ids of the properties to be updated</param>
/// <param name="sqlPropertyTypeIds">Optional SQL statement used for the sub-query to select the properties type ids for the properties to be updated</param>
private void MovePropertyDataToVariantCulture(int defaultLangId, IReadOnlyCollection<int> propertyTypeIds = null, Sql<ISqlContext> sqlPropertyTypeIds = null)
{
//first clear out any existing property data that might already exists under the default lang
var sqlDelete = Sql()
.Delete<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == defaultLangId);
if (sqlPropertyTypeIds != null)
sqlDelete.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, sqlPropertyTypeIds);
if (propertyTypeIds != null)
sqlDelete.WhereIn<PropertyDataDto>(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<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == null)
.WhereIn<PropertyDataDto>(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<PropertyDataDto>(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<PropertyDataDto>(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<PropertyDataDto>()
.Where<PropertyDataDto>(x => x.LanguageId == null);
if (sqlPropertyTypeIds != null)
sqlSelectData.WhereIn<PropertyDataDto>(x => x.PropertyTypeId, sqlPropertyTypeIds);
if (propertyTypeIds != null)
sqlSelectData.WhereIn<PropertyDataDto>(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)