Performance: Reduce number of database calls in save and publish operations (#20485)

* Added request caching to media picker media retrieval, to improve performance in save operations.

* WIP: Update or insert in bulk when updating property data.

* Add tests verifying UpdateBatch.

* Fixed issue with UpdateBatch and SQL Server.

* Removed stopwatch.

* Fix test on SQLite (failing on SQLServer).

* Added temporary test for direct call to NPoco UpdateBatch.

* Fixed test on SQLServer.

* Add integration test verifying the same property data is persisted as before the performance refactor.

* Log expected warning in DocumentUrlService as debug.

(cherry picked from commit 12adfd52bd)
This commit is contained in:
Andy Butland
2025-10-14 11:22:21 +02:00
committed by Zeegaan
parent ea44850804
commit 37b239b8ca
6 changed files with 142 additions and 10 deletions

View File

@@ -1155,25 +1155,38 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
IEnumerable<PropertyDataDto> propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures);
var toUpdate = new List<PropertyDataDto>();
var toInsert = new List<PropertyDataDto>();
foreach (PropertyDataDto propertyDataDto in propertyDataDtos)
{
// Check if this already exists and update, else insert a new one
if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out PropertyDataDto? propData))
{
propertyDataDto.Id = propData.Id;
Database.Update(propertyDataDto);
toUpdate.Add(propertyDataDto);
}
else
{
// TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs.
// This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases.
Database.Insert(propertyDataDto);
toInsert.Add(propertyDataDto);
}
// track which ones have been processed
existingPropDataIds.Remove(propertyDataDto.Id);
}
if (toUpdate.Count > 0)
{
var updateBatch = toUpdate
.Select(x => UpdateBatch.For(x))
.ToList();
Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 });
}
if (toInsert.Count > 0)
{
Database.InsertBulk(toInsert);
}
// For any remaining that haven't been processed they need to be deleted
if (existingPropDataIds.Count > 0)
{

View File

@@ -54,6 +54,8 @@ public class MediaPicker3PropertyEditor : DataEditor
/// </summary>
internal sealed class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference
{
private const string MediaCacheKeyFormat = nameof(MediaPicker3PropertyValueEditor) + "_Media_{0}";
private readonly IDataTypeConfigurationCache _dataTypeReadCache;
private readonly IJsonSerializer _jsonSerializer;
private readonly IMediaImportService _mediaImportService;
@@ -61,6 +63,7 @@ public class MediaPicker3PropertyEditor : DataEditor
private readonly ITemporaryFileService _temporaryFileService;
private readonly IScopeProvider _scopeProvider;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly AppCaches _appCaches;
/// <summary>
/// Initializes a new instance of the <see cref="MediaPicker3PropertyValueEditor"/> class.
@@ -93,6 +96,8 @@ public class MediaPicker3PropertyEditor : DataEditor
_scopeProvider = scopeProvider;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_dataTypeReadCache = dataTypeReadCache;
_appCaches = appCaches;
var validators = new TypedJsonValidatorRunner<List<MediaWithCropsDto>, MediaPicker3Configuration>(
jsonSerializer,
new MinMaxValidator(localizedTextService),
@@ -203,13 +208,31 @@ public class MediaPicker3PropertyEditor : DataEditor
foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
{
IMedia? media = _mediaService.GetById(mediaWithCropsDto.MediaKey);
IMedia? media = GetMediaById(mediaWithCropsDto.MediaKey);
mediaWithCropsDto.MediaTypeAlias = media?.ContentType.Alias ?? unknownMediaType;
}
return mediaWithCropsDtos.Where(m => m.MediaTypeAlias != unknownMediaType).ToList();
}
private IMedia? GetMediaById(Guid key)
{
// Cache media lookups in case the same media is handled multiple times across a save operation,
// which is possible, particularly if we have multiple languages and blocks.
var cacheKey = string.Format(MediaCacheKeyFormat, key);
IMedia? media = _appCaches.RequestCache.GetCacheItem<IMedia?>(cacheKey);
if (media is null)
{
media = _mediaService.GetById(key);
if (media is not null)
{
_appCaches.RequestCache.Set(cacheKey, media);
}
}
return media;
}
private List<MediaWithCropsDto> HandleTemporaryMediaUploads(List<MediaWithCropsDto> mediaWithCropsDtos, MediaPicker3Configuration configuration)
{
var invalidDtos = new List<MediaWithCropsDto>();
@@ -217,7 +240,7 @@ public class MediaPicker3PropertyEditor : DataEditor
foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos)
{
// if the media already exist, don't bother with it
if (_mediaService.GetById(mediaWithCropsDto.MediaKey) != null)
if (GetMediaById(mediaWithCropsDto.MediaKey) != null)
{
continue;
}