Blog: enforce Restrict FK on BlogPost.Author and Comment.Author

Tout billet a un auteur, tout commentaire a un auteur. On aligne la
base sur ce contrat (Postgres) en droppant les orphelins existants
puis en remplaçant les FK en cascade par des FK Restrict.

- ApplicationDbContext: fluent pour BlogPost.Author et Comment.Author
  en DeleteBehavior.Restrict.
- ApplicationUser: ajoute la nav inverse BlogComments (manquait,
  EF aurait sinon créé une shadow FK).
- Migration 20260711173717_EnforceBlogAuthorFKs: Up purge les
  Comment/BlogSpot dont l'AuthorId n'existe plus, log le volume,
  puis drop+add des FK. Down laisse la cascade (état pré-migration).

Le code applicatif (BlogSpotService.Details) s'appuiera sur cette
contrainte dans un commit séparé.
This commit is contained in:
Lum 2026-07-11 18:45:27 +01:00
commit 98613e7070
5 changed files with 4793 additions and 3 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,89 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Yavsc.Migrations
{
/// <inheritdoc />
public partial class EnforceBlogAuthorFKs : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Assainir les orphelins AVANT d'enforcer la FK Restrict.
// En prod (Postgres), la migration aurait sinon planté
// sur des billets/commentaires dont l'AuthorId pointe
// vers un user déjà supprimé. La logique métier refuse
// désormais l'orphelin (cf. BlogSpotService.Details) — on
// aligne l'état de la base avec ce contrat.
migrationBuilder.Sql(@"
DO $$
DECLARE n_comments int;
n_posts int;
BEGIN
DELETE FROM ""Comment""
WHERE ""AuthorId"" NOT IN (SELECT ""Id"" FROM ""AspNetUsers"");
GET DIAGNOSTICS n_comments = ROW_COUNT;
DELETE FROM ""BlogSpot""
WHERE ""AuthorId"" NOT IN (SELECT ""Id"" FROM ""AspNetUsers"");
GET DIAGNOSTICS n_posts = ROW_COUNT;
RAISE NOTICE 'EnforceBlogAuthorFKs: % orphaned comments deleted, % orphaned blog posts deleted',
n_comments, n_posts;
END $$;
");
migrationBuilder.DropForeignKey(
name: "FK_BlogSpot_AspNetUsers_AuthorId",
table: "BlogSpot");
migrationBuilder.DropForeignKey(
name: "FK_Comment_AspNetUsers_AuthorId",
table: "Comment");
migrationBuilder.AddForeignKey(
name: "FK_BlogSpot_AspNetUsers_AuthorId",
table: "BlogSpot",
column: "AuthorId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Comment_AspNetUsers_AuthorId",
table: "Comment",
column: "AuthorId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_BlogSpot_AspNetUsers_AuthorId",
table: "BlogSpot");
migrationBuilder.DropForeignKey(
name: "FK_Comment_AspNetUsers_AuthorId",
table: "Comment");
migrationBuilder.AddForeignKey(
name: "FK_BlogSpot_AspNetUsers_AuthorId",
table: "BlogSpot",
column: "AuthorId",
principalTable: "AspNetUsers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Comment_AspNetUsers_AuthorId",
table: "Comment",
column: "AuthorId",
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}

View file

@ -3803,7 +3803,8 @@ namespace Yavsc.Migrations
{
b.HasOne("Yavsc.Models.ApplicationUser", "Author")
.WithMany("Posts")
.HasForeignKey("AuthorId");
.HasForeignKey("AuthorId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Author");
});
@ -3830,9 +3831,9 @@ namespace Yavsc.Migrations
modelBuilder.Entity("Yavsc.Models.Blog.Comment", b =>
{
b.HasOne("Yavsc.Models.ApplicationUser", "Author")
.WithMany()
.WithMany("BlogComments")
.HasForeignKey("AuthorId")
.OnDelete(DeleteBehavior.Cascade)
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Yavsc.Models.Blog.Comment", "Parent")
@ -4542,6 +4543,8 @@ namespace Yavsc.Migrations
b.Navigation("BlackList");
b.Navigation("BlogComments");
b.Navigation("Book");
b.Navigation("Circles");