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
Een commentaarsysteem-onderdeel toevoegen 1 - Opstelling van de database
Monday, 26 August 2024
Inleiding
Eén van de belangrijkste aspecten van een blogsite als deze is een commentaarsysteem. Ik besloot er een te bouwen. Dit is het eerste deel van een reeks posten over hoe je een commentatuursysteem moet bouwen.
Opstelling van de Databasis
Zoals bij de rest van de site gebruik ik Postgres voor de blogs; om te beginnen met I' gebruik ik recursieve zoekopdrachten om de opmerkingen op te slaan . Ik vind dit erg ongemakkelijkM SK4 maar het haalt ons uit het raam met een gedraadde commentaarsysteem.
De EF-context
In het huidige systeem gebruiken we een 'Komment Closure' om de relaties tussen de opmerkingen te definiëren.
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
}
Je kunt hier zien dat elke CommentEntity heeft een PostId Voor de post zijn het's aanhangen aan, en een ParentCommentId Voor de commentaar is het 'aanhangend aan (indien er een is).. CommentClosure Tabel wordt gebruikt om de relaties tussen opmerkingen te slaan.
De CommentService
De gemeenschappelijke wetgeving van het land. CommentService Het is verantwoordelijk voor het beheren van opmerkingen. Het heeft methodes om aan te voegen , goedkeuring, afwijzenM SK3 en commentatjes te verwijderenMSC4 Het kent ook methodes voor het krijgen van commentaar voor een postMST5 en het krijgen vonden voor een commentaarMst6
Onze interface voor deze dienst ziet er zo uit:
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);
}
De Voeg-Methode
Onze Add-methode vat de structuur van dit commentaarsysteem samen.
Het begint eerst met een nieuwe transactie; voor de niet bekende transacties zijn een manier om ervoor te zorgen dat een groep operaties behandeld wordt als één enkele werkeenheid . Als één van de operaties mislukt, kan de transactie teruggedraaid wordenM SK3 en alle veranderingen worden ongedaan gezetMSC4 Dit is belangrijk wanneer je meerdere operaties hebt die samen moeten worden gedaanMST5 en je wilt ervoor zorgen dat ze allemaal samen succesvol of mislukkenMSP6
Het berekent dan de commentaartekst van Markdown naar HTML met behulp van de Markdig bibliotheek. Dit is een eenvoudige manier om gebruikers hun opmerkingen te laten formateren met Markdown.
Dan creëert het de entiteit en bewaart dat in de database.
Dit gebeurt in twee stappen: - eerst wordt de commentaar gestopt, dan is de commentaire afsluiting gestopt. Dit komt omdat het commentaar bewaard moet worden om de Id te genereren.
Dan bouwen we de hiërarchie van CommentClosures Die definiëren de relaties tussen de commentaties. We beginnen met een zelf-relatief afsluit-inskrywingM SK2 dan als er een ouderaanmerking is, halen we alle voorouders van de ouderaanhanging en voegen ze toe aan de nieuwe aanmerking.
Uiteindelijk, doen we de transactie af. Als een van de operaties mislukt, is de transaction teruggedraaid.
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;
}
De GetForPost-methode
We zullen de hele zaak niet behandelen, maar Add en Get zijn onze belangrijkste CRUD-operaties voor deze dienst.
Zoals je ziet hebben we een paging gebaseerd op de toplevel commentaar. We hebben ook een maxDepth parameter waarmee we de diepte van de commentaarboom kunnen beperken.. Dit is bruikbaar als we alleen de bovenste comments willen laten zien.
Onverwacht met deze recursieve aanpak moeten we de dieptefilter toepassen nadat we de opmerkingen hebben gehaald, zoals we dat kunnen doen in de zoekopdracht. Dit komt omdat de diepe van een commentaar bepaald wordt door het aantal voorouders dat hij heeft, en we dit niet makkelijk kunnen in SQL vragen.
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);
}
}
In slotting:
Dit is een eenvoudig getrainde commentaarsysteem dat recursieve zoekopdrachten gebruikt om de relaties tussen reacties op te slaan.. Het is niet het meest efficiënte systeem, maar het is simpel en werkt.
Tot dan toe, kan je een commentaar achterlaten onder