57d6342524
Introduce bulk operation extensions and transaction helpers for efficient, batched EF Core operations. Refactor MediaStreamInfo schema by splitting audio/video details into dedicated tables, reducing table bloat and improving update performance. Add migration for data movement and schema changes. Update EF Core models and context for new structure. Enhance PostgreSQL provider configuration and add comprehensive documentation and monitoring guides. Includes minor middleware and code cleanups.
407 lines
14 KiB
C#
407 lines
14 KiB
C#
// <copyright file="JellyfinDbContext.cs" company="PlaceholderCompany">
|
|
// Copyright (c) PlaceholderCompany. All rights reserved.
|
|
// </copyright>
|
|
|
|
#pragma warning disable SA1202
|
|
|
|
namespace Jellyfin.Database.Implementations;
|
|
|
|
using System;
|
|
using System.Data.Common;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Database.Implementations.Entities;
|
|
using Jellyfin.Database.Implementations.Entities.Security;
|
|
using Jellyfin.Database.Implementations.Interfaces;
|
|
using Jellyfin.Database.Implementations.Locking;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
/// <inheritdoc/>
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="JellyfinDbContext"/> class.
|
|
/// </summary>
|
|
/// <param name="options">The database context options.</param>
|
|
/// <param name="logger">Logger.</param>
|
|
/// <param name="jellyfinDatabaseProvider">The provider for the database engine specific operations.</param>
|
|
/// <param name="entityFrameworkCoreLocking">The locking behavior.</param>
|
|
public class JellyfinDbContext(DbContextOptions<JellyfinDbContext> options, ILogger<JellyfinDbContext> logger, IJellyfinDatabaseProvider jellyfinDatabaseProvider, IEntityFrameworkCoreLockingBehavior entityFrameworkCoreLocking) : DbContext(options)
|
|
{
|
|
private static readonly Action<ILogger, Exception?> SaveChangesError =
|
|
LoggerMessage.Define(
|
|
LogLevel.Error,
|
|
new EventId(1, nameof(SaveChanges)),
|
|
"Error trying to save changes.");
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the access schedules.
|
|
/// </summary>
|
|
public DbSet<AccessSchedule> AccessSchedules => this.Set<AccessSchedule>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the activity logs.
|
|
/// </summary>
|
|
public DbSet<ActivityLog> ActivityLogs => this.Set<ActivityLog>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the API keys.
|
|
/// </summary>
|
|
public DbSet<ApiKey> ApiKeys => this.Set<ApiKey>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the devices.
|
|
/// </summary>
|
|
public DbSet<Device> Devices => this.Set<Device>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the device options.
|
|
/// </summary>
|
|
public DbSet<DeviceOptions> DeviceOptions => this.Set<DeviceOptions>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the display preferences.
|
|
/// </summary>
|
|
public DbSet<DisplayPreferences> DisplayPreferences => this.Set<DisplayPreferences>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the image infos.
|
|
/// </summary>
|
|
public DbSet<ImageInfo> ImageInfos => this.Set<ImageInfo>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the item display preferences.
|
|
/// </summary>
|
|
public DbSet<ItemDisplayPreferences> ItemDisplayPreferences => this.Set<ItemDisplayPreferences>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the custom item display preferences.
|
|
/// </summary>
|
|
public DbSet<CustomItemDisplayPreferences> CustomItemDisplayPreferences => this.Set<CustomItemDisplayPreferences>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the permissions.
|
|
/// </summary>
|
|
public DbSet<Permission> Permissions => this.Set<Permission>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the preferences.
|
|
/// </summary>
|
|
public DbSet<Preference> Preferences => this.Set<Preference>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the users.
|
|
/// </summary>
|
|
public DbSet<User> Users => this.Set<User>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the trickplay metadata.
|
|
/// </summary>
|
|
public DbSet<TrickplayInfo> TrickplayInfos => this.Set<TrickplayInfo>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the media segments.
|
|
/// </summary>
|
|
public DbSet<MediaSegment> MediaSegments => this.Set<MediaSegment>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
|
|
/// </summary>
|
|
public DbSet<UserData> UserData => this.Set<UserData>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
|
|
/// </summary>
|
|
public DbSet<AncestorId> AncestorIds => this.Set<AncestorId>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
|
|
/// </summary>
|
|
public DbSet<AttachmentStreamInfo> AttachmentStreamInfos => this.Set<AttachmentStreamInfo>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
|
|
/// </summary>
|
|
public DbSet<BaseItemEntity> BaseItems => this.Set<BaseItemEntity>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the user data.
|
|
/// </summary>
|
|
public DbSet<Chapter> Chapters => this.Set<Chapter>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/>.
|
|
/// </summary>
|
|
public DbSet<ItemValue> ItemValues => this.Set<ItemValue>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/>.
|
|
/// </summary>
|
|
public DbSet<ItemValueMap> ItemValuesMap => this.Set<ItemValueMap>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/>.
|
|
/// </summary>
|
|
public DbSet<MediaStreamInfo> MediaStreamInfos => this.Set<MediaStreamInfo>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing audio-specific stream details.
|
|
/// </summary>
|
|
public DbSet<AudioStreamDetails> AudioStreamDetails => this.Set<AudioStreamDetails>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing video-specific stream details.
|
|
/// </summary>
|
|
public DbSet<VideoStreamDetails> VideoStreamDetails => this.Set<VideoStreamDetails>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/>.
|
|
/// </summary>
|
|
public DbSet<People> Peoples => this.Set<People>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/>.
|
|
/// </summary>
|
|
public DbSet<PeopleBaseItemMap> PeopleBaseItemMap => this.Set<PeopleBaseItemMap>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing the referenced Providers with ids.
|
|
/// </summary>
|
|
public DbSet<BaseItemProvider> BaseItemProviders => this.Set<BaseItemProvider>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/>.
|
|
/// </summary>
|
|
public DbSet<BaseItemImageInfo> BaseItemImageInfos => this.Set<BaseItemImageInfo>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/>.
|
|
/// </summary>
|
|
public DbSet<BaseItemMetadataField> BaseItemMetadataFields => this.Set<BaseItemMetadataField>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/>.
|
|
/// </summary>
|
|
public DbSet<BaseItemTrailerType> BaseItemTrailerTypes => this.Set<BaseItemTrailerType>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/>.
|
|
/// </summary>
|
|
public DbSet<KeyframeData> KeyframeData => this.Set<KeyframeData>();
|
|
|
|
/// <summary>
|
|
/// Gets the <see cref="DbSet{TEntity}"/> containing persisted library options.
|
|
/// </summary>
|
|
public DbSet<LibraryOptionsEntity> LibraryOptions => this.Set<LibraryOptionsEntity>();
|
|
|
|
/*public DbSet<Artwork> Artwork => Set<Artwork>();
|
|
|
|
public DbSet<Book> Books => Set<Book>();
|
|
|
|
public DbSet<BookMetadata> BookMetadata => Set<BookMetadata>();
|
|
|
|
public DbSet<Chapter> Chapters => Set<Chapter>();
|
|
|
|
public DbSet<Collection> Collections => Set<Collection>();
|
|
|
|
public DbSet<CollectionItem> CollectionItems => Set<CollectionItem>();
|
|
|
|
public DbSet<Company> Companies => Set<Company>();
|
|
|
|
public DbSet<CompanyMetadata> CompanyMetadata => Set<CompanyMetadata>();
|
|
|
|
public DbSet<CustomItem> CustomItems => Set<CustomItem>();
|
|
|
|
public DbSet<CustomItemMetadata> CustomItemMetadata => Set<CustomItemMetadata>();
|
|
|
|
public DbSet<Episode> Episodes => Set<Episode>();
|
|
|
|
public DbSet<EpisodeMetadata> EpisodeMetadata => Set<EpisodeMetadata>();
|
|
|
|
public DbSet<Genre> Genres => Set<Genre>();
|
|
|
|
public DbSet<Group> Groups => Set<Groups>();
|
|
|
|
public DbSet<Library> Libraries => Set<Library>();
|
|
|
|
public DbSet<LibraryItem> LibraryItems => Set<LibraryItems>();
|
|
|
|
public DbSet<LibraryRoot> LibraryRoot => Set<LibraryRoot>();
|
|
|
|
public DbSet<MediaFile> MediaFiles => Set<MediaFiles>();
|
|
|
|
public DbSet<MediaFileStream> MediaFileStream => Set<MediaFileStream>();
|
|
|
|
public DbSet<Metadata> Metadata => Set<Metadata>();
|
|
|
|
public DbSet<MetadataProvider> MetadataProviders => Set<MetadataProvider>();
|
|
|
|
public DbSet<MetadataProviderId> MetadataProviderIds => Set<MetadataProviderId>();
|
|
|
|
public DbSet<Movie> Movies => Set<Movie>();
|
|
|
|
public DbSet<MovieMetadata> MovieMetadata => Set<MovieMetadata>();
|
|
|
|
public DbSet<MusicAlbum> MusicAlbums => Set<MusicAlbum>();
|
|
|
|
public DbSet<MusicAlbumMetadata> MusicAlbumMetadata => Set<MusicAlbumMetadata>();
|
|
|
|
public DbSet<Person> People => Set<Person>();
|
|
|
|
public DbSet<PersonRole> PersonRoles => Set<PersonRole>();
|
|
|
|
public DbSet<Photo> Photo => Set<Photo>();
|
|
|
|
public DbSet<PhotoMetadata> PhotoMetadata => Set<PhotoMetadata>();
|
|
|
|
public DbSet<ProviderMapping> ProviderMappings => Set<ProviderMapping>();
|
|
|
|
public DbSet<Rating> Ratings => Set<Rating>();
|
|
|
|
/// <summary>
|
|
/// Repository for global::Jellyfin.Data.Entities.RatingSource - This is the entity to
|
|
/// store review ratings, not age ratings.
|
|
/// </summary>
|
|
public DbSet<RatingSource> RatingSources => Set<RatingSource>();
|
|
|
|
public DbSet<Release> Releases => Set<Release>();
|
|
|
|
public DbSet<Season> Seasons => Set<Season>();
|
|
|
|
public DbSet<SeasonMetadata> SeasonMetadata => Set<SeasonMetadata>();
|
|
|
|
public DbSet<Series> Series => Set<Series>();
|
|
|
|
public DbSet<SeriesMetadata> SeriesMetadata => Set<SeriesMetadata();
|
|
|
|
public DbSet<Track> Tracks => Set<Track>();
|
|
|
|
public DbSet<TrackMetadata> TrackMetadata => Set<TrackMetadata>();*/
|
|
|
|
/// <inheritdoc/>
|
|
public override async Task<int> SaveChangesAsync(
|
|
bool acceptAllChangesOnSuccess,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
this.HandleConcurrencyToken();
|
|
|
|
try
|
|
{
|
|
var result = -1;
|
|
await entityFrameworkCoreLocking.OnSaveChangesAsync(this, async () =>
|
|
{
|
|
result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken).ConfigureAwait(false);
|
|
}).ConfigureAwait(false);
|
|
return result;
|
|
}
|
|
catch (DbUpdateException ex)
|
|
{
|
|
// Check if it's a constraint violation (works across all database providers)
|
|
var isConstraintViolation = ex.InnerException?.GetType().Name.Contains("Exception", StringComparison.Ordinal) == true &&
|
|
(ex.Message.Contains("constraint", StringComparison.OrdinalIgnoreCase) ||
|
|
ex.Message.Contains("duplicate", StringComparison.OrdinalIgnoreCase) ||
|
|
ex.Message.Contains("unique", StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (isConstraintViolation)
|
|
{
|
|
// Log constraint violations as warnings (expected in some concurrent scenarios)
|
|
#pragma warning disable CA1848 // Use LoggerMessage delegates - exception type is dynamic
|
|
logger.LogWarning(
|
|
ex,
|
|
"Database constraint violation: Attempted to insert or update data that violates a database constraint. " +
|
|
"This may indicate a concurrency issue or a bug in the update logic. " +
|
|
"Inner exception: {InnerExceptionType}",
|
|
ex.InnerException?.GetType().Name ?? "unknown");
|
|
#pragma warning restore CA1848
|
|
}
|
|
else
|
|
{
|
|
// Other DbUpdateExceptions are true errors
|
|
SaveChangesError(logger, ex);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SaveChangesError(logger, e);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override int SaveChanges(bool acceptAllChangesOnSuccess) // SaveChanges(bool) is beeing called by SaveChanges() with default to false.
|
|
{
|
|
this.HandleConcurrencyToken();
|
|
|
|
try
|
|
{
|
|
var result = -1;
|
|
entityFrameworkCoreLocking.OnSaveChanges(this, () =>
|
|
{
|
|
result = base.SaveChanges(acceptAllChangesOnSuccess);
|
|
});
|
|
return result;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
SaveChangesError(logger, e);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private void HandleConcurrencyToken()
|
|
{
|
|
foreach (var saveEntity in this.ChangeTracker.Entries()
|
|
.Where(e => e.State == EntityState.Modified)
|
|
.Select(entry => entry.Entity)
|
|
.OfType<IHasConcurrencyToken>())
|
|
{
|
|
saveEntity.OnSavingChanges();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the recommended batch size for bulk operations based on the number of items to process.
|
|
/// Smaller batches during library scans reduce dead tuple generation in PostgreSQL.
|
|
/// </summary>
|
|
/// <param name="entityCount">The total number of entities being processed.</param>
|
|
/// <returns>The recommended batch size for optimal performance.</returns>
|
|
public int GetBatchSize(int entityCount)
|
|
{
|
|
// Smaller batches during media scans = fewer dead tuples
|
|
// < 100 items: batch 10
|
|
// 100-1000: batch 50
|
|
// 1000-10000: batch 100
|
|
// > 10000: batch 200
|
|
return entityCount switch
|
|
{
|
|
< 100 => 10,
|
|
< 1000 => 50,
|
|
< 10000 => 100,
|
|
_ => 200
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
jellyfinDatabaseProvider.OnModelCreating(modelBuilder);
|
|
base.OnModelCreating(modelBuilder);
|
|
|
|
ArgumentNullException.ThrowIfNull(modelBuilder);
|
|
|
|
// Configuration for each entity is in its own class inside 'ModelConfiguration'.
|
|
modelBuilder.ApplyConfigurationsFromAssembly(typeof(JellyfinDbContext).Assembly);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
|
|
{
|
|
jellyfinDatabaseProvider.ConfigureConventions(configurationBuilder);
|
|
base.ConfigureConventions(configurationBuilder);
|
|
}
|
|
}
|