Refactoring DatabaseCacheRepository to de-duplicate code and remove warnings (#19942)

* Refactoring to de-duplicate code and remove warnings.

* Update src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Andy Butland
2025-08-20 09:48:50 +01:00
committed by GitHub
parent 5068b93f44
commit 467e55c5aa
2 changed files with 120 additions and 150 deletions

View File

@@ -21,6 +21,7 @@ using static Umbraco.Cms.Core.Persistence.SqlExtensionsStatics;
namespace Umbraco.Cms.Infrastructure.HybridCache.Persistence;
/// <inheritdoc/>
internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRepository
{
private readonly IContentCacheDataSerializerFactory _contentCacheDataSerializerFactory;
@@ -58,9 +59,11 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
_nucacheSettings = nucacheSettings;
}
/// <inheritdoc/>
public async Task DeleteContentItemAsync(int id)
=> await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId=@id", new { id = id });
=> await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId = @id", new { id });
/// <inheritdoc/>
public async Task RefreshContentAsync(ContentCacheNode contentCacheNode, PublishedState publishedState)
{
IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
@@ -69,7 +72,8 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
if (contentCacheNode.IsDraft)
{
await OnRepositoryRefreshed(serializer, contentCacheNode, true);
// if it's a draft node we don't need to worry about the published state
// If it's a draft node we don't need to worry about the published state.
return;
}
@@ -79,11 +83,12 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
await OnRepositoryRefreshed(serializer, contentCacheNode, false);
break;
case PublishedState.Unpublishing:
await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId=@id AND published=1", new { id = contentCacheNode.Id });
await Database.ExecuteAsync("DELETE FROM cmsContentNu WHERE nodeId = @id AND published = 1", new { id = contentCacheNode.Id });
break;
}
}
/// <inheritdoc/>
public async Task RefreshMediaAsync(ContentCacheNode contentCacheNode)
{
IContentCacheDataSerializer serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
@@ -107,15 +112,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
mediaTypeIds is not null && mediaTypeIds.Count == 0 &&
memberTypeIds is not null && memberTypeIds.Count == 0)
{
if (Database.DatabaseType == DatabaseType.SqlServer2012)
{
Database.Execute($"TRUNCATE TABLE cmsContentNu");
}
if (Database.DatabaseType == DatabaseType.SQLite)
{
Database.Execute($"DELETE FROM cmsContentNu");
}
TruncateContent();
}
RebuildContentDbCache(serializer, _nucacheSettings.Value.SqlPageSize, contentTypeIds);
@@ -123,6 +120,20 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
RebuildMemberDbCache(serializer, _nucacheSettings.Value.SqlPageSize, memberTypeIds);
}
private void TruncateContent()
{
if (Database.DatabaseType == DatabaseType.SqlServer2012)
{
Database.Execute($"TRUNCATE TABLE cmsContentNu");
}
if (Database.DatabaseType == DatabaseType.SQLite)
{
Database.Execute($"DELETE FROM cmsContentNu");
}
}
/// <inheritdoc/>
public async Task<ContentCacheNode?> GetContentSourceAsync(Guid key, bool preview = false)
{
Sql<ISqlContext>? sql = SqlContentSourcesSelect()
@@ -147,6 +158,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
return CreateContentNodeKit(dto, serializer, preview);
}
/// <inheritdoc/>
public async Task<IEnumerable<ContentCacheNode>> GetContentSourcesAsync(IEnumerable<Guid> keys, bool preview = false)
{
Sql<ISqlContext>? sql = SqlContentSourcesSelect()
@@ -170,7 +182,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
private IEnumerable<ContentSourceDto> GetContentSourceByDocumentTypeKey(IEnumerable<Guid> documentTypeKeys, Guid objectType)
{
Guid[] keys = documentTypeKeys.ToArray();
if (keys.Any() is false)
if (keys.Length == 0)
{
return [];
}
@@ -182,14 +194,15 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
: throw new ArgumentOutOfRangeException(nameof(objectType), objectType, null);
sql.InnerJoin<NodeDto>("n")
.On<NodeDto, ContentDto>((n, c) => n.NodeId == c.ContentTypeId, "n", "umbracoContent")
.Append(SqlObjectTypeNotTrashed(SqlContext, objectType))
.WhereIn<NodeDto>(x => x.UniqueId, keys,"n")
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
.On<NodeDto, ContentDto>((n, c) => n.NodeId == c.ContentTypeId, "n", "umbracoContent")
.Append(SqlObjectTypeNotTrashed(SqlContext, objectType))
.WhereIn<NodeDto>(x => x.UniqueId, keys,"n")
.Append(SqlOrderByLevelIdSortOrder(SqlContext));
return GetContentNodeDtos(sql);
}
/// <inheritdoc/>
public IEnumerable<ContentCacheNode> GetContentByContentTypeKey(IEnumerable<Guid> keys, ContentCacheDataSerializerEntityType entityType)
{
Guid objectType = entityType switch
@@ -222,6 +235,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
public IEnumerable<Guid> GetDocumentKeysByContentTypeKeys(IEnumerable<Guid> keys, bool published = false)
=> GetContentSourceByDocumentTypeKey(keys, Constants.ObjectTypes.Document).Where(x => x.Published == published).Select(x => x.Key);
/// <inheritdoc/>
public async Task<ContentCacheNode?> GetMediaSourceAsync(Guid key)
{
Sql<ISqlContext>? sql = SqlMediaSourcesSelect()
@@ -241,6 +255,7 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
return CreateMediaNodeKit(dto, serializer);
}
/// <inheritdoc/>
public async Task<IEnumerable<ContentCacheNode>> GetMediaSourcesAsync(IEnumerable<Guid> keys)
{
Sql<ISqlContext>? sql = SqlMediaSourcesSelect()
@@ -262,13 +277,12 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
private async Task OnRepositoryRefreshed(IContentCacheDataSerializer serializer, ContentCacheNode content, bool preview)
{
// use a custom SQL to update row version on each update
// db.InsertOrUpdate(dto);
ContentNuDto dto = GetDtoFromCacheNode(content, !preview, serializer);
await Database.InsertOrUpdateAsync(
dto,
"SET data=@data, dataRaw=@dataRaw, rv=rv+1 WHERE nodeId=@id AND published=@published",
"SET data = @data, dataRaw = @dataRaw, rv = rv + 1 WHERE nodeId = @id AND published = @published",
new
{
dataRaw = dto.RawData ?? Array.Empty<byte>(),
@@ -278,7 +292,12 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
});
}
// assumes content tree lock
/// <summary>
/// Rebuilds the content database cache for documents.
/// </summary>
/// <remarks>
/// Assumes content tree lock.
/// </remarks>
private void RebuildContentDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection<int>? contentTypeIds)
{
if (contentTypeIds is null)
@@ -288,55 +307,35 @@ internal sealed class DatabaseCacheRepository : RepositoryBase, IDatabaseCacheRe
Guid contentObjectType = Constants.ObjectTypes.Document;
// remove all - if anything fails the transaction will rollback
// Remove all - if anything fails the transaction will rollback.
if (contentTypeIds.Count == 0)
{
// must support SQL-CE
Database.Execute(
@"DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType
)",
new { objType = contentObjectType });
DeleteForObjectType(contentObjectType);
}
else
{
// assume number of ctypes won't blow IN(...)
// must support SQL-CE
Database.Execute(
$@"DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode
JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id
WHERE umbracoNode.nodeObjectType=@objType
AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes)
)",
new { objType = contentObjectType, ctypes = contentTypeIds });
DeleteForObjectTypeAndContentTypes(contentObjectType, contentTypeIds);
}
// insert back - if anything fails the transaction will rollback
IQuery<IContent> query = SqlContext.Query<IContent>();
if (contentTypeIds.Count > 0)
{
query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...)
}
// Insert back - if anything fails the transaction will rollback.
IQuery<IContent> query = GetInsertQuery<IContent>(contentTypeIds);
long pageIndex = 0;
long processed = 0;
long total;
do
{
// the tree is locked, counting and comparing to total is safe
// The tree is locked, counting and comparing to total is safe.
IEnumerable<IContent> descendants =
_documentRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path"));
var items = new List<ContentNuDto>();
var count = 0;
foreach (IContent c in descendants)
{
// always the edited version
// Always include the edited version.
items.Add(GetDtoFromContent(c, false, serializer));
// and also the published version if it makes any sense
// And also the published version if the document is published.
if (c.Published)
{
items.Add(GetDtoFromContent(c, true, serializer));
@@ -347,10 +346,16 @@ WHERE cmsContentNu.nodeId IN (
Database.BulkInsertRecords(items);
processed += count;
} while (processed < total);
}
while (processed < total);
}
// assumes media tree lock
/// <summary>
/// Rebuilds the content database cache for media.
/// </summary>
/// <remarks>
/// Assumes media tree lock.
/// </remarks>
private void RebuildMediaDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection<int>? contentTypeIds)
{
if (contentTypeIds is null)
@@ -360,54 +365,40 @@ WHERE cmsContentNu.nodeId IN (
Guid mediaObjectType = Constants.ObjectTypes.Media;
// remove all - if anything fails the transaction will rollback
// Remove all - if anything fails the transaction will rollback.
if (contentTypeIds.Count == 0)
{
// must support SQL-CE
Database.Execute(
@"DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType
)",
new { objType = mediaObjectType });
DeleteForObjectType(mediaObjectType);
}
else
{
// assume number of ctypes won't blow IN(...)
// must support SQL-CE
Database.Execute(
$@"DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode
JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id
WHERE umbracoNode.nodeObjectType=@objType
AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes)
)",
new { objType = mediaObjectType, ctypes = contentTypeIds });
DeleteForObjectTypeAndContentTypes(mediaObjectType, contentTypeIds);
}
// insert back - if anything fails the transaction will rollback
IQuery<IMedia> query = SqlContext.Query<IMedia>();
if (contentTypeIds.Count > 0)
{
query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...)
}
// Insert back - if anything fails the transaction will rollback.
IQuery<IMedia> query = GetInsertQuery<IMedia>(contentTypeIds);
long pageIndex = 0;
long processed = 0;
long total;
do
{
// the tree is locked, counting and comparing to total is safe
// The tree is locked, counting and comparing to total is safe.
IEnumerable<IMedia> descendants =
_mediaRepository.GetPage(query, pageIndex++, groupSize, out total, null, Ordering.By("Path"));
var items = descendants.Select(m => GetDtoFromContent(m, false, serializer)).ToArray();
ContentNuDto[] items = descendants.Select(m => GetDtoFromContent(m, false, serializer)).ToArray();
Database.BulkInsertRecords(items);
processed += items.Length;
} while (processed < total);
}
while (processed < total);
}
// assumes member tree lock
/// <summary>
/// Rebuilds the content database cache for members.
/// </summary>
/// <remarks>
/// Assumes member tree lock.
/// </remarks>
private void RebuildMemberDbCache(IContentCacheDataSerializer serializer, int groupSize, IReadOnlyCollection<int>? contentTypeIds)
{
if (contentTypeIds is null)
@@ -417,38 +408,18 @@ WHERE cmsContentNu.nodeId IN (
Guid memberObjectType = Constants.ObjectTypes.Member;
// remove all - if anything fails the transaction will rollback
// Remove all - if anything fails the transaction will rollback.
if (contentTypeIds.Count == 0)
{
// must support SQL-CE
Database.Execute(
@"DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType=@objType
)",
new { objType = memberObjectType });
DeleteForObjectType(memberObjectType);
}
else
{
// assume number of ctypes won't blow IN(...)
// must support SQL-CE
Database.Execute(
$@"DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode
JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id
WHERE umbracoNode.nodeObjectType=@objType
AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes)
)",
new { objType = memberObjectType, ctypes = contentTypeIds });
DeleteForObjectTypeAndContentTypes(memberObjectType, contentTypeIds);
}
// insert back - if anything fails the transaction will rollback
IQuery<IMember> query = SqlContext.Query<IMember>();
if (contentTypeIds.Count > 0)
{
query = query.WhereIn(x => x.ContentTypeId, contentTypeIds); // assume number of ctypes won't blow IN(...)
}
// Insert back - if anything fails the transaction will rollback.
IQuery<IMember> query = GetInsertQuery<IMember>(contentTypeIds);
long pageIndex = 0;
long processed = 0;
@@ -460,12 +431,46 @@ WHERE cmsContentNu.nodeId IN (
ContentNuDto[] items = descendants.Select(m => GetDtoFromContent(m, false, serializer)).ToArray();
Database.BulkInsertRecords(items);
processed += items.Length;
} while (processed < total);
}
while (processed < total);
}
private void DeleteForObjectType(Guid nodeObjectType) =>
Database.Execute(
@"
DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode WHERE umbracoNode.nodeObjectType = @objType
)",
new { objType = nodeObjectType });
private void DeleteForObjectTypeAndContentTypes(Guid nodeObjectType, IReadOnlyCollection<int> contentTypeIds) =>
Database.Execute(
$@"
DELETE FROM cmsContentNu
WHERE cmsContentNu.nodeId IN (
SELECT id FROM umbracoNode
JOIN {Constants.DatabaseSchema.Tables.Content} ON {Constants.DatabaseSchema.Tables.Content}.nodeId=umbracoNode.id
WHERE umbracoNode.nodeObjectType = @objType
AND {Constants.DatabaseSchema.Tables.Content}.contentTypeId IN (@ctypes)
)",
new { objType = nodeObjectType, ctypes = contentTypeIds });
private IQuery<TContent> GetInsertQuery<TContent>(IReadOnlyCollection<int> contentTypeIds)
where TContent : IContentBase
{
IQuery<TContent> query = SqlContext.Query<TContent>();
if (contentTypeIds.Count > 0)
{
query = query.WhereIn(x => x.ContentTypeId, contentTypeIds);
}
return query;
}
private ContentNuDto GetDtoFromCacheNode(ContentCacheNode cacheNode, bool published, IContentCacheDataSerializer serializer)
{
// the dictionary that will be serialized
// Prepare the data structure that will be serialized.
var contentCacheData = new ContentCacheDataModel
{
PropertyData = cacheNode.Data?.Properties,
@@ -487,23 +492,19 @@ WHERE cmsContentNu.nodeId IN (
private ContentNuDto GetDtoFromContent(IContentBase content, bool published, IContentCacheDataSerializer serializer)
{
// should inject these in ctor
// BUT for the time being we decide not to support ConvertDbToXml/String
// var propertyEditorResolver = PropertyEditorResolver.Current;
// var dataTypeService = ApplicationContext.Current.Services.DataTypeService;
var propertyData = new Dictionary<string, PropertyData[]>();
foreach (IProperty prop in content.Properties)
{
var pdatas = new List<PropertyData>();
foreach (IPropertyValue pvalue in prop.Values.OrderBy(x => x.Culture))
{
// sanitize - properties should be ok but ... never knows
// 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'
// Note: at service level, invariant is 'null', but here invariant becomes 'string.Empty'
var value = published ? pvalue.PublishedValue : pvalue.EditedValue;
if (value != null)
{
@@ -521,7 +522,7 @@ WHERE cmsContentNu.nodeId IN (
var cultureData = new Dictionary<string, CultureVariation>();
// sanitize - names should be ok but ... never knows
// Sanitize - names should be ok but ... never knows.
if (content.ContentType.VariesByCulture())
{
ContentCultureInfosCollection? infos = content is IContent document
@@ -530,7 +531,6 @@ WHERE cmsContentNu.nodeId IN (
: document.CultureInfos
: content.CultureInfos;
// ReSharper disable once UseDeconstruction
if (infos is not null)
{
foreach (ContentCultureInfos cultureInfo in infos)
@@ -547,7 +547,7 @@ WHERE cmsContentNu.nodeId IN (
}
}
// the dictionary that will be serialized
// Prepare the data structure that will be serialized.
var contentCacheData = new ContentCacheDataModel
{
PropertyData = propertyData,
@@ -566,7 +566,6 @@ WHERE cmsContentNu.nodeId IN (
return dto;
}
// we want arrays, we want them all loaded, not an enumerable
private Sql<ISqlContext> SqlContentSourcesSelect(Func<ISqlContext, Sql<ISqlContext>>? joins = null)
{
SqlTemplate sqlTemplate = SqlContext.Templates.Get(
@@ -632,36 +631,6 @@ WHERE cmsContentNu.nodeId IN (
return sql;
}
private Sql<ISqlContext> SqlContentSourcesSelectUmbracoNodeJoin(ISqlContext sqlContext)
{
ISqlSyntaxProvider syntax = sqlContext.SqlSyntax;
SqlTemplate sqlTemplate = sqlContext.Templates.Get(
Constants.SqlTemplates.NuCacheDatabaseDataSource.SourcesSelectUmbracoNodeJoin, builder =>
builder.InnerJoin<NodeDto>("x")
.On<NodeDto, NodeDto>(
(left, right) => left.NodeId == right.NodeId ||
SqlText<bool>(left.Path, right.Path,
(lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"),
aliasRight: "x"));
Sql<ISqlContext> sql = sqlTemplate.Sql();
return sql;
}
private Sql<ISqlContext> SqlWhereNodeId(ISqlContext sqlContext, int id)
{
ISqlSyntaxProvider syntax = sqlContext.SqlSyntax;
SqlTemplate sqlTemplate = sqlContext.Templates.Get(
Constants.SqlTemplates.NuCacheDatabaseDataSource.WhereNodeId,
builder =>
builder.Where<NodeDto>(x => x.NodeId == SqlTemplate.Arg<int>("id")));
Sql<ISqlContext> sql = sqlTemplate.Sql(id);
return sql;
}
private Sql<ISqlContext> SqlWhereNodeKey(ISqlContext sqlContext, Guid key)
{
ISqlSyntaxProvider syntax = sqlContext.SqlSyntax;
@@ -702,10 +671,8 @@ WHERE cmsContentNu.nodeId IN (
}
/// <summary>
/// Returns a slightly more optimized query to use for the document counting when paging over the content sources
/// Returns a slightly more optimized query to use for the document counting when paging over the content sources.
/// </summary>
/// <param name="joins"></param>
/// <returns></returns>
private Sql<ISqlContext> SqlContentSourcesCount(Func<ISqlContext, Sql<ISqlContext>>? joins = null)
{
SqlTemplate sqlTemplate = SqlContext.Templates.Get(

View File

@@ -3,6 +3,9 @@ using Umbraco.Cms.Infrastructure.HybridCache.Serialization;
namespace Umbraco.Cms.Infrastructure.HybridCache.Persistence;
/// <summary>
/// Defines a repository for persisting content cache data.
/// </summary>
internal interface IDatabaseCacheRepository
{
/// <summary>