refactor: simplify data connections from many-to-many site assignment to direct site ownership
Replace SiteDataConnectionAssignment join table with a direct SiteId FK on DataConnection, simplifying the data model, repositories, UI, CLI, and deployment service.
This commit is contained in:
@@ -46,26 +46,11 @@ public class DataConnectionConfiguration : IEntityTypeConfiguration<DataConnecti
|
||||
builder.Property(d => d.Configuration)
|
||||
.HasMaxLength(4000);
|
||||
|
||||
builder.HasIndex(d => d.Name).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
public class SiteDataConnectionAssignmentConfiguration : IEntityTypeConfiguration<SiteDataConnectionAssignment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SiteDataConnectionAssignment> builder)
|
||||
{
|
||||
builder.HasKey(a => a.Id);
|
||||
|
||||
builder.HasOne<Site>()
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.SiteId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
.HasForeignKey(d => d.SiteId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne<DataConnection>()
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.DataConnectionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasIndex(a => new { a.SiteId, a.DataConnectionId }).IsUnique();
|
||||
builder.HasIndex(d => new { d.SiteId, d.Name }).IsUnique();
|
||||
}
|
||||
}
|
||||
|
||||
1227
src/ScadaLink.ConfigurationDatabase/Migrations/20260321210402_AddSiteIdToDataConnections.Designer.cs
generated
Normal file
1227
src/ScadaLink.ConfigurationDatabase/Migrations/20260321210402_AddSiteIdToDataConnections.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,184 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ScadaLink.ConfigurationDatabase.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSiteIdToDataConnections : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Step 1: Drop old unique index on Name (allows duplicate names across sites)
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_DataConnections_Name",
|
||||
table: "DataConnections");
|
||||
|
||||
// Step 2: Add nullable SiteId column
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "SiteId",
|
||||
table: "DataConnections",
|
||||
type: "int",
|
||||
nullable: true);
|
||||
|
||||
// Step 3: Migrate data from SiteDataConnectionAssignments
|
||||
migrationBuilder.Sql(@"
|
||||
-- Phase A: Assign the first site to each existing DataConnection
|
||||
UPDATE dc
|
||||
SET dc.SiteId = a.SiteId
|
||||
FROM DataConnections dc
|
||||
INNER JOIN (
|
||||
SELECT DataConnectionId, MIN(SiteId) AS SiteId
|
||||
FROM SiteDataConnectionAssignments
|
||||
GROUP BY DataConnectionId
|
||||
) a ON dc.Id = a.DataConnectionId
|
||||
WHERE dc.SiteId IS NULL;
|
||||
|
||||
-- Phase B: For connections assigned to additional sites, create copies
|
||||
-- and update InstanceConnectionBindings to point to the new copy
|
||||
DECLARE @AssignSiteId INT, @AssignConnId INT, @NewConnId INT;
|
||||
DECLARE @OrigName NVARCHAR(200), @OrigProtocol NVARCHAR(50), @OrigConfig NVARCHAR(4000);
|
||||
|
||||
DECLARE assignment_cursor CURSOR FOR
|
||||
SELECT a.SiteId, a.DataConnectionId
|
||||
FROM SiteDataConnectionAssignments a
|
||||
INNER JOIN DataConnections dc ON a.DataConnectionId = dc.Id
|
||||
WHERE dc.SiteId <> a.SiteId;
|
||||
|
||||
OPEN assignment_cursor;
|
||||
FETCH NEXT FROM assignment_cursor INTO @AssignSiteId, @AssignConnId;
|
||||
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
SELECT @OrigName = Name, @OrigProtocol = Protocol, @OrigConfig = Configuration
|
||||
FROM DataConnections WHERE Id = @AssignConnId;
|
||||
|
||||
INSERT INTO DataConnections (SiteId, Name, Protocol, Configuration)
|
||||
VALUES (@AssignSiteId, @OrigName, @OrigProtocol, @OrigConfig);
|
||||
|
||||
SET @NewConnId = SCOPE_IDENTITY();
|
||||
|
||||
-- Update bindings for instances on this site to point to the new connection
|
||||
UPDATE icb
|
||||
SET icb.DataConnectionId = @NewConnId
|
||||
FROM InstanceConnectionBindings icb
|
||||
INNER JOIN Instances i ON icb.InstanceId = i.Id
|
||||
WHERE icb.DataConnectionId = @AssignConnId
|
||||
AND i.SiteId = @AssignSiteId;
|
||||
|
||||
FETCH NEXT FROM assignment_cursor INTO @AssignSiteId, @AssignConnId;
|
||||
END
|
||||
|
||||
CLOSE assignment_cursor;
|
||||
DEALLOCATE assignment_cursor;
|
||||
|
||||
-- Phase C: Handle any DataConnections not assigned to any site
|
||||
-- (assign to the first site as a fallback)
|
||||
UPDATE dc
|
||||
SET dc.SiteId = (SELECT TOP 1 Id FROM Sites ORDER BY Id)
|
||||
FROM DataConnections dc
|
||||
WHERE dc.SiteId IS NULL;
|
||||
");
|
||||
|
||||
// Step 4: Make SiteId non-nullable
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "SiteId",
|
||||
table: "DataConnections",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int",
|
||||
oldNullable: true);
|
||||
|
||||
// Step 5: Add composite unique index and FK
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataConnections_SiteId_Name",
|
||||
table: "DataConnections",
|
||||
columns: new[] { "SiteId", "Name" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_DataConnections_Sites_SiteId",
|
||||
table: "DataConnections",
|
||||
column: "SiteId",
|
||||
principalTable: "Sites",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
|
||||
// Step 6: Drop SiteDataConnectionAssignments table
|
||||
migrationBuilder.DropTable(
|
||||
name: "SiteDataConnectionAssignments");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Recreate SiteDataConnectionAssignments table
|
||||
migrationBuilder.CreateTable(
|
||||
name: "SiteDataConnectionAssignments",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "int", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
DataConnectionId = table.Column<int>(type: "int", nullable: false),
|
||||
SiteId = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SiteDataConnectionAssignments", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_SiteDataConnectionAssignments_DataConnections_DataConnectionId",
|
||||
column: x => x.DataConnectionId,
|
||||
principalTable: "DataConnections",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_SiteDataConnectionAssignments_Sites_SiteId",
|
||||
column: x => x.SiteId,
|
||||
principalTable: "Sites",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SiteDataConnectionAssignments_DataConnectionId",
|
||||
table: "SiteDataConnectionAssignments",
|
||||
column: "DataConnectionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SiteDataConnectionAssignments_SiteId_DataConnectionId",
|
||||
table: "SiteDataConnectionAssignments",
|
||||
columns: new[] { "SiteId", "DataConnectionId" },
|
||||
unique: true);
|
||||
|
||||
// Migrate data back
|
||||
migrationBuilder.Sql(@"
|
||||
INSERT INTO SiteDataConnectionAssignments (SiteId, DataConnectionId)
|
||||
SELECT SiteId, Id FROM DataConnections;
|
||||
");
|
||||
|
||||
// Remove FK and composite index
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_DataConnections_Sites_SiteId",
|
||||
table: "DataConnections");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_DataConnections_SiteId_Name",
|
||||
table: "DataConnections");
|
||||
|
||||
// Restore unique index on Name
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DataConnections_Name",
|
||||
table: "DataConnections",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
// Drop SiteId column
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SiteId",
|
||||
table: "DataConnections");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -766,9 +766,12 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<int>("SiteId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
b.HasIndex("SiteId", "Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("DataConnections");
|
||||
@@ -821,30 +824,6 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
|
||||
b.ToTable("Sites");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ScadaLink.Commons.Entities.Sites.SiteDataConnectionAssignment", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("DataConnectionId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("SiteId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DataConnectionId");
|
||||
|
||||
b.HasIndex("SiteId", "DataConnectionId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("SiteDataConnectionAssignments");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ScadaLink.Commons.Entities.Templates.Template", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1153,18 +1132,12 @@ namespace ScadaLink.ConfigurationDatabase.Migrations
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ScadaLink.Commons.Entities.Sites.SiteDataConnectionAssignment", b =>
|
||||
modelBuilder.Entity("ScadaLink.Commons.Entities.Sites.DataConnection", b =>
|
||||
{
|
||||
b.HasOne("ScadaLink.Commons.Entities.Sites.DataConnection", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("DataConnectionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ScadaLink.Commons.Entities.Sites.Site", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SiteId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
|
||||
@@ -27,17 +27,18 @@ public class CentralUiRepository : ICentralUiRepository
|
||||
|
||||
public async Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SiteDataConnectionAssignments
|
||||
return await _context.DataConnections
|
||||
.AsNoTracking()
|
||||
.Where(a => a.SiteId == siteId)
|
||||
.Join(_context.DataConnections, a => a.DataConnectionId, d => d.Id, (_, d) => d)
|
||||
.Where(d => d.SiteId == siteId)
|
||||
.OrderBy(d => d.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<SiteDataConnectionAssignment>> GetAllSiteDataConnectionAssignmentsAsync(CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<DataConnection>> GetAllDataConnectionsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.SiteDataConnectionAssignments
|
||||
return await _context.DataConnections
|
||||
.AsNoTracking()
|
||||
.OrderBy(d => d.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,13 +76,8 @@ public class SiteRepository : ISiteRepository
|
||||
|
||||
public async Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var connectionIds = await _dbContext.SiteDataConnectionAssignments
|
||||
.Where(a => a.SiteId == siteId)
|
||||
.Select(a => a.DataConnectionId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return await _dbContext.DataConnections
|
||||
.Where(c => connectionIds.Contains(c.Id))
|
||||
.Where(c => c.SiteId == siteId)
|
||||
.OrderBy(c => c.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
@@ -107,43 +102,13 @@ public class SiteRepository : ISiteRepository
|
||||
}
|
||||
else
|
||||
{
|
||||
var stub = new DataConnection("stub", "stub") { Id = id };
|
||||
var stub = new DataConnection("stub", "stub", 0) { Id = id };
|
||||
_dbContext.DataConnections.Attach(stub);
|
||||
_dbContext.DataConnections.Remove(stub);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// --- Site-Connection Assignments ---
|
||||
|
||||
public async Task<SiteDataConnectionAssignment?> GetSiteDataConnectionAssignmentAsync(
|
||||
int siteId, int dataConnectionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbContext.SiteDataConnectionAssignments
|
||||
.FirstOrDefaultAsync(a => a.SiteId == siteId && a.DataConnectionId == dataConnectionId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task AddSiteDataConnectionAssignmentAsync(SiteDataConnectionAssignment assignment, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _dbContext.SiteDataConnectionAssignments.AddAsync(assignment, cancellationToken);
|
||||
}
|
||||
|
||||
public Task DeleteSiteDataConnectionAssignmentAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = _dbContext.SiteDataConnectionAssignments.Local.FirstOrDefault(a => a.Id == id);
|
||||
if (entity != null)
|
||||
{
|
||||
_dbContext.SiteDataConnectionAssignments.Remove(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
var stub = new SiteDataConnectionAssignment { Id = id };
|
||||
_dbContext.SiteDataConnectionAssignments.Attach(stub);
|
||||
_dbContext.SiteDataConnectionAssignments.Remove(stub);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// --- Instances (for deletion constraint checks) ---
|
||||
|
||||
public async Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default)
|
||||
|
||||
@@ -35,7 +35,6 @@ public class ScadaLinkDbContext : DbContext, IDataProtectionKeyContext
|
||||
// Sites
|
||||
public DbSet<Site> Sites => Set<Site>();
|
||||
public DbSet<DataConnection> DataConnections => Set<DataConnection>();
|
||||
public DbSet<SiteDataConnectionAssignment> SiteDataConnectionAssignments => Set<SiteDataConnectionAssignment>();
|
||||
|
||||
// Deployment
|
||||
public DbSet<DeploymentRecord> DeploymentRecords => Set<DeploymentRecord>();
|
||||
|
||||
Reference in New Issue
Block a user