From 5a9ccb3660c2c0c02ecc69ccb8bd57517caab97a Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Tue, 23 Feb 2021 05:08:48 -0600 Subject: [PATCH] use dapper in a few places (#29) * use dapper in a few places * use single dapper queries --- .../AggregateModels/GenericIntegerId.cs | 4 - .../AggregateModels/MediaCollectionSummary.cs | 4 - .../AggregateModels/MediaItemSummary.cs | 4 - .../GenericIntegerIdConfiguration.cs | 12 -- .../MediaCollectionSummaryConfiguration.cs | 12 -- .../MediaItemSummaryConfiguration.cs | 12 -- .../Repositories/MediaCollectionRepository.cs | 108 +++++++++++------- .../Data/Repositories/TelevisionRepository.cs | 73 +++++++----- .../Data/SqliteTypeHandlers.cs | 32 ++++++ ErsatzTV.Infrastructure/Data/TvContext.cs | 12 +- .../ErsatzTV.Infrastructure.csproj | 1 + ErsatzTV/Startup.cs | 15 ++- 12 files changed, 159 insertions(+), 130 deletions(-) delete mode 100644 ErsatzTV.Core/AggregateModels/GenericIntegerId.cs delete mode 100644 ErsatzTV.Core/AggregateModels/MediaCollectionSummary.cs delete mode 100644 ErsatzTV.Core/AggregateModels/MediaItemSummary.cs delete mode 100644 ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs delete mode 100644 ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs delete mode 100644 ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs create mode 100644 ErsatzTV.Infrastructure/Data/SqliteTypeHandlers.cs diff --git a/ErsatzTV.Core/AggregateModels/GenericIntegerId.cs b/ErsatzTV.Core/AggregateModels/GenericIntegerId.cs deleted file mode 100644 index fec0b0164..000000000 --- a/ErsatzTV.Core/AggregateModels/GenericIntegerId.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ErsatzTV.Core.AggregateModels -{ - public record GenericIntegerId(int Id); -} diff --git a/ErsatzTV.Core/AggregateModels/MediaCollectionSummary.cs b/ErsatzTV.Core/AggregateModels/MediaCollectionSummary.cs deleted file mode 100644 index 0df552482..000000000 --- a/ErsatzTV.Core/AggregateModels/MediaCollectionSummary.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ErsatzTV.Core.AggregateModels -{ - public record MediaCollectionSummary(int Id, string Name, int ItemCount, bool IsSimple); -} diff --git a/ErsatzTV.Core/AggregateModels/MediaItemSummary.cs b/ErsatzTV.Core/AggregateModels/MediaItemSummary.cs deleted file mode 100644 index be009ab73..000000000 --- a/ErsatzTV.Core/AggregateModels/MediaItemSummary.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace ErsatzTV.Core.AggregateModels -{ - public record MediaItemSummary(int MediaItemId, string Title, string SortTitle, string Subtitle, string Poster); -} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs deleted file mode 100644 index 406162533..000000000 --- a/ErsatzTV.Infrastructure/Data/Configurations/GenericIntegerIdConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ErsatzTV.Core.AggregateModels; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace ErsatzTV.Infrastructure.Data.Configurations -{ - public class GenericIntegerIdConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) => - builder.HasNoKey().ToView("No table or view exists for GenericIntegerId"); - } -} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs deleted file mode 100644 index 97b1a6730..000000000 --- a/ErsatzTV.Infrastructure/Data/Configurations/MediaCollectionSummaryConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ErsatzTV.Core.AggregateModels; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace ErsatzTV.Infrastructure.Data.Configurations -{ - public class MediaCollectionSummaryConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) => - builder.HasNoKey().ToView("No table or view exists for MediaCollectionSummary"); - } -} diff --git a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs b/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs deleted file mode 100644 index 5bed7f586..000000000 --- a/ErsatzTV.Infrastructure/Data/Configurations/MediaItemSummaryConfiguration.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ErsatzTV.Core.AggregateModels; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace ErsatzTV.Infrastructure.Data.Configurations -{ - public class MediaItemSummaryConfiguration : IEntityTypeConfiguration - { - public void Configure(EntityTypeBuilder builder) => - builder.HasNoKey().ToView("No table or view exists for MediaItemSummary"); - } -} diff --git a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs index 812a63a70..8dfa21a68 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/MediaCollectionRepository.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Threading.Tasks; +using Dapper; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; using LanguageExt; @@ -12,9 +14,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories { public class MediaCollectionRepository : IMediaCollectionRepository { + private readonly IDbConnection _dbConnection; private readonly TvContext _dbContext; - public MediaCollectionRepository(TvContext dbContext) => _dbContext = dbContext; + public MediaCollectionRepository(TvContext dbContext, IDbConnection dbConnection) + { + _dbContext = dbContext; + _dbConnection = dbConnection; + } public async Task Add(SimpleMediaCollection collection) { @@ -110,61 +117,80 @@ namespace ErsatzTV.Infrastructure.Data.Repositories private async Task> GetTelevisionShowItems(SimpleMediaCollection collection) { - // TODO: would be nice to get the media items in one go, but ef... - List showItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw( - @"select tmi.Id + var parameters = new { CollectionId = collection.Id }; + return await _dbConnection + .QueryAsync( + @"select tmi.Id, tmi.SeasonId, mi.MediaSourceId, mi.LastWriteTime, mi.Path, mi.Poster, mi.PosterLastWriteTime, +mi.Statistics_AudioCodec as AudioCodec, mi.Statistics_DisplayAspectRatio as DisplayAspectRatio, mi.Statistics_Duration as Duration, mi.Statistics_Height as Height, mi.Statistics_LastWriteTime as LastWriteTime, mi.Statistics_SampleAspectRatio as SampleAspectRatio, +mi.Statistics_VideoCodec as VideoCodec, mi.Statistics_VideoScanType as VideoScanType, mi.Statistics_Width as Width, +tem.TelevisionEpisodeId, tem.Id, tem.Season, tem.Episode, tem.Plot, tem.Aired, tem.Source, tem.LastWriteTime, tem.Title, tem.SortTitle from TelevisionEpisodes tmi +inner join MediaItems mi on tmi.Id = mi.Id +inner join TelevisionEpisodeMetadata tem on tem.TelevisionEpisodeId = tmi.Id inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId inner join TelevisionShows ts on ts.Id = tsn.TelevisionShowId inner join SimpleMediaCollectionShows s on s.TelevisionShowsId = ts.Id -where s.SimpleMediaCollectionsId = {0}", - collection.Id) - .Select(i => i.Id) - .ToListAsync(); - - return await _dbContext.TelevisionEpisodeMediaItems - .AsNoTracking() - .Include(e => e.Metadata) - .Where(mi => showItemIds.Contains(mi.Id)) - .ToListAsync(); +where s.SimpleMediaCollectionsId = @CollectionId", + (episode, statistics, metadata) => + { + episode.Statistics = statistics; + episode.Metadata = metadata; + return episode; + }, + parameters, + splitOn: "AudioCodec,TelevisionEpisodeId").Map(result => result.ToList()); } private async Task> GetTelevisionSeasonItems(SimpleMediaCollection collection) { - // TODO: would be nice to get the media items in one go, but ef... - List seasonItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw( - @"select tmi.Id + var parameters = new { CollectionId = collection.Id }; + return await _dbConnection + .QueryAsync( + @"select tmi.Id, tmi.SeasonId, mi.MediaSourceId, mi.LastWriteTime, mi.Path, mi.Poster, mi.PosterLastWriteTime, +mi.Statistics_AudioCodec as AudioCodec, mi.Statistics_DisplayAspectRatio as DisplayAspectRatio, mi.Statistics_Duration as Duration, mi.Statistics_Height as Height, mi.Statistics_LastWriteTime as LastWriteTime, mi.Statistics_SampleAspectRatio as SampleAspectRatio, +mi.Statistics_VideoCodec as VideoCodec, mi.Statistics_VideoScanType as VideoScanType, mi.Statistics_Width as Width, +tem.TelevisionEpisodeId, tem.Id, tem.Season, tem.Episode, tem.Plot, tem.Aired, tem.Source, tem.LastWriteTime, tem.Title, tem.SortTitle from TelevisionEpisodes tmi +inner join MediaItems mi on tmi.Id = mi.Id +inner join TelevisionEpisodeMetadata tem on tem.TelevisionEpisodeId = tmi.Id inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId inner join SimpleMediaCollectionSeasons s on s.TelevisionSeasonsId = tsn.Id -where s.SimpleMediaCollectionsId = {0}", - collection.Id) - .Select(i => i.Id) - .ToListAsync(); - - return await _dbContext.TelevisionEpisodeMediaItems - .AsNoTracking() - .Include(e => e.Metadata) - .Where(mi => seasonItemIds.Contains(mi.Id)) - .ToListAsync(); +where s.SimpleMediaCollectionsId = @CollectionId", + (episode, statistics, metadata) => + { + episode.Statistics = statistics; + episode.Metadata = metadata; + return episode; + }, + parameters, + splitOn: "AudioCodec,TelevisionEpisodeId").Map(result => result.ToList()); } private async Task> GetTelevisionEpisodeItems(SimpleMediaCollection collection) { - // TODO: would be nice to get the media items in one go, but ef... - List episodeItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw( - @"select s.TelevisionEpisodesId as Id -from SimpleMediaCollectionEpisodes s -where s.SimpleMediaCollectionsId = {0}", - collection.Id) - .Select(i => i.Id) - .ToListAsync(); - - return await _dbContext.TelevisionEpisodeMediaItems - .AsNoTracking() - .Include(e => e.Metadata) - .Where(mi => episodeItemIds.Contains(mi.Id)) - .ToListAsync(); + var parameters = new { CollectionId = collection.Id }; + return await _dbConnection + .QueryAsync( + @"select tmi.Id, tmi.SeasonId, mi.MediaSourceId, mi.LastWriteTime, mi.Path, mi.Poster, mi.PosterLastWriteTime, +mi.Statistics_AudioCodec as AudioCodec, mi.Statistics_DisplayAspectRatio as DisplayAspectRatio, mi.Statistics_Duration as Duration, mi.Statistics_Height as Height, mi.Statistics_LastWriteTime as LastWriteTime, mi.Statistics_SampleAspectRatio as SampleAspectRatio, +mi.Statistics_VideoCodec as VideoCodec, mi.Statistics_VideoScanType as VideoScanType, mi.Statistics_Width as Width, +tem.TelevisionEpisodeId, tem.Id, tem.Season, tem.Episode, tem.Plot, tem.Aired, tem.Source, tem.LastWriteTime, tem.Title, tem.SortTitle +from TelevisionEpisodes tmi +inner join MediaItems mi on tmi.Id = mi.Id +inner join TelevisionEpisodeMetadata tem on tem.TelevisionEpisodeId = tmi.Id +inner join SimpleMediaCollectionEpisodes s on s.TelevisionEpisodesId = tmi.Id +where s.SimpleMediaCollectionsId = @CollectionId", + (episode, statistics, metadata) => + { + episode.Statistics = statistics; + episode.Metadata = metadata; + return episode; + }, + parameters, + splitOn: "AudioCodec,TelevisionEpisodeId").Map(result => result.ToList()); } } } diff --git a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs index 8b7f3d42f..ce2894ec0 100644 --- a/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs +++ b/ErsatzTV.Infrastructure/Data/Repositories/TelevisionRepository.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Threading.Tasks; +using Dapper; using ErsatzTV.Core; using ErsatzTV.Core.Domain; using ErsatzTV.Core.Interfaces.Repositories; @@ -13,9 +15,14 @@ namespace ErsatzTV.Infrastructure.Data.Repositories { public class TelevisionRepository : ITelevisionRepository { + private readonly IDbConnection _dbConnection; private readonly TvContext _dbContext; - public TelevisionRepository(TvContext dbContext) => _dbContext = dbContext; + public TelevisionRepository(TvContext dbContext, IDbConnection dbConnection) + { + _dbContext = dbContext; + _dbConnection = dbConnection; + } public async Task Update(TelevisionShow show) { @@ -245,41 +252,53 @@ namespace ErsatzTV.Infrastructure.Data.Repositories public async Task> GetShowItems(int televisionShowId) { - // TODO: would be nice to get the media items in one go, but ef... - List showItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw( - @"select tmi.Id + var parameters = new { ShowId = televisionShowId }; + return await _dbConnection + .QueryAsync( + @"select tmi.Id, tmi.SeasonId, mi.MediaSourceId, mi.LastWriteTime, mi.Path, mi.Poster, mi.PosterLastWriteTime, +mi.Statistics_AudioCodec as AudioCodec, mi.Statistics_DisplayAspectRatio as DisplayAspectRatio, mi.Statistics_Duration as Duration, mi.Statistics_Height as Height, mi.Statistics_LastWriteTime as LastWriteTime, mi.Statistics_SampleAspectRatio as SampleAspectRatio, +mi.Statistics_VideoCodec as VideoCodec, mi.Statistics_VideoScanType as VideoScanType, mi.Statistics_Width as Width, +tem.TelevisionEpisodeId, tem.Id, tem.Season, tem.Episode, tem.Plot, tem.Aired, tem.Source, tem.LastWriteTime, tem.Title, tem.SortTitle from TelevisionEpisodes tmi +inner join MediaItems mi on tmi.Id = mi.Id +inner join TelevisionEpisodeMetadata tem on tem.TelevisionEpisodeId = tmi.Id inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId inner join TelevisionShows ts on ts.Id = tsn.TelevisionShowId -where ts.Id = {0}", - televisionShowId) - .Select(i => i.Id) - .ToListAsync(); - - return await _dbContext.TelevisionEpisodeMediaItems - .AsNoTracking() - .Include(e => e.Metadata) - .Where(mi => showItemIds.Contains(mi.Id)) - .ToListAsync(); +where ts.Id = @ShowId", + (episode, statistics, metadata) => + { + episode.Statistics = statistics; + episode.Metadata = metadata; + return episode; + }, + parameters, + splitOn: "AudioCodec,TelevisionEpisodeId").Map(result => result.ToList()); } public async Task> GetSeasonItems(int televisionSeasonId) { - // TODO: would be nice to get the media items in one go, but ef... - List seasonItemIds = await _dbContext.GenericIntegerIds.FromSqlRaw( - @"select tmi.Id + var parameters = new { SeasonId = televisionSeasonId }; + return await _dbConnection + .QueryAsync( + @"select tmi.Id, tmi.SeasonId, mi.MediaSourceId, mi.LastWriteTime, mi.Path, mi.Poster, mi.PosterLastWriteTime, +mi.Statistics_AudioCodec as AudioCodec, mi.Statistics_DisplayAspectRatio as DisplayAspectRatio, mi.Statistics_Duration as Duration, mi.Statistics_Height as Height, mi.Statistics_LastWriteTime as LastWriteTime, mi.Statistics_SampleAspectRatio as SampleAspectRatio, +mi.Statistics_VideoCodec as VideoCodec, mi.Statistics_VideoScanType as VideoScanType, mi.Statistics_Width as Width, +tem.TelevisionEpisodeId, tem.Id, tem.Season, tem.Episode, tem.Plot, tem.Aired, tem.Source, tem.LastWriteTime, tem.Title, tem.SortTitle from TelevisionEpisodes tmi +inner join MediaItems mi on tmi.Id = mi.Id +inner join TelevisionEpisodeMetadata tem on tem.TelevisionEpisodeId = tmi.Id inner join TelevisionSeasons tsn on tsn.Id = tmi.SeasonId -where tsn.Id = {0}", - televisionSeasonId) - .Select(i => i.Id) - .ToListAsync(); - - return await _dbContext.TelevisionEpisodeMediaItems - .AsNoTracking() - .Include(e => e.Metadata) - .Where(mi => seasonItemIds.Contains(mi.Id)) - .ToListAsync(); +where tsn.Id = @SeasonId", + (episode, statistics, metadata) => + { + episode.Statistics = statistics; + episode.Metadata = metadata; + return episode; + }, + parameters, + splitOn: "AudioCodec,TelevisionEpisodeId").Map(result => result.ToList()); } private async Task> AddSeason( diff --git a/ErsatzTV.Infrastructure/Data/SqliteTypeHandlers.cs b/ErsatzTV.Infrastructure/Data/SqliteTypeHandlers.cs new file mode 100644 index 000000000..c9ac2fb99 --- /dev/null +++ b/ErsatzTV.Infrastructure/Data/SqliteTypeHandlers.cs @@ -0,0 +1,32 @@ +using System; +using System.Data; +using Dapper; + +namespace ErsatzTV.Infrastructure.Data +{ + public abstract class SqliteTypeHandler : SqlMapper.TypeHandler + { + // Parameters are converted by Microsoft.Data.Sqlite + public override void SetValue(IDbDataParameter parameter, T value) + => parameter.Value = value; + } + + public class DateTimeOffsetHandler : SqliteTypeHandler + { + public override DateTimeOffset Parse(object value) + => DateTimeOffset.Parse((string)value); + } + + public class GuidHandler : SqliteTypeHandler + { + public override Guid Parse(object value) + => Guid.Parse((string)value); + } + + public class TimeSpanHandler : SqliteTypeHandler + { + public override TimeSpan Parse(object value) + => TimeSpan.Parse((string)value); + } + +} diff --git a/ErsatzTV.Infrastructure/Data/TvContext.cs b/ErsatzTV.Infrastructure/Data/TvContext.cs index 688a8e885..d6a0854f6 100644 --- a/ErsatzTV.Infrastructure/Data/TvContext.cs +++ b/ErsatzTV.Infrastructure/Data/TvContext.cs @@ -1,5 +1,4 @@ -using ErsatzTV.Core.AggregateModels; -using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Domain; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; @@ -34,11 +33,6 @@ namespace ErsatzTV.Infrastructure.Data public DbSet TelevisionShowMetadata { get; set; } public DbSet TelevisionSeasons { get; set; } - // support raw sql queries - public DbSet MediaCollectionSummaries { get; set; } - public DbSet GenericIntegerIds { get; set; } - public DbSet MediaItemSummaries { get; set; } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseLoggerFactory(_loggerFactory); @@ -46,10 +40,6 @@ namespace ErsatzTV.Infrastructure.Data { base.OnModelCreating(builder); - builder.Ignore(); - builder.Ignore(); - builder.Ignore(); - builder.ApplyConfigurationsFromAssembly(typeof(TvContext).Assembly); } } diff --git a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj index db9613c4a..c43fc02bc 100644 --- a/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj +++ b/ErsatzTV.Infrastructure/ErsatzTV.Infrastructure.csproj @@ -6,6 +6,7 @@ + all diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 85210d13b..9e75db1e2 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -1,7 +1,9 @@ using System; +using System.Data; using System.IO; using System.Reflection; using System.Threading.Channels; +using Dapper; using ErsatzTV.Application; using ErsatzTV.Application.Channels.Queries; using ErsatzTV.Core; @@ -27,6 +29,7 @@ using FluentValidation.AspNetCore; using MediatR; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -109,17 +112,23 @@ namespace ErsatzTV // string xmltvPath = Path.Combine(appDataFolder, "xmltv.xml"); // Log.Logger.Information("XMLTV is at {XmltvPath}", xmltvPath); + var connectionString = $"Data Source={FileSystemLayout.DatabasePath}"; + services.AddDbContext( options => options.UseSqlite( - $"Data Source={FileSystemLayout.DatabasePath}", + connectionString, o => { o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); o.MigrationsAssembly("ErsatzTV.Infrastructure"); })); - services.AddDbContext( - options => options.UseSqlite($"Data Source={FileSystemLayout.LogDatabasePath}")); + services.AddDbContext(options => options.UseSqlite(connectionString)); + + services.AddTransient(_ => new SqliteConnection(connectionString)); + SqlMapper.AddTypeHandler(new DateTimeOffsetHandler()); + SqlMapper.AddTypeHandler(new GuidHandler()); + SqlMapper.AddTypeHandler(new TimeSpanHandler()); services.AddMediatR(typeof(GetAllChannels).Assembly);