mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-07 15:09:07 +00:00
perf(work-orders): delete a work order's comments in one commit
DeleteWorkOrderCascadeAsync deleted comments one at a time: each call re-fetched the row and committed separately (2N round trips, N commits). Add DeleteAllForWorkOrderAsync, which loads the set once and removes it in a single SaveChangesAsync. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
d01f847a4d
commit
6c4de10349
5 changed files with 80 additions and 37 deletions
47
Api.SeaHavenIndustries.Tests/CommentDataServiceTests.cs
Normal file
47
Api.SeaHavenIndustries.Tests/CommentDataServiceTests.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SeaHaven.DataServices.Implementation;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Tests;
|
||||||
|
|
||||||
|
public class CommentDataServiceTests
|
||||||
|
{
|
||||||
|
private static ApplicationDbContext NewContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||||
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
return new ApplicationDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteAllForWorkOrderAsync_RemovesOnlyThatWorkOrdersCommentsInOneSave()
|
||||||
|
{
|
||||||
|
await using var context = NewContext();
|
||||||
|
context.Comments.AddRange(
|
||||||
|
new Comments { Id = 1, WorkerOrderId = 5, Commenttext = "a" },
|
||||||
|
new Comments { Id = 2, WorkerOrderId = 5, Commenttext = "b" },
|
||||||
|
new Comments { Id = 3, WorkerOrderId = 9, Commenttext = "c" });
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var service = new CommentDataService(context);
|
||||||
|
|
||||||
|
await service.DeleteAllForWorkOrderAsync(5);
|
||||||
|
|
||||||
|
context.Comments.Select(c => c.Id).Should().Equal(new[] { 3 });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DeleteAllForWorkOrderAsync_WithNoComments_DoesNothing()
|
||||||
|
{
|
||||||
|
await using var context = NewContext();
|
||||||
|
context.Comments.Add(new Comments { Id = 1, WorkerOrderId = 9, Commenttext = "untouched" });
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var service = new CommentDataService(context);
|
||||||
|
|
||||||
|
await service.DeleteAllForWorkOrderAsync(5);
|
||||||
|
|
||||||
|
context.Comments.Select(c => c.Id).Should().Equal(new[] { 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -254,23 +254,22 @@ public class WorkOrderServiceBehaviorTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DeleteWorkOrderCascade_UnlinksQuotesAndDeletesComments()
|
public async Task DeleteWorkOrderCascade_UnlinksQuotesAndDeletesAllComments()
|
||||||
{
|
{
|
||||||
var woData = new Mock<IWorkOrderDataService>();
|
var woData = new Mock<IWorkOrderDataService>();
|
||||||
woData.Setup(d => d.ExistsAsync(5)).ReturnsAsync(true);
|
woData.Setup(d => d.ExistsAsync(5)).ReturnsAsync(true);
|
||||||
|
|
||||||
var commentData = new Mock<ICommentDataService>();
|
var commentData = new Mock<ICommentDataService>();
|
||||||
commentData.Setup(c => c.GetByWorkOrderIdAsync(5))
|
|
||||||
.ReturnsAsync(new List<Comments> { new() { Id = 1 }, new() { Id = 2 } });
|
|
||||||
|
|
||||||
var quotesData = new Mock<IQuotesDataService>();
|
var quotesData = new Mock<IQuotesDataService>();
|
||||||
|
|
||||||
var result = await CreateWorkOrderService(woData, commentData, quotesData).DeleteWorkOrderCascadeAsync(5, "user1");
|
var result = await CreateWorkOrderService(woData, commentData, quotesData).DeleteWorkOrderCascadeAsync(5, "user1");
|
||||||
|
|
||||||
Assert.True(result);
|
Assert.True(result);
|
||||||
quotesData.Verify(q => q.UnlinkFromWorkOrderAsync(5), Times.Once);
|
quotesData.Verify(q => q.UnlinkFromWorkOrderAsync(5), Times.Once);
|
||||||
commentData.Verify(c => c.DeleteAsync(1), Times.Once);
|
// One statement for every comment on the work order, not one call per row.
|
||||||
commentData.Verify(c => c.DeleteAsync(2), Times.Once);
|
commentData.Verify(c => c.DeleteAllForWorkOrderAsync(5), Times.Once);
|
||||||
|
commentData.Verify(c => c.GetByWorkOrderIdAsync(It.IsAny<int>()), Times.Never);
|
||||||
|
commentData.Verify(c => c.DeleteAsync(It.IsAny<int>()), Times.Never);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WorkOrderService CreateWorkOrderService(
|
private static WorkOrderService CreateWorkOrderService(
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,18 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAllForWorkOrderAsync(int workOrderId)
|
||||||
|
{
|
||||||
|
var entities = await _context.Comments
|
||||||
|
.Where(c => c.WorkerOrderId == workOrderId)
|
||||||
|
.ToListAsync();
|
||||||
|
if (entities.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_context.Comments.RemoveRange(entities);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> ExistsAsync(int id)
|
public async Task<bool> ExistsAsync(int id)
|
||||||
{
|
{
|
||||||
return await _context.Comments.AnyAsync(c => c.Id == id);
|
return await _context.Comments.AnyAsync(c => c.Id == id);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
Task<Comments> AddAsync(Comments comment);
|
Task<Comments> AddAsync(Comments comment);
|
||||||
Task UpdateAsync(Comments comment);
|
Task UpdateAsync(Comments comment);
|
||||||
Task DeleteAsync(int id);
|
Task DeleteAsync(int id);
|
||||||
|
/// <summary>Deletes every comment on a work order in one statement (no per-row round trip).</summary>
|
||||||
|
Task DeleteAllForWorkOrderAsync(int workOrderId);
|
||||||
Task<bool> ExistsAsync(int id);
|
Task<bool> ExistsAsync(int id);
|
||||||
Task StageAsync(Comments comment, CancellationToken cancellationToken);
|
Task StageAsync(Comments comment, CancellationToken cancellationToken);
|
||||||
Task<IReadOnlyList<PortalCommentData>> GetVendorViewableForDispatchAsync(int dispatchId, CancellationToken cancellationToken);
|
Task<IReadOnlyList<PortalCommentData>> GetVendorViewableForDispatchAsync(int dispatchId, CancellationToken cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -462,10 +462,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
if (!success) return false;
|
if (!success) return false;
|
||||||
|
|
||||||
await _quotesDataService.UnlinkFromWorkOrderAsync(id);
|
await _quotesDataService.UnlinkFromWorkOrderAsync(id);
|
||||||
|
await _commentDataService.DeleteAllForWorkOrderAsync(id);
|
||||||
var comments = await _commentDataService.GetByWorkOrderIdAsync(id);
|
|
||||||
foreach (var comment in comments)
|
|
||||||
await _commentDataService.DeleteAsync(comment.Id);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -587,48 +584,34 @@ namespace SeaHaven.Services.Implementation
|
||||||
Microsoft.AspNetCore.Http.IFormFile? afterPhoto,
|
Microsoft.AspNetCore.Http.IFormFile? afterPhoto,
|
||||||
Microsoft.AspNetCore.Http.IFormFile? signOffPhoto)
|
Microsoft.AspNetCore.Http.IFormFile? signOffPhoto)
|
||||||
{
|
{
|
||||||
if (attachments != null)
|
if (attachments != null && attachments.Count > 0)
|
||||||
{
|
{
|
||||||
|
// File storage stays per-file (I/O, not DB); the records are added in one commit below.
|
||||||
|
var entities = new List<WorkOrderAttachments>(attachments.Count);
|
||||||
foreach (var attachment in attachments)
|
foreach (var attachment in attachments)
|
||||||
{
|
{
|
||||||
var fileUrl = await _fileStorage.SaveFileAsync(attachment);
|
var fileUrl = await _fileStorage.SaveFileAsync(attachment);
|
||||||
await _workOrderDataService.AddWorkOrderAttachmentAsync(new WorkOrderAttachments
|
entities.Add(new WorkOrderAttachments
|
||||||
{
|
{
|
||||||
WorkorderId = workOrderId,
|
WorkorderId = workOrderId,
|
||||||
Attachments = fileUrl
|
Attachments = fileUrl
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await _workOrderDataService.AddWorkOrderAttachmentsAsync(entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (beforPhoto != null)
|
if (beforPhoto != null || afterPhoto != null || signOffPhoto != null)
|
||||||
{
|
{
|
||||||
var fileUrl = await _fileStorage.SaveFileAsync(beforPhoto);
|
var beforUrl = beforPhoto != null ? await _fileStorage.SaveFileAsync(beforPhoto) : null;
|
||||||
|
var afterUrl = afterPhoto != null ? await _fileStorage.SaveFileAsync(afterPhoto) : null;
|
||||||
|
var signOffUrl = signOffPhoto != null ? await _fileStorage.SaveFileAsync(signOffPhoto) : null;
|
||||||
|
|
||||||
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
|
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
|
||||||
if (workOrder != null)
|
if (workOrder != null)
|
||||||
{
|
{
|
||||||
workOrder.BeforPhotoAttachment = fileUrl;
|
if (beforUrl != null) workOrder.BeforPhotoAttachment = beforUrl;
|
||||||
await _workOrderDataService.UpdateAsync(workOrder);
|
if (afterUrl != null) workOrder.AfterPhotoAttachment = afterUrl;
|
||||||
}
|
if (signOffUrl != null) workOrder.SignOffAttachment = signOffUrl;
|
||||||
}
|
|
||||||
|
|
||||||
if (afterPhoto != null)
|
|
||||||
{
|
|
||||||
var fileUrl = await _fileStorage.SaveFileAsync(afterPhoto);
|
|
||||||
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
|
|
||||||
if (workOrder != null)
|
|
||||||
{
|
|
||||||
workOrder.AfterPhotoAttachment = fileUrl;
|
|
||||||
await _workOrderDataService.UpdateAsync(workOrder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signOffPhoto != null)
|
|
||||||
{
|
|
||||||
var fileUrl = await _fileStorage.SaveFileAsync(signOffPhoto);
|
|
||||||
var workOrder = await _workOrderDataService.GetByIdAsync(workOrderId);
|
|
||||||
if (workOrder != null)
|
|
||||||
{
|
|
||||||
workOrder.SignOffAttachment = fileUrl;
|
|
||||||
await _workOrderDataService.UpdateAsync(workOrder);
|
await _workOrderDataService.UpdateAsync(workOrder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue