- 重命名 log_file 为 test_scenario(测试场景) - 拆分维度表:tx_panel、tx_hardware、tx_software、rx_type - 采用星型模型设计,消除数据冗余 - 为所有表添加软删除字段 is_deleted - 更新关系说明,包含全局查询过滤器配置 - 更新 ER 图和查询示例 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
360 lines
10 KiB
Markdown
360 lines
10 KiB
Markdown
# 关系说明
|
||
|
||
> 所属模块:[数据模型概览](../概览.md)
|
||
|
||
---
|
||
|
||
## 1. 关系概览
|
||
|
||
### 维度表 → 场景表
|
||
|
||
| 关系 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| tx_panel → test_scenario | 1:N | 一个面板类型被多个场景引用 |
|
||
| tx_hardware → test_scenario | 1:N | 一个硬件版本被多个场景引用 |
|
||
| tx_software → test_scenario | 1:N | 一个软件版本被多个场景引用 |
|
||
| rx_type → test_scenario | 1:N | 一个RX类型被多个场景引用 |
|
||
|
||
### 场景表 → 数据表
|
||
|
||
| 关系 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| test_scenario → qfod_record | 1:N | 一个场景包含多条 Qfod 记录 |
|
||
| test_scenario → ploss_record | 1:N | 一个场景包含多条 Ploss 记录 |
|
||
|
||
---
|
||
|
||
## 2. 关系详情
|
||
|
||
### 2.1 维度表 → test_scenario
|
||
|
||
```mermaid
|
||
erDiagram
|
||
tx_panel ||--o{ test_scenario : "引用"
|
||
tx_hardware ||--o{ test_scenario : "引用"
|
||
tx_software ||--o{ test_scenario : "引用"
|
||
rx_type ||--o{ test_scenario : "引用"
|
||
```
|
||
|
||
| 属性 | 说明 |
|
||
|------|------|
|
||
| 类型 | 一对多(1:N) |
|
||
| 外键 | 场景表存储维度表的外键 |
|
||
| 删除行为 | RESTRICT(维度被引用时不能删除) |
|
||
| 业务含义 | 维度值可被多个场景复用,消除冗余 |
|
||
|
||
### 2.2 test_scenario → qfod_record
|
||
|
||
```mermaid
|
||
erDiagram
|
||
test_scenario ||--o{ qfod_record : "包含"
|
||
```
|
||
|
||
| 属性 | 说明 |
|
||
|------|------|
|
||
| 类型 | 一对多(1:N) |
|
||
| 外键 | qfod_record.scenario_id → test_scenario.id(必需) |
|
||
| 删除行为 | CASCADE(删除场景时同步删除关联记录) |
|
||
| 业务含义 | 一个测试场景产生多条 Qfod 检测数据 |
|
||
|
||
### 2.3 test_scenario → ploss_record
|
||
|
||
```mermaid
|
||
erDiagram
|
||
test_scenario ||--o{ ploss_record : "包含"
|
||
```
|
||
|
||
| 属性 | 说明 |
|
||
|------|------|
|
||
| 类型 | 一对多(1:N) |
|
||
| 外键 | ploss_record.scenario_id → test_scenario.id(必需) |
|
||
| 删除行为 | CASCADE(删除场景时同步删除关联记录) |
|
||
| 业务含义 | 一个测试场景产生多条 Ploss FOD 数据 |
|
||
|
||
---
|
||
|
||
## 3. EF Core 配置
|
||
|
||
```csharp
|
||
// Data/WctMinerDbContext.cs
|
||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||
{
|
||
// 维度表 → 场景表 关系配置
|
||
modelBuilder.Entity<TestScenario>(entity =>
|
||
{
|
||
// TX面板
|
||
entity.HasOne(e => e.TxPanel)
|
||
.WithMany(p => p.TestScenarios)
|
||
.HasForeignKey(e => e.TxPanelId)
|
||
.OnDelete(DeleteBehavior.Restrict); // 维度被引用时不能删除
|
||
|
||
// TX硬件版本
|
||
entity.HasOne(e => e.TxHardware)
|
||
.WithMany(h => h.TestScenarios)
|
||
.HasForeignKey(e => e.TxHardwareId)
|
||
.OnDelete(DeleteBehavior.Restrict);
|
||
|
||
// TX软件版本
|
||
entity.HasOne(e => e.TxSoftware)
|
||
.WithMany(s => s.TestScenarios)
|
||
.HasForeignKey(e => e.TxSoftwareId)
|
||
.OnDelete(DeleteBehavior.Restrict);
|
||
|
||
// RX类型
|
||
entity.HasOne(e => e.RxType)
|
||
.WithMany(r => r.TestScenarios)
|
||
.HasForeignKey(e => e.RxTypeId)
|
||
.OnDelete(DeleteBehavior.Restrict);
|
||
|
||
// 唯一约束
|
||
entity.HasIndex(e => new {
|
||
e.TxPanelId,
|
||
e.TxHardwareId,
|
||
e.TxSoftwareId,
|
||
e.RxTypeId,
|
||
e.TestPurpose,
|
||
e.TestDate,
|
||
e.TestSequence
|
||
}).IsUnique();
|
||
});
|
||
|
||
// 场景表 → QfodRecord 关系配置
|
||
modelBuilder.Entity<QfodRecord>(entity =>
|
||
{
|
||
entity.HasOne(e => e.Scenario)
|
||
.WithMany(s => s.QfodRecords)
|
||
.HasForeignKey(e => e.ScenarioId)
|
||
.OnDelete(DeleteBehavior.Cascade); // 删除场景时同步删除记录
|
||
});
|
||
|
||
// 场景表 → PlossRecord 关系配置
|
||
modelBuilder.Entity<PlossRecord>(entity =>
|
||
{
|
||
entity.HasOne(e => e.Scenario)
|
||
.WithMany(s => s.PlossRecords)
|
||
.HasForeignKey(e => e.ScenarioId)
|
||
.OnDelete(DeleteBehavior.Cascade);
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 查询示例
|
||
|
||
### 基础查询
|
||
|
||
```csharp
|
||
// 查询场景及其所有维度属性
|
||
var scenario = await context.TestScenarios
|
||
.Include(s => s.TxPanel)
|
||
.Include(s => s.TxHardware)
|
||
.Include(s => s.TxSoftware)
|
||
.Include(s => s.RxType)
|
||
.FirstOrDefaultAsync(s => s.Id == scenarioId);
|
||
|
||
// 输出维度属性
|
||
Console.WriteLine($"面板: {scenario.TxPanel.Name}");
|
||
Console.WriteLine($"硬件: {scenario.TxHardware.Version}");
|
||
Console.WriteLine($"软件: {scenario.TxSoftware.Version}");
|
||
Console.WriteLine($"RX: {scenario.RxType.Name}");
|
||
```
|
||
|
||
### 按维度筛选
|
||
|
||
```csharp
|
||
// 查询指定面板类型的所有场景
|
||
var scenarios = await context.TestScenarios
|
||
.Include(s => s.TxPanel)
|
||
.Where(s => s.TxPanel.Name == "single-mold")
|
||
.ToListAsync();
|
||
|
||
// 查询指定硬件版本的所有场景
|
||
var scenarios = await context.TestScenarios
|
||
.Include(s => s.TxHardware)
|
||
.Where(s => s.TxHardware.Version == "V1.0")
|
||
.ToListAsync();
|
||
```
|
||
|
||
### 按维度分组统计
|
||
|
||
```csharp
|
||
// 按面板类型统计 Qfod 记录数
|
||
var statsByPanel = await context.QfodRecords
|
||
.Include(r => r.Scenario)
|
||
.ThenInclude(s => s.TxPanel)
|
||
.GroupBy(r => r.Scenario.TxPanel.Name)
|
||
.Select(g => new { TxPanel = g.Key, Count = g.Count() })
|
||
.ToListAsync();
|
||
|
||
// 按硬件版本统计场景数
|
||
var statsByHardware = await context.TestScenarios
|
||
.Include(s => s.TxHardware)
|
||
.GroupBy(s => s.TxHardware.Version)
|
||
.Select(g => new { TxHardware = g.Key, ScenarioCount = g.Count() })
|
||
.ToListAsync();
|
||
|
||
// 按RX类型统计 Ploss 记录数
|
||
var statsByRx = await context.PlossRecords
|
||
.Include(r => r.Scenario)
|
||
.ThenInclude(s => s.RxType)
|
||
.GroupBy(r => r.Scenario.RxType.Name)
|
||
.Select(g => new { RxType = g.Key, Count = g.Count() })
|
||
.ToListAsync();
|
||
```
|
||
|
||
### 多维度联合查询
|
||
|
||
```csharp
|
||
// 查询指定面板+硬件+软件的所有场景
|
||
var scenarios = await context.TestScenarios
|
||
.Include(s => s.TxPanel)
|
||
.Include(s => s.TxHardware)
|
||
.Include(s => s.TxSoftware)
|
||
.Include(s => s.RxType)
|
||
.Where(s =>
|
||
s.TxPanel.Name == "single-mold" &&
|
||
s.TxHardware.Version == "V1.0" &&
|
||
s.TxSoftware.Version == "0x1234")
|
||
.ToListAsync();
|
||
|
||
// 查询指定维度组合的所有 Qfod 记录
|
||
var records = await context.QfodRecords
|
||
.Include(r => r.Scenario)
|
||
.ThenInclude(s => s.TxPanel)
|
||
.Include(r => r.Scenario)
|
||
.ThenInclude(s => s.TxHardware)
|
||
.Where(r =>
|
||
r.Scenario.TxPanel.Name == "single-mold" &&
|
||
r.Scenario.TxHardware.Version == "V1.0")
|
||
.ToListAsync();
|
||
```
|
||
|
||
---
|
||
|
||
## 5. 软删除设计
|
||
|
||
### 全局查询过滤器
|
||
|
||
```csharp
|
||
// Data/WctMinerDbContext.cs
|
||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||
{
|
||
// 维度表 → 场景表 关系配置
|
||
modelBuilder.Entity<TestScenario>(entity =>
|
||
{
|
||
// ... 其他配置
|
||
|
||
// 唯一约束(排除已删除记录)
|
||
entity.HasIndex(e => new {
|
||
e.TxPanelId,
|
||
e.TxHardwareId,
|
||
e.TxSoftwareId,
|
||
e.RxTypeId,
|
||
e.TestPurpose,
|
||
e.TestDate,
|
||
e.TestSequence
|
||
}).HasFilter("is_deleted = false").IsUnique();
|
||
});
|
||
|
||
// 全局查询过滤器(自动过滤已删除记录)
|
||
modelBuilder.Entity<TxPanel>().HasQueryFilter(e => !e.IsDeleted);
|
||
modelBuilder.Entity<TxHardware>().HasQueryFilter(e => !e.IsDeleted);
|
||
modelBuilder.Entity<TxSoftware>().HasQueryFilter(e => !e.IsDeleted);
|
||
modelBuilder.Entity<RxType>().HasQueryFilter(e => !e.IsDeleted);
|
||
modelBuilder.Entity<TestScenario>().HasQueryFilter(e => !e.IsDeleted);
|
||
modelBuilder.Entity<QfodRecord>().HasQueryFilter(e => !e.IsDeleted);
|
||
modelBuilder.Entity<PlossRecord>().HasQueryFilter(e => !e.IsDeleted);
|
||
}
|
||
```
|
||
|
||
### 删除行为说明
|
||
|
||
| 操作 | 行为 | 说明 |
|
||
|------|------|------|
|
||
| 删除维度 | 软删除 | 设置 is_deleted = true,不物理删除 |
|
||
| 删除场景 | 软删除 | 设置 is_deleted = true,关联记录保留 |
|
||
| 删除记录 | 软删除 | 设置 is_deleted = true,不物理删除 |
|
||
|
||
> **注意**:软删除后,关联记录不会被级联删除,因为这是软删除而非物理删除。
|
||
|
||
### 软删除操作示例
|
||
|
||
```csharp
|
||
// 软删除维度
|
||
public async Task SoftDeleteTxPanelAsync(Guid txPanelId)
|
||
{
|
||
var entity = await context.TxPanels
|
||
.IgnoreQueryFilters() // 包含已删除记录
|
||
.FirstOrDefaultAsync(t => t.Id == txPanelId);
|
||
|
||
if (entity != null && !entity.IsDeleted)
|
||
{
|
||
entity.IsDeleted = true;
|
||
await context.SaveChangesAsync();
|
||
}
|
||
}
|
||
|
||
// 软删除场景及其关联记录
|
||
public async Task SoftDeleteScenarioWithRecordsAsync(Guid scenarioId)
|
||
{
|
||
var scenario = await context.TestScenarios.FindAsync(scenarioId);
|
||
if (scenario != null)
|
||
{
|
||
scenario.IsDeleted = true;
|
||
|
||
// 同时软删除关联记录
|
||
var qfodRecords = await context.QfodRecords
|
||
.Where(r => r.ScenarioId == scenarioId)
|
||
.ToListAsync();
|
||
foreach (var record in qfodRecords)
|
||
{
|
||
record.IsDeleted = true;
|
||
}
|
||
|
||
var plossRecords = await context.PlossRecords
|
||
.Where(r => r.ScenarioId == scenarioId)
|
||
.ToListAsync();
|
||
foreach (var record in plossRecords)
|
||
{
|
||
record.IsDeleted = true;
|
||
}
|
||
|
||
await context.SaveChangesAsync();
|
||
}
|
||
}
|
||
|
||
// 恢复已删除记录
|
||
public async Task RestoreAsync(Guid id)
|
||
{
|
||
var entity = await context.TxPanels
|
||
.IgnoreQueryFilters()
|
||
.FirstOrDefaultAsync(t => t.Id == id);
|
||
|
||
if (entity != null && entity.IsDeleted)
|
||
{
|
||
entity.IsDeleted = false;
|
||
await context.SaveChangesAsync();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 查询已删除记录
|
||
|
||
```csharp
|
||
// 包含已删除记录的查询
|
||
var allRecords = await context.TxPanels
|
||
.IgnoreQueryFilters()
|
||
.ToListAsync();
|
||
|
||
// 只查询已删除记录
|
||
var deletedRecords = await context.TxPanels
|
||
.IgnoreQueryFilters()
|
||
.Where(t => t.IsDeleted)
|
||
.ToListAsync();
|
||
|
||
// 统计已删除记录数
|
||
var deletedCount = await context.TxPanels
|
||
.IgnoreQueryFilters()
|
||
.CountAsync(t => t.IsDeleted);
|
||
``` |