Improved Performance of sql queries related to articles

This commit is contained in:
Mia Rose Winter 2024-04-09 19:44:52 +02:00
parent cdd1490801
commit 04bea51271
Signed by: miawinter
GPG key ID: 4B6F6A83178F595E
4 changed files with 44 additions and 24 deletions

View file

@ -164,25 +164,22 @@
} }
protected override void OnInitialized() { protected override void OnInitialized() {
using var context = ContextFactory.CreateDbContext();
var query = context.Set<Article>()
.IgnoreQueryFilters().Where(a => !a.IsDeleted)
.Include(a => a.Author)
.Include(a => a.Reviewer)
.Include(a => a.Categories);
// We need blocking calls here, bc otherwise Blazor will execute Render in parallel, // We need blocking calls here, bc otherwise Blazor will execute Render in parallel,
// running into a null pointer on the Article property and panicking // running into a null pointer on the Article property and panicking
if (Id is not null) { if (Id is not null) {
using var context = ContextFactory.CreateDbContext(); Article = query.AsSingleQuery().FirstOrDefault(a => a.Id == Id);
Article = context.Set<Article>()
.IgnoreQueryFilters().Where(a => !a.IsDeleted)
.Include(a => a.Author)
.Include(a => a.Reviewer)
.Include(a => a.Categories)
.FirstOrDefault(a => a.Id == Id);
} else if (Date is { } date && Title is { } title) { } else if (Date is { } date && Title is { } title) {
using var context = ContextFactory.CreateDbContext();
string? slug = TitleEncoded == null ? null : Uri.EscapeDataString(TitleEncoded); string? slug = TitleEncoded == null ? null : Uri.EscapeDataString(TitleEncoded);
Article ??= context.Set<Article>() Article = query.AsSingleQuery().FirstOrDefault(a =>
.IgnoreQueryFilters().Where(a => !a.IsDeleted) a.PublishDate.Date == date.Date
.Include(a => a.Author) && (slug != null && a.Slug == slug || a.Title.ToLower() == title));
.Include(a => a.Reviewer)
.Include(a => a.Categories)
.FirstOrDefault(a => a.PublishDate.Date == date.Date && (slug != null && a.Slug == slug || a.Title.ToLower() == title));
} }
} }

View file

@ -42,10 +42,22 @@
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
await using var context = await ContextFactory.CreateDbContextAsync(); await using var context = await ContextFactory.CreateDbContextAsync();
string category = WebUtility.UrlDecode(CategoryName); string category = WebUtility.UrlDecode(CategoryName);
await context.Set<Category>().LoadAsync(); var now = DateTimeOffset.UtcNow;
// First load Category with simple chain and manual filters, as to minimize
// filter redundancy and query complexity (category -> Articles -> Author is linear)
Category = await context.Set<Category>() Category = await context.Set<Category>()
.Include(c => c.Articles).ThenInclude(a => a.Author) .IgnoreAutoIncludes().IgnoreQueryFilters()
.Include(c => c.Articles).ThenInclude(a => a.Categories) .Include(c => c.Articles.Where(a => !a.IsDeleted && a.PublishDate <= now))
.ThenInclude(a => a.Author)
.FirstOrDefaultAsync(c => c.Name == category); .FirstOrDefaultAsync(c => c.Name == category);
// Load all the other categories missing on the articles, by loading all relevant
// articles ID with their categories, so EF can map them to the already loaded entries
// (again manual filter to minimize redundancy)
await context.Set<Article>()
.IgnoreAutoIncludes().IgnoreQueryFilters()
.Where(a => !a.IsDeleted && a.PublishDate <= now && a.Categories.Contains(Category!))
.Select(a => new {
a.Id, a.Categories
}).LoadAsync();
} }
} }

View file

@ -161,11 +161,14 @@
try { try {
await using var context = await ContextFactory.CreateDbContextAsync(); await using var context = await ContextFactory.CreateDbContextAsync();
var query = context.Set<Article>() var query = context.Set<Article>();
.Include(a => a.Author).Include(a => a.Categories)
.OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id);
Featured = await query.FirstOrDefaultAsync(); Featured = await query
.Include(a => a.Author)
.Include(a => a.Categories)
.OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id)
.AsSplitQuery()
.FirstOrDefaultAsync();
TotalPages = (int) Math.Max(Math.Ceiling((await query.CountAsync() - 1) / 10.0), 1); TotalPages = (int) Math.Max(Math.Ceiling((await query.CountAsync() - 1) / 10.0), 1);
} catch { } catch {
Message.ShowError(Localizer["Articles_Load_Error"]); Message.ShowError(Localizer["Articles_Load_Error"]);
@ -178,6 +181,7 @@
return await context.Set<Article>() return await context.Set<Article>()
.Include(a => a.Author).Include(a => a.Categories) .Include(a => a.Author).Include(a => a.Categories)
.OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id) .OrderByDescending(a => a.PublishDate).ThenBy(a => a.Id)
.AsSplitQuery()
.Skip(page + 1).Take(count).ToListAsync(); .Skip(page + 1).Take(count).ToListAsync();
} catch { } catch {
Message.ShowError(Localizer["Articles_Load_Error"]); Message.ShowError(Localizer["Articles_Load_Error"]);

View file

@ -93,13 +93,20 @@
// Find user // Find user
if (Id is not null) { if (Id is not null) {
User = await context.Users.Include(u => u.Articles) var now = DateTimeOffset.UtcNow;
.ThenInclude(a => a.Categories) User = await context.Users
.IgnoreAutoIncludes().IgnoreQueryFilters()
.Include(u => u.Articles.Where(a => !a.IsDeleted && a.PublishDate <= now))
.FirstOrDefaultAsync(u => u.Id == Id.ToString()); .FirstOrDefaultAsync(u => u.Id == Id.ToString());
await context.Set<Article>()
.Where(a => a.Author.Id == Id.ToString())
.Select(a => new {
a.Id, a.Categories
}).LoadAsync();
} }
// Validate access to user // Validate access to user
if (User is not null && User.Articles.Count > 0) { if (User is not null && (User.Articles.Count > 0 || HttpContext.User.IsInRole("Admin"))) {
} else if (User is not null && HttpContext.User.FindFirst("Id")?.Value == User.Id) { } else if (User is not null && HttpContext.User.FindFirst("Id")?.Value == User.Id) {
Message.ShowWarning(Localizer["ProfileNotPublic_Message"]); Message.ShowWarning(Localizer["ProfileNotPublic_Message"]);
} else { } else {