danylokhodus's picture
feat: optimize canvas load and save using lazy loading and delta upserts
1b321bd
Raw
History Blame Contribute Delete
5.32 kB
using FlowAPI.Application.Interfaces;
using FlowAPI.Domain.Entities;
using FlowAPI.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace FlowAPI.Infrastructure.Repositories
{
public class GraphRepository : GenericRepository<Graph>, IGraphRepository
{
public GraphRepository(AppDbContext context) : base(context) { }
public async Task<IEnumerable<Graph>> GetAllByUserIdAsync(Guid userId)
{
return await _dbSet
.Where(g => g.UserId == userId)
.Include(g => g.Nodes)
.Include(g => g.Edges)
.AsNoTracking()
.ToListAsync();
}
public async Task<Graph?> GetByIdWithDetailsAsync(Guid id)
{
return await _dbSet
.Include(g => g.Nodes)
.Include(g => g.Edges)
.FirstOrDefaultAsync(g => g.Id == id);
}
public async Task ClearDetailsAsync(Guid id)
{
var graph = await _dbSet
.Include(g => g.Nodes)
.Include(g => g.Edges)
.FirstOrDefaultAsync(g => g.Id == id);
if (graph != null)
{
if (graph.Edges.Any())
{
_context.Edges.RemoveRange(graph.Edges);
}
if (graph.Nodes.Any())
{
_context.TaskNodes.RemoveRange(graph.Nodes);
}
await _context.SaveChangesAsync();
}
}
public async Task<bool> UpsertFullGraphAsync(Guid graphId, IEnumerable<TaskNode> nodes, IEnumerable<Edge> edges)
{
var graph = await _dbSet
.Include(g => g.Nodes)
.Include(g => g.Edges)
.FirstOrDefaultAsync(g => g.Id == graphId);
if (graph == null) return false;
var existingNodes = graph.Nodes.ToList();
var incomingNodes = nodes.ToList();
var existingEdges = graph.Edges.ToList();
var incomingEdges = edges.ToList();
// 1. Identify nodes and edges to delete
var nodesToDelete = existingNodes.Where(e => !incomingNodes.Any(i => i.Id == e.Id)).ToList();
var deletedNodeIds = nodesToDelete.Select(n => n.Id).ToHashSet();
var edgesToDelete = existingEdges
.Where(e => !incomingEdges.Any(i => i.FromNodeId == e.FromNodeId && i.ToNodeId == e.ToNodeId && i.Condition == e.Condition)
|| deletedNodeIds.Contains(e.FromNodeId)
|| deletedNodeIds.Contains(e.ToNodeId))
.ToList();
// 2. Queue deletes (but don't save yet)
if (edgesToDelete.Any())
{
_context.Edges.RemoveRange(edgesToDelete);
}
if (nodesToDelete.Any())
{
_context.TaskNodes.RemoveRange(nodesToDelete);
}
// 3. Update or Create Nodes
foreach (var node in incomingNodes)
{
var existingNode = existingNodes.FirstOrDefault(e => e.Id == node.Id);
if (existingNode != null)
{
existingNode.Label = node.Label;
existingNode.PosX = node.PosX;
existingNode.PosY = node.PosY;
existingNode.Type = node.Type;
existingNode.Decision = node.Decision;
existingNode.State = node.State;
existingNode.Description = node.Description;
existingNode.Color = node.Color;
existingNode.IsPinned = node.IsPinned;
existingNode.Width = node.Width;
existingNode.Height = node.Height;
if (node.Data != null)
{
existingNode.Data = node.Data;
}
}
else
{
node.GraphId = graphId;
_context.TaskNodes.Add(node);
}
}
var incomingNodeIds = incomingNodes.Select(n => n.Id).ToHashSet();
// 4. Update or Create Edges
foreach (var edge in incomingEdges)
{
// Only create/update edge if both FromNode and ToNode exist in the incoming nodes list
if (!incomingNodeIds.Contains(edge.FromNodeId) || !incomingNodeIds.Contains(edge.ToNodeId))
{
continue;
}
var existingEdge = existingEdges.FirstOrDefault(e => e.FromNodeId == edge.FromNodeId && e.ToNodeId == edge.ToNodeId && e.Condition == edge.Condition);
if (existingEdge != null)
{
existingEdge.Condition = edge.Condition;
}
else
{
edge.Id = Guid.NewGuid();
edge.GraphId = graphId;
_context.Edges.Add(edge);
}
}
// 5. Single atomic save to minimize lock duration
await _context.SaveChangesAsync();
return true;
}
}
}