Aggiungere una parte del sistema di commenti 1 - Alzare la base di dati
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!