From 0683e5f55bdccdcd63953910c1f76e32f001ca67 Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Wed, 27 Nov 2019 15:36:34 -0500 Subject: [PATCH] Optimize tag persistence --- DataLayer/UNTESTED/EfClasses/Book.cs | 59 ++++++++----------- .../UNTESTED/TagPersistenceInterceptor.cs | 6 +- DataLayer/_HowTo- EF Core.txt | 25 +++++--- DataLayer/appsettings.json | 5 +- FileManager/UNTESTED/TagsPersistence.cs | 6 +- 5 files changed, 53 insertions(+), 48 deletions(-) diff --git a/DataLayer/UNTESTED/EfClasses/Book.cs b/DataLayer/UNTESTED/EfClasses/Book.cs index cc695409..1ce9ca10 100644 --- a/DataLayer/UNTESTED/EfClasses/Book.cs +++ b/DataLayer/UNTESTED/EfClasses/Book.cs @@ -123,16 +123,10 @@ namespace DataLayer ArgumentValidator.EnsureEnumerableNotNullOrEmpty(newContributors, nameof(newContributors)); // the edge cases of doing local-loaded vs remote-only got weird. just load it - if (_contributorsLink == null) - { - ArgumentValidator.EnsureNotNull(context, nameof(context)); - if (!context.Entry(this).IsKeySet) - throw new InvalidOperationException("Could not add contributors"); + if (_contributorsLink is null) + getEntry(context).Collection(s => s.ContributorsLink).Load(); - context.Entry(this).Collection(s => s.ContributorsLink).Load(); - } - - var roleContributions = getContributions(role); + var roleContributions = getContributions(role); var isIdentical = roleContributions.Select(c => c.Contributor).SequenceEqual(newContributors); if (isIdentical) return; @@ -140,7 +134,8 @@ namespace DataLayer _contributorsLink.RemoveWhere(bc => bc.Role == role); addNewContributors(newContributors, role); } - private void addNewContributors(IEnumerable newContributors, Role role) + + private void addNewContributors(IEnumerable newContributors, Role role) { byte order = 0; var newContributionsEnum = newContributors.Select(c => new BookContributor(this, c, role, order++)); @@ -155,6 +150,18 @@ namespace DataLayer .ToList(); #endregion + private Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry getEntry(DbContext context) + { + ArgumentValidator.EnsureNotNull(context, nameof(context)); + + var entry = context.Entry(this); + + if (!entry.IsKeySet) + throw new InvalidOperationException("Could not load a valid Book from database"); + + return entry; + } + #region series private HashSet _seriesLink; public IEnumerable SeriesLink => _seriesLink?.ToList(); @@ -186,16 +193,10 @@ namespace DataLayer // our add() is conditional upon what's already included in the collection. // therefore if not loaded, a trip is required. might as well just load it - if (_seriesLink == null) - { - ArgumentValidator.EnsureNotNull(context, nameof(context)); - if (!context.Entry(this).IsKeySet) - throw new InvalidOperationException("Could not add series"); + if (_seriesLink is null) + getEntry(context).Collection(s => s.SeriesLink).Load(); - context.Entry(this).Collection(s => s.SeriesLink).Load(); - } - - var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series); + var singleSeriesBook = _seriesLink.SingleOrDefault(sb => sb.Series == series); if (singleSeriesBook == null) _seriesLink.Add(new SeriesBook(series, this, index)); else @@ -211,8 +212,7 @@ namespace DataLayer public void AddSupplementDownloadUrl(string url) { // supplements are owned by Book, so no need to Load(): - // OwnsMany: "Can only ever appear on navigation properties of other entity types. - // Are automatically loaded, and can only be tracked by a DbContext alongside their owner." + // Are automatically loaded, and can only be tracked by a DbContext alongside their owner. ArgumentValidator.EnsureNotNullOrWhiteSpace(url, nameof(url)); @@ -232,19 +232,12 @@ namespace DataLayer } public void UpdateCategory(Category category, DbContext context = null) - { - // since category is never null, nullity means it hasn't been loaded. non null means we're correctly loaded. just overwrite - if (Category != null) - { - Category = category; - return; - } + { + // since category is never null, nullity means it hasn't been loaded + if (Category is null) + getEntry(context).Reference(s => s.Category).Load(); - if (context == null) - throw new Exception("need context"); - - context.Entry(this).Reference(s => s.Category).Load(); - Category = category; + Category = category; } public override string ToString() => $"[{AudibleProductId}] {Title}"; diff --git a/DataLayer/UNTESTED/TagPersistenceInterceptor.cs b/DataLayer/UNTESTED/TagPersistenceInterceptor.cs index 0d4bc606..fcead7ac 100644 --- a/DataLayer/UNTESTED/TagPersistenceInterceptor.cs +++ b/DataLayer/UNTESTED/TagPersistenceInterceptor.cs @@ -26,13 +26,13 @@ namespace DataLayer private static void persistTags(List modifiedEntities) { - var tagSets = modifiedEntities + var tagsCollection = modifiedEntities .Select(e => e.Entity as UserDefinedItem) // filter by null but NOT by blank. blank is the valid way to show the absence of tags .Where(a => a != null) + .Select(t => (t.Book.AudibleProductId, t.Tags)) .ToList(); - foreach (var t in tagSets) - FileManager.TagsPersistence.Save(t.Book.AudibleProductId, t.Tags); + FileManager.TagsPersistence.Save(tagsCollection); } } } diff --git a/DataLayer/_HowTo- EF Core.txt b/DataLayer/_HowTo- EF Core.txt index 533cf718..a64994f4 100644 --- a/DataLayer/_HowTo- EF Core.txt +++ b/DataLayer/_HowTo- EF Core.txt @@ -7,15 +7,22 @@ nuget Microsoft.EntityFrameworkCore.Tools (needed for using Package Manager Console) Microsoft.EntityFrameworkCore.Sqlite -MIGRATIONS require standard, not core -using standard instead of core. edit 3 things in csproj -1of3: pluralize xml TargetFramework tag to TargetFrameworks -2of2: TargetFrameworks from: netstandard2.1 -to: netcoreapp3.0;netstandard2.1 -3of3: add - - true - +MIGRATIONS + require core, not standard + this can be a problem b/c standard and framework can only reference standard, not core +TO USE MIGRATIONS (core and/or standard) + add to csproj + + true + +TO USE MIGRATIONS AS *BOTH* CORE AND STANDARD + edit csproj + pluralize this xml tag + from: TargetFramework + to: TargetFrameworks + inside of TargetFrameworks + from: netstandard2.1 + to: netcoreapp3.0;netstandard2.1 run. error SQLite Error 1: 'no such table: Blogs'. diff --git a/DataLayer/appsettings.json b/DataLayer/appsettings.json index d98c3023..717577fe 100644 --- a/DataLayer/appsettings.json +++ b/DataLayer/appsettings.json @@ -3,7 +3,10 @@ "LibationContext_sqlserver": "Server=(LocalDb)\\MSSQLLocalDB;Database=DataLayer.LibationContext;Integrated Security=true;", "LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;", - "// on windows sqlite paths accept windows and/or unix slashes": "", + "// sqlite notes": "", + "// absolute path example": "Data Source=C:/foo/bar/sample.db", + "// relative path example": "Data Source=sample.db", + "// on windows: sqlite paths accept windows and/or unix slashes": "", "MyTestContext": "Data Source=%DESKTOP%/sample.db" } } \ No newline at end of file diff --git a/FileManager/UNTESTED/TagsPersistence.cs b/FileManager/UNTESTED/TagsPersistence.cs index 58666f1e..fa22ca58 100644 --- a/FileManager/UNTESTED/TagsPersistence.cs +++ b/FileManager/UNTESTED/TagsPersistence.cs @@ -27,11 +27,13 @@ namespace FileManager = Policy.Handle() .WaitAndRetry(new[] { TimeSpan.FromMilliseconds(100) }); - public static void Save(string productId, string tags) + public static void Save(IEnumerable<(string productId, string tags)> tagsCollection) { ensureCache(); - cache[productId] = tags; + // on initial reload, there's a huge benefit to adding to cache individually then updating the file only once + foreach ((string productId, string tags) in tagsCollection) + cache[productId] = tags; lock (locker) policy.Execute(() => File.WriteAllText(TagsFile, JsonConvert.SerializeObject(cache, Formatting.Indented)));