From d3d4d37e82b9b394804f84dbdb0b873fd10f3c29 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 17 Jan 2026 13:10:00 +0100 Subject: Simplify UserDataManager and remove unused private methods Removes unused private GetUserData and GetUserDataInternal methods. Moves GetUserDataBatch to be an abstract interface method rather than having a default implementation for clarity. --- .../Library/UserDataManager.cs | 85 +++++++++++++--------- 1 file changed, 52 insertions(+), 33 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserDataManager.cs') diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 72c8d7a9d2..9e138dbdaa 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -177,53 +177,72 @@ namespace Emby.Server.Implementations.Library }; } - private UserItemData? GetUserData(User user, Guid itemId, List keys) + /// + public Dictionary GetUserDataBatch(IReadOnlyList items, User user) { - var cacheKey = GetCacheKey(user.InternalId, itemId); + var result = new Dictionary(items.Count); + var itemsNeedingQuery = new List<(BaseItem Item, List Keys)>(); - if (_cache.TryGet(cacheKey, out var data)) + foreach (var item in items) { - return data; - } - - data = GetUserDataInternal(user.Id, itemId, keys); - - if (data is null) - { - return new UserItemData() + var cacheKey = GetCacheKey(user.InternalId, item.Id); + if (_cache.TryGet(cacheKey, out var cachedData)) { - Key = keys[0], - }; + result[item.Id] = cachedData; + } + else + { + var userData = item.UserData?.Where(e => e.UserId.Equals(user.Id)).Select(Map).FirstOrDefault(); + if (userData is not null) + { + result[item.Id] = userData; + _cache.AddOrUpdate(cacheKey, userData); + } + else + { + var keys = item.GetUserDataKeys(); + itemsNeedingQuery.Add((item, keys)); + } + } } - return _cache.GetOrAdd(cacheKey, _ => data); - } - - private UserItemData? GetUserDataInternal(Guid userId, Guid itemId, List keys) - { - if (keys.Count == 0) + if (itemsNeedingQuery.Count == 0) { - return null; + return result; } - using var context = _repository.CreateDbContext(); - var userData = context.UserData.AsNoTracking().Where(e => e.ItemId == itemId && keys.Contains(e.CustomDataKey) && e.UserId.Equals(userId)).ToArray(); - - if (userData.Length > 0) + // Build a single query for all missing items + var allItemIds = itemsNeedingQuery.Select(x => x.Item.Id).ToList(); + var allKeys = itemsNeedingQuery.SelectMany(x => x.Keys).Distinct().ToList(); + if (allKeys.Count > 0) { - var directDataReference = userData.FirstOrDefault(e => e.CustomDataKey == itemId.ToString("N")); - if (directDataReference is not null) + using var context = _repository.CreateDbContext(); + var userDataArray = context.UserData + .AsNoTracking() + .Where(e => allItemIds.Contains(e.ItemId) && allKeys.Contains(e.CustomDataKey) && e.UserId.Equals(user.Id)) + .ToArray(); + + var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray()); + foreach (var (item, keys) in itemsNeedingQuery) { - return Map(directDataReference); - } + UserItemData userData; + if (userDataByItem.TryGetValue(item.Id, out var itemUserData) && itemUserData.Length > 0) + { + var directDataReference = itemUserData.FirstOrDefault(e => e.CustomDataKey == item.Id.ToString("N")); + userData = directDataReference is not null ? Map(directDataReference) : Map(itemUserData.First()); + } + else + { + userData = new UserItemData { Key = keys.Count > 0 ? keys[0] : string.Empty }; + } - return Map(userData.First()); + result[item.Id] = userData; + var cacheKey = GetCacheKey(user.InternalId, item.Id); + _cache.AddOrUpdate(cacheKey, userData); + } } - return new UserItemData - { - Key = keys.Last()! - }; + return result; } /// -- cgit v1.2.3 From f806ae40187ff5d853fff7cdd72709eab39bc9ac Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 19 Apr 2026 10:27:47 +0200 Subject: Fix too many SQL variables error on large libraries --- .../Library/UserDataManager.cs | 4 +++- .../Item/BaseItemRepository.QueryBuilding.cs | 21 +++++++-------------- .../Routines/FixIncorrectOwnerIdRelationships.cs | 2 +- .../Migrations/Routines/MigrateLinkedChildren.cs | 2 +- 4 files changed, 12 insertions(+), 17 deletions(-) (limited to 'Emby.Server.Implementations/Library/UserDataManager.cs') diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 9e138dbdaa..1281f1587f 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -219,7 +219,9 @@ namespace Emby.Server.Implementations.Library using var context = _repository.CreateDbContext(); var userDataArray = context.UserData .AsNoTracking() - .Where(e => allItemIds.Contains(e.ItemId) && allKeys.Contains(e.CustomDataKey) && e.UserId.Equals(user.Id)) + .Where(e => e.UserId.Equals(user.Id)) + .WhereOneOrMany(allItemIds, e => e.ItemId) + .WhereOneOrMany(allKeys, e => e.CustomDataKey) .ToArray(); var userDataByItem = userDataArray.GroupBy(e => e.ItemId).ToDictionary(g => g.Key, g => g.ToArray()); diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs index 812b6ab59c..12bb1e95d4 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.QueryBuilding.cs @@ -62,29 +62,23 @@ public sealed partial class BaseItemRepository private IQueryable ApplyGroupingFilter(JellyfinDbContext context, IQueryable dbQuery, InternalItemsQuery filter) { - // This whole block is needed to filter duplicate entries on request - // for the time being it cannot be used because it would destroy the ordering - // this results in "duplicate" responses for queries that try to lookup individual series or multiple versions but - // for that case the invoker has to run a DistinctBy(e => e.PresentationUniqueKey) on their own - + // Collapse duplicates sharing a presentation key (e.g. alternate versions) by picking + // the min Id per group. Keep the grouped ids as an IQueryable sub-select; materializing + // to a List would inline one bound parameter per id and hit SQLite's variable cap. var enableGroupByPresentationUniqueKey = EnableGroupByPresentationUniqueKey(filter); - // Materialize GroupBy IDs first to split the complex expression tree. - // This runs the filter+GroupBy+Min as one simple SQL query, then the downstream - // Order/Paging/Navigations work on a flat WHERE Id IN (...) list, avoiding - // EF Core having to compile a deeply nested expression tree. if (enableGroupByPresentationUniqueKey && filter.GroupBySeriesPresentationUniqueKey) { - var groupedIds = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.Min(x => x.Id)).ToList(); + var groupedIds = dbQuery.GroupBy(e => new { e.PresentationUniqueKey, e.SeriesPresentationUniqueKey }).Select(e => e.Min(x => x.Id)); dbQuery = context.BaseItems.Where(e => groupedIds.Contains(e.Id)); } else if (enableGroupByPresentationUniqueKey) { - var groupedIds = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.Min(x => x.Id)).ToList(); + var groupedIds = dbQuery.GroupBy(e => e.PresentationUniqueKey).Select(e => e.Min(x => x.Id)); dbQuery = context.BaseItems.Where(e => groupedIds.Contains(e.Id)); } else if (filter.GroupBySeriesPresentationUniqueKey) { - var groupedIds = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.Min(x => x.Id)).ToList(); + var groupedIds = dbQuery.GroupBy(e => e.SeriesPresentationUniqueKey).Select(e => e.Min(x => x.Id)); dbQuery = context.BaseItems.Where(e => groupedIds.Contains(e.Id)); } else @@ -96,8 +90,7 @@ public sealed partial class BaseItemRepository { dbQuery = ApplyBoxSetCollapsing(context, dbQuery, filter.CollapseBoxSetItemTypes); - // Apply name-range filters after collapse so BoxSets are filtered by their own name, - // not by their children's names. + // Name filters run after collapse so BoxSets match by their own name, not a child's. dbQuery = ApplyNameFilters(dbQuery, filter); } diff --git a/Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs b/Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs index 8538f5d7dc..c743590e43 100644 --- a/Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs +++ b/Jellyfin.Server/Migrations/Routines/FixIncorrectOwnerIdRelationships.cs @@ -294,7 +294,7 @@ public class FixIncorrectOwnerIdRelationships : IAsyncMigrationRoutine // Batch-load all child items in a single query var childIds = alternateVersionLinks.Select(l => l.ChildId).Distinct().ToList(); var childItems = await context.BaseItems - .Where(b => childIds.Contains(b.Id)) + .WhereOneOrMany(childIds, b => b.Id) .ToDictionaryAsync(b => b.Id, cancellationToken) .ConfigureAwait(false); diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs b/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs index f0e4803f18..129d443ca5 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLinkedChildren.cs @@ -209,7 +209,7 @@ internal class MigrateLinkedChildren : IDatabaseMigrationRoutine var childIds = toInsert.Select(lc => lc.ChildId).Distinct().ToList(); var existingChildIds = context.BaseItems - .Where(b => childIds.Contains(b.Id)) + .WhereOneOrMany(childIds, b => b.Id) .Select(b => b.Id) .ToHashSet(); -- cgit v1.2.3