254 lines
8.9 KiB
C#
254 lines
8.9 KiB
C#
using BuildFlow.Workflow.Tests.Reliability;
|
|
using FluentAssertions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Text.Json.Nodes;
|
|
using Workflow.Application.Form.Aggregates;
|
|
using Workflow.Domain.Entities;
|
|
using Workflow.Domain.Enums;
|
|
using Xunit;
|
|
|
|
namespace BuildFlow.Workflow.Tests.Performance;
|
|
|
|
/// <summary>
|
|
/// 表单聚合刷新查询次数门禁,避免目标行和统计字段触发 N+1。
|
|
/// </summary>
|
|
[Collection(PostgresWorkflowCollection.Name)]
|
|
public class FormAggregateQueryCountTests(PostgresWorkflowFixture fixture)
|
|
{
|
|
[Fact]
|
|
public async Task AggregateRefreshUsesConstantSelectCountForManyRowsAndFields()
|
|
{
|
|
await ResetAsync();
|
|
var targetFormId = Guid.NewGuid();
|
|
var sourceFormId = Guid.NewGuid();
|
|
var instanceId = Guid.NewGuid();
|
|
await using (var seed = fixture.CreateDbContext())
|
|
{
|
|
seed.FormDefinitions.AddRange(
|
|
new FormDefinition
|
|
{
|
|
Id = targetFormId,
|
|
Name = "费用汇总",
|
|
Code = "expense-main",
|
|
Version = 1,
|
|
Status = FormStatus.Published,
|
|
SchemaJson = AggregateSchema,
|
|
},
|
|
new FormDefinition
|
|
{
|
|
Id = sourceFormId,
|
|
Name = "费用明细",
|
|
Code = "expense-line",
|
|
Version = 1,
|
|
Status = FormStatus.Published,
|
|
SchemaJson = """{"type":"object","properties":{}}""",
|
|
});
|
|
seed.WorkflowInstances.Add(new WorkflowInstance
|
|
{
|
|
Id = instanceId,
|
|
DefinitionId = Guid.NewGuid(),
|
|
DefinitionVersion = 1,
|
|
Title = "聚合查询计数",
|
|
Status = InstanceStatus.Running,
|
|
InitiatorId = Guid.NewGuid(),
|
|
Variables = "{}",
|
|
});
|
|
|
|
for (var i = 0; i < 10; i++)
|
|
{
|
|
seed.FormData.Add(new global::Workflow.Domain.Entities.FormData
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
FormDefinitionId = targetFormId,
|
|
InstanceId = instanceId,
|
|
DataJson = $$"""{"requestId":"REQ-{{i}}","lineTotal":0,"lineCount":0}""",
|
|
});
|
|
seed.FormData.AddRange(
|
|
CreateSourceRow(sourceFormId, instanceId, i, 10),
|
|
CreateSourceRow(sourceFormId, instanceId, i, 20));
|
|
}
|
|
|
|
await seed.SaveChangesAsync();
|
|
}
|
|
|
|
var interceptor = new CountingCommandInterceptor();
|
|
await using (var db = fixture.CreateDbContext(interceptor))
|
|
{
|
|
interceptor.Reset();
|
|
var service = new FormAggregateRefreshService(db);
|
|
|
|
await service.RefreshAsync(targetFormId, instanceId, CancellationToken.None);
|
|
|
|
interceptor.SelectCount.Should().Be(4);
|
|
}
|
|
|
|
await using var verify = fixture.CreateDbContext();
|
|
var targetRows = await verify.FormData
|
|
.Where(x => x.FormDefinitionId == targetFormId)
|
|
.ToListAsync();
|
|
targetRows.Should().HaveCount(10);
|
|
targetRows.Should().OnlyContain(x => HasExpectedAggregates(x));
|
|
var instance = await verify.WorkflowInstances.SingleAsync(x => x.Id == instanceId);
|
|
var variables = JsonNode.Parse(instance.Variables!)!.AsObject();
|
|
variables["lineTotal"]!.GetValue<decimal>().Should().Be(30);
|
|
variables["lineCount"]!.GetValue<decimal>().Should().Be(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AggregateRefreshSupportsSourceRowsFromTheTargetForm()
|
|
{
|
|
await ResetAsync();
|
|
var formDefinitionId = Guid.NewGuid();
|
|
var instanceId = Guid.NewGuid();
|
|
await using (var seed = fixture.CreateDbContext())
|
|
{
|
|
seed.FormDefinitions.Add(new FormDefinition
|
|
{
|
|
Id = formDefinitionId,
|
|
Name = "自聚合表单",
|
|
Code = "self-aggregate",
|
|
Version = 1,
|
|
Status = FormStatus.Published,
|
|
SchemaJson = SelfAggregateSchema,
|
|
});
|
|
seed.FormData.AddRange(
|
|
new global::Workflow.Domain.Entities.FormData
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
FormDefinitionId = formDefinitionId,
|
|
InstanceId = instanceId,
|
|
DataJson = """{"requestId":"A","amount":10,"total":0}""",
|
|
},
|
|
new global::Workflow.Domain.Entities.FormData
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
FormDefinitionId = formDefinitionId,
|
|
InstanceId = instanceId,
|
|
DataJson = """{"requestId":"B","amount":20,"total":0}""",
|
|
});
|
|
await seed.SaveChangesAsync();
|
|
}
|
|
|
|
await using (var db = fixture.CreateDbContext())
|
|
{
|
|
var service = new FormAggregateRefreshService(db);
|
|
await service.RefreshAsync(formDefinitionId, instanceId, CancellationToken.None);
|
|
}
|
|
|
|
await using var verify = fixture.CreateDbContext();
|
|
var rows = await verify.FormData
|
|
.Where(x => x.FormDefinitionId == formDefinitionId)
|
|
.ToListAsync();
|
|
rows.Should().Contain(x => HasTotal(x, 10));
|
|
rows.Should().Contain(x => HasTotal(x, 20));
|
|
}
|
|
|
|
private async Task ResetAsync()
|
|
{
|
|
await using var db = fixture.CreateDbContext();
|
|
await db.WorkflowTimerJobs.ExecuteDeleteAsync();
|
|
await db.WorkflowTasks.ExecuteDeleteAsync();
|
|
await db.WorkflowTokens.ExecuteDeleteAsync();
|
|
await db.FormData.ExecuteDeleteAsync();
|
|
await db.BuildFlowImportJobs.ExecuteDeleteAsync();
|
|
await db.WorkflowInstances.ExecuteDeleteAsync();
|
|
await db.FormDefinitions.ExecuteDeleteAsync();
|
|
}
|
|
|
|
private static global::Workflow.Domain.Entities.FormData CreateSourceRow(
|
|
Guid sourceFormId,
|
|
Guid instanceId,
|
|
int requestIndex,
|
|
int amount)
|
|
=> new()
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
FormDefinitionId = sourceFormId,
|
|
InstanceId = instanceId,
|
|
DataJson = $$"""{"requestId":"REQ-{{requestIndex}}","amount":{{amount}}}""",
|
|
};
|
|
|
|
private static bool HasExpectedAggregates(
|
|
global::Workflow.Domain.Entities.FormData row)
|
|
{
|
|
var data = JsonNode.Parse(row.DataJson)!.AsObject();
|
|
return data["lineTotal"]!.GetValue<decimal>() == 30
|
|
&& data["lineCount"]!.GetValue<decimal>() == 2;
|
|
}
|
|
|
|
private static bool HasTotal(
|
|
global::Workflow.Domain.Entities.FormData row,
|
|
decimal expected)
|
|
{
|
|
var data = JsonNode.Parse(row.DataJson)!.AsObject();
|
|
return data["amount"]!.GetValue<decimal>() == expected
|
|
&& data["total"]!.GetValue<decimal>() == expected;
|
|
}
|
|
|
|
private const string AggregateSchema = """
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"requestId": { "type": "string", "title": "申请编号", "x-component": "Input" },
|
|
"lineTotal": {
|
|
"type": "number",
|
|
"title": "明细总金额",
|
|
"x-component": "InputNumber",
|
|
"x-field-kind": "aggregate",
|
|
"x-derived": {
|
|
"mode": "aggregate",
|
|
"source": {
|
|
"type": "formData",
|
|
"formDefinitionCode": "expense-line",
|
|
"relationPath": "requestId",
|
|
"relationValuePath": "requestId"
|
|
},
|
|
"aggregate": { "function": "sum", "fieldPath": "amount" }
|
|
}
|
|
},
|
|
"lineCount": {
|
|
"type": "number",
|
|
"title": "明细数量",
|
|
"x-component": "InputNumber",
|
|
"x-field-kind": "aggregate",
|
|
"x-derived": {
|
|
"mode": "aggregate",
|
|
"source": {
|
|
"type": "formData",
|
|
"formDefinitionCode": "expense-line",
|
|
"relationPath": "requestId",
|
|
"relationValuePath": "requestId"
|
|
},
|
|
"aggregate": { "function": "count" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
|
|
private const string SelfAggregateSchema = """
|
|
{
|
|
"type": "object",
|
|
"properties": {
|
|
"requestId": { "type": "string", "x-component": "Input" },
|
|
"amount": { "type": "number", "x-component": "InputNumber" },
|
|
"total": {
|
|
"type": "number",
|
|
"x-component": "InputNumber",
|
|
"x-field-kind": "aggregate",
|
|
"x-derived": {
|
|
"mode": "aggregate",
|
|
"source": {
|
|
"type": "formData",
|
|
"formDefinitionCode": "self-aggregate",
|
|
"relationPath": "requestId",
|
|
"relationValuePath": "requestId"
|
|
},
|
|
"aggregate": { "function": "sum", "fieldPath": "amount" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
}
|