fix/issue-3-splitquery #5

Merged
notazof merged 8 commits from fix/issue-3-splitquery into main 2026-07-12 02:47:16 +01:00
5 changed files with 4793 additions and 3 deletions
Showing only changes of commit 98613e7070 - Show all commits

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é.
Lum 2026-07-11 18:45:27 +01:00

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");

View file

@ -214,6 +214,22 @@ namespace Yavsc.Models
// Log immuable — pas de update autorisé
e.ToTable(tb => tb.HasCheckConstraint("CK_ModerationLog_Immutable", "1=1"));
});
// ── Blog FK strictness ─────────────────────────────────────────────
// Tout billet a un auteur, tout commentaire a un auteur : pas de
// cascade en suppression d'un user, pas d'orphelin toléré. Le code
// applicatif (BlogSpotService.Details) s'appuie sur cette
// contrainte pour pouvoir assumer l'existence de l'auteur.
builder.Entity<BlogPost>()
.HasOne(b => b.Author)
.WithMany(u => u.Posts)
.HasForeignKey(b => b.AuthorId)
.OnDelete(DeleteBehavior.Restrict);
builder.Entity<Comment>()
.HasOne(c => c.Author)
.WithMany(u => u.BlogComments)
.HasForeignKey(c => c.AuthorId)
.OnDelete(DeleteBehavior.Restrict);
}
/// <summary>

View file

@ -114,6 +114,13 @@ namespace Yavsc.Models
[InverseProperty("Member")]
public virtual List<CircleMember>? Membership { get; set; }
/// <summary>
/// User's blog comments
/// </summary>
[JsonIgnore]
[InverseProperty("Author")]
public virtual List<Blog.Comment>? BlogComments { get; set; }
IAccountBalance? IApplicationUser.AccountBalance => AccountBalance;
ILocation? IApplicationUser.PostalAddress { get => PostalAddress; }