Back to "Aggiungere una parte del sistema di commenti 1 - Alzare la base di dati"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

Entity Framework

Aggiungere una parte del sistema di commenti 1 - Alzare la base di dati

Monday, 26 August 2024

Introduzione

Uno degli aspetti chiave di un sito di blog come questo è il sistema dei commenti. Quindi, ho deciso di costruirne unoM SK2 Questa è la prima parte di una serie di post su come costruire un sistema di commenti.

Alzare la base di dati

Come con il resto del sito io' uso Postgres per i blog; per cominciare con l'IM SK2 sto usando quei queri ripetitivi per immagazzinare i commenti . mi dispiace molto questoMSC4 ma ci tira fuori la porta con un sistema di commento threaded MST5 e non hoMst6c'è poco trafficoM st7 quindiMSt8non è una gran cosaMstr9 IoMt10 saremo spostati verso un sistema più efficiente nel futuroMtr11

Il contesto dell'EF

Nel sistema attuale usiamo un 'Comment Closure' per definire le relazioni tra i commenti - così posso supportare commenti threadedM SK3 Il nostro diagramma di classe assomiglia a questo

classDiagram
    class CommentEntity {
        int Id
        CommentStatus Status
        string Author
        string? HtmlContent
        string Content
        DateTime CreatedAt
        int PostId
        int? ParentCommentId
    }

    class BlogPostEntity {
        int Id
        string Title
        string Content
        DateTime CreatedAt
    }

    class CommentClosure {
        int AncestorId
        int DescendantId
        int Depth
    }

    CommentEntity "1" --> "1" BlogPostEntity : belongs to
    CommentEntity "1" --> "0..1" CommentEntity : parent
    CommentEntity "0..*" --> "0..*" CommentClosure : Ancestors
    CommentEntity "0..*" --> "0..*" CommentClosure : Descendants
    CommentClosure "1" --> "1" CommentEntity : Ancestor
    CommentClosure "1" --> "1" CommentEntity : Descendant

    class CommentStatus {
        <<enum>>
        Pending
        Approved
        Rejected
        Deleted
    }

Qui potete vedere che ogni CommentEntity ha una PostId per il posto sono attaccate alle stampe , e a ParentCommentId per il commento è 's attaccato a (se c'èM SK2 CommentClosure la tavola è usata per conservare le relazioni tra i commenti.

Il CommentService

L'informazione figura nella parte dispositiva. CommentService è responsabile della gestione dei commenti. Ha metodi per aggiungereMSC1 approvare, rifiutareM SK3 e cancellare i commenti

L'interfaccia per questo servizio assomiglia a questa.

public interface ICommentService
{
    Task<string> Add( int postId, int? parentCommentId, string author, string content);
    Task<List<CommentEntity>> GetForPost(int blogPostId, int page = 1, int pageSize = 10, int? maxDepth = null, CommentStatus? status = null);
    Task<List<CommentEntity>> GetDescendants(int commentId, int maxDepth = 0);

     Task<CommentEntity> Get(int commentId);
    Task<List<CommentEntity>> GetAncestors(int commentId);
    Task Delete(int commentId);
    Task Reject(int commentId);
    Task Approve(int commentId);
}

Il metodo "Add"

Il nostro metodo Add riassume la struttura di questo sistema di commenti.

Inizialmente inizia una nuova transazione; per quelle transazioni non familiari è un modo di assicurare che un gruppo di operazioni venga trattato come un'unica unità di lavoro. Se una delle operazioni fallisceM SK2 la transazione può essere riprodurreMSC3 e tutti i cambiamenti sono cancellatiMST4 Questo è importante quando avete molte operazioni che devono essere compiute insiemeMst5 e volete assicurarvi che tutte abbiano successo o falliscono insieme MST6

Poi analizza il testo dei commenti da Markdown in HTML usando la Markdig library. È un modo semplice per permettere agli utenti di formatare i loro commenti usando Markdown.

Poi crea l'entità e la conserva nella base di dati.

Questo è fatto in due fasi - prima il commento viene salvato, poi la chiusura del commento è salvataM SK2 Questo perché il commente deve essere salvato per generare l'IDMSC3 che viene usato nella chiusuraMNK4

Poi costruiamo la gerarchia di CommentClosures che definiscono le relazioni tra i commenti. Cominciamo con un auto-intry di riferimento alla chiusuraM SK2 poi, se c'è un commento parente, preleviamo tutti gli antenati del commento Parente e li aggiungiamo al nuovo commento.

Infine, commettiamo la transazione. Se una delle operazioni fallisceM SK2 l'operazione viene riprodurreMSC3

 public async Task<string> Add(int postId, int? parentCommentId, string author, string content)
  {
      await using var transaction = await context.Database.BeginTransactionAsync();
      try
      {
         var html = Markdig.Markdown.ToHtml(content);
          // Create the new comment
          var newComment = new CommentEntity()
          {
              HtmlContent = html,
              Content = content,
              CreatedAt = DateTime.UtcNow,
              PostId = postId,
              Author = author,
              Status = CommentStatus.Pending,
              ParentCommentId = parentCommentId
          };
            
          context.Comments.Add(newComment);
          await context.SaveChangesAsync();
          logger.LogInformation("Saved comment to DB");// Save to generate the new comment's Id

          // Insert into CommentClosure table
          var commentClosures = new List<CommentClosure>();

          // Self-referencing closure entry
          commentClosures.Add(new CommentClosure
          {
              AncestorId = newComment.Id,
              DescendantId = newComment.Id,
              Depth = 0
          });

          // If there is a parent comment, insert the ancestor relationships
          if (parentCommentId.HasValue)
          {
              // Fetch all ancestors of the parent comment
              var parentAncestors = await context.CommentClosures
                  .Where(cc => cc.DescendantId == parentCommentId.Value)
                  .ToListAsync();

              // Add ancestor relationships for the new comment
              foreach (var ancestor in parentAncestors)
              {
                  commentClosures.Add(new CommentClosure
                  {
                      AncestorId = ancestor.AncestorId,
                      DescendantId = newComment.Id,
                      Depth = ancestor.Depth + 1
                  });
              }

              // Add a direct parent-child relationship
              commentClosures.Add(new CommentClosure
              {
                  AncestorId = parentCommentId.Value,
                  DescendantId = newComment.Id,
                  Depth = 1
              });
          }

          context.CommentClosures.AddRange(commentClosures);
          await context.SaveChangesAsync();
          logger.LogInformation("Saved comment closure to DB");

          // Commit transaction
          await transaction.CommitAsync();
          return html;
      }
      catch (Exception e)
      {
          // Rollback transaction in case of failure
          await transaction.RollbackAsync();
          logger.LogError(e, "Failed to save comment to DB");
      }

      return string.Empty;
  }

Il metodo GetForPost

Noi non ricopriremo l'intera cosa, ma Add e Get sono le nostre principali operazioni CRUD per questo servizio.

Come potete vedere abbiamo dei messaggi basati sul commento di alto livello. Abbiamo anche un maxDepth parametro che ci permette di limitare la profondità dell'albero dei commenti. Questo è utile se vogliamo mostrare solo i commenti a livello superioreM SK1, o se vogliamo limitarla per migliorare la performance.

Con questo approccio recursivo dobbiamo applicare il filtro di profondità dopo aver prelevato i commenti.

  public async Task<List<CommentEntity>> GetForPost(int blogPostId, int page = 1, int pageSize = 10, int? maxDepth = null, CommentStatus? status = null)
  {
      // Step 1: Query the top-level comments for the specified blog post
      var query = context.Comments
          .Where(c => c.PostId == blogPostId)
          .OrderByDescending(c => c.CreatedAt)
          .Skip((page - 1) * pageSize)
          .Take(pageSize);

      // Step 2: Filter by status if provided
      if (status.HasValue)
      {
          query = query.Where(c => c.Status == status.Value);
      }

      var topLevelComments = await query
          .Include(c => c.ParentComment)
          .Include(d=>d.Descendants)
          .ToListAsync();

      // Step 4: Filter descendants based on the maxDepth
      foreach (var comment in topLevelComments)
      {
          if (maxDepth != null)
          {
              FilterDescendantsByDepth(comment, 0, maxDepth.Value);
          }
      }

      return topLevelComments;
  }

// Recursive helper method to limit the descendants based on the specified depth
  private void FilterDescendantsByDepth(CommentEntity comment, int currentDepth, int maxDepth)
  {
      if (currentDepth >= maxDepth)
      {
          // If the max depth is reached or there are no descendants, stop recursion
          comment.Descendants = new List<CommentClosure>();  // Clear further descendants beyond maxDepth
          return;
      }

      foreach (var closure in comment.Descendants.ToList())  // Iterate over a copy to prevent modification during iteration
      {
          FilterDescendantsByDepth(closure.Descendant, currentDepth + 1, maxDepth);
      }
  }

Conclusione

Questo è un semplice sistema di commenti threaded che usa quei queri ricursivi per immagazzinare le relazioni tra i commenti. E'' non il sistema più efficienteM SK2 ma è ' semplice e funzionaMSC4 In futuroMST5 IMSSK6 coprerò gli aspetti frontali di questo sistemaMSV7 tanti HTMXMSP8 Alpine MSV9js M SV10 e Tailwind CSSM SV11

Fino a quel momento, lasciate il vostro commento qui sotto!

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.