Στοιχεία Ιεραρχίες Μέρος 1.5: PostgreSQL ltree με πυρήνα EF (ελληνικά (Greek))

Στοιχεία Ιεραρχίες Μέρος 1.5: PostgreSQL ltree με πυρήνα EF

Saturday, 06 December 2025

//

15 minute read

Η επέκταση Ltree του PostgreSQL σας δίνει υλοποιημένες διαδρομές με υπερδυνάμεις βάσης δεδομένων: δείκτες GIST, εξειδικευμένοι φορείς όπως @> και <@Αν είστε αφοσιωμένοι στο PostgreSQL και θέλετε την καλύτερη απόδοση ερώτημα ιεραρχίας, ltree είναι δύσκολο να νικήσει.

Καλά νέα: Η Ο πάροχος Npgsql EF Core υποστηρίζει τις μεταφράσεις LINQ για τις λειτουργίες ltree μέσω του LTree type. Μπορείτε να χρησιμοποιήσετε μεθόδους όπως IsAncestorOf(), IsDescendantOf(), και MatchesLQuery() Ωστόσο, το EF Core δεν υποστηρίζει ακόμα επαναλαμβανόμενα CTE, οπότε θα χρειαστείτε ωμό SQL για λειτουργίες που τα απαιτούν (όπως την κατασκευή πλήρων αποτελεσμάτων subtree με υπολογισμένα βάθη).

Χάρη στην Σέι ΡογιάνσκιCity name (optional, probably does not need a translation) για την επισήμανση της υποστήριξης μετάφρασης LINQ!

Πλοήγηση σειράς


Τι είναι ltree;

ltree είναι μια επέκταση PostgreSQL που παρέχει έναν εγγενή τύπο δεδομένων για ιεραρχικά μονοπάτια ετικετών. Υλοποιημένη διαδρομή με υπερδυνάμεις - η βάση δεδομένων κατανοεί τη δομή και παρέχει βελτιστοποιημένους χειριστές, λειτουργίες, και υποστήριξη δείκτη GiST.

Αντί να αντιμετωπίζει το μονοπάτι ως μια χαζή συμβολοσειρά και χρησιμοποιώντας LIKE ερωτήματα, PostgreSQL μπορεί:

  • Χρήση εξειδικευμένων φορέων εκμετάλλευσης (@> γιατί "είναι πρόγονος" <@ γιατί "είναι απόγονος του")
  • Εφαρμογή δεικτών GIST για αποτελεσματικά ερωτήματα ιεραρχίας
  • Μοτίβα ταιριάσματος με wildcards (Top.*.Europe)
  • Εκτελέστε λειτουργίες συνόλου σε μονοπάτια

Βασική διορατικότητα: Το ltree είναι το καλύτερο και των δύο κόσμων - η απλότητα των υλοποιημένων μονοπατιών με τη βελτιστοποίηση της βάσης δεδομένων. Η ανταλλαγή είναι PostgreSQL κλειδαριά, και ενώ πολλές επιχειρήσεις ltree λειτουργούν μέσω LINQ, reversive CTEs εξακολουθούν να απαιτούν ωμά SQL.

Μορφή διαδρομής ltree

Μονοπάτια σε περιόδους χρήσης ltree ως διαχωριστές και αλφαριθμητικές ετικέτες:

Top.Countries.Europe.UK
Top.Countries.Asia.Japan.Tokyo
Top.Products.Electronics.Computers.Laptops

Κανόνες:

  • Οι ετικέτες μπορούν να περιέχουν γράμματα, ψηφία και επισημάνσεις
  • Οι ετικέτες είναι ευαίσθητες στην περίπτωση
  • Το μέγιστο μήκος ετικέτας είναι 256 χαρακτήρες
  • Το μέγιστο μήκος διαδρομής είναι 65535 ετικέτες

Για τα συστήματα σχολίων, θα χρησιμοποιούσαμε ταυτότητες ως ετικέτες: 1.3.7 που σημαίνει "Σχόλιο 7 στο σχόλιο 3 στο σχόλιο 1."

Ρύθμιση του δέντρου

Πρώτον, ενεργοποιήστε την επέκταση (απαιτεί δικαιώματα υπερχρήστη βάσης δεδομένων):

CREATE EXTENSION IF NOT EXISTS ltree;

Ή μέσω της μετανάστευσης πυρήνα EF:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Sql("CREATE EXTENSION IF NOT EXISTS ltree");
}

Ορισμός οντότητας

Ο πάροχος Npgsql περιλαμβάνει ένα LTree type that maps directly to PostgreSQL's ltree and provides LINQ-translatable methods:

using Microsoft.EntityFrameworkCore;

public class Comment
{
    public int Id { get; set; }
    public string Content { get; set; } = string.Empty;
    public string Author { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }

    public int PostId { get; set; }
    public BlogPost Post { get; set; } = null!;

    // ========== LTREE PATH ==========

    // The hierarchical path in ltree format
    // Format: ancestor1.ancestor2.thisNode
    // Examples:
    //   Root comment: "1"
    //   Child of 1: "1.5"
    //   Grandchild: "1.5.12"
    //
    // Using the LTree type enables LINQ translations for ltree operators
    public LTree Path { get; set; }

    // Keep ParentCommentId for convenience
    public int? ParentCommentId { get; set; }
    public Comment? ParentComment { get; set; }
    public ICollection<Comment> Children { get; set; } = new List<Comment>();

    // ========== HELPER METHODS ==========

    // Helper to get depth - LTree has NLevel property for this
    public int GetDepth() => Path.NLevel - 1;

    public IEnumerable<int> GetAncestorIds()
    {
        var pathString = Path.ToString();
        if (string.IsNullOrEmpty(pathString)) yield break;

        var parts = pathString.Split('.');
        // All except last (which is this node)
        for (int i = 0; i < parts.Length - 1; i++)
        {
            if (int.TryParse(parts[i], out var id))
                yield return id;
        }
    }
}

Ρύθμιση πυρήνα EF

public class CommentConfiguration : IEntityTypeConfiguration<Comment>
{
    public void Configure(EntityTypeBuilder<Comment> builder)
    {
        builder.HasKey(c => c.Id);

        builder.Property(c => c.Content)
            .IsRequired()
            .HasMaxLength(10000);

        builder.Property(c => c.Author)
            .IsRequired()
            .HasMaxLength(200);

        // ========== PATH COLUMN ==========
        // The LTree type is automatically mapped to PostgreSQL's ltree type
        // by the Npgsql provider - no explicit column type needed
        builder.Property(c => c.Path)
            .IsRequired();

        // Relationship to blog post
        builder.HasOne(c => c.Post)
            .WithMany(p => p.Comments)
            .HasForeignKey(c => c.PostId)
            .OnDelete(DeleteBehavior.Cascade);

        // Self-referencing
        builder.HasOne(c => c.ParentComment)
            .WithMany(c => c.Children)
            .HasForeignKey(c => c.ParentCommentId)
            .OnDelete(DeleteBehavior.Restrict);

        // Standard indexes
        builder.HasIndex(c => c.PostId);
        builder.HasIndex(c => c.ParentCommentId);
    }
}

Προσθέστε τον δείκτη GIST μέσω της μετανάστευσης:

protected override void Up(MigrationBuilder migrationBuilder)
{
    // GiST index for ltree - enables efficient @>, <@, and ~ operators
    migrationBuilder.Sql(
        "CREATE INDEX ix_comments_path_gist ON comments USING GIST (path)");

    // Alternative: B-tree index for exact match and sorting
    // migrationBuilder.Sql(
    //     "CREATE INDEX ix_comments_path_btree ON comments USING BTREE (path)");
}

Χειριστές ltree

Το ltree παρέχει ισχυρούς χειριστές. Ο πάροχος Npgsql EF Core μεταφράζει LTree μέθοδοι για τους εν λόγω φορείς εκμετάλλευσης:

Ο χειριστής εννοεί τη μέθοδο LINQ SQL Παράδειγμα |----------|---------|-------------|-------------| | @> Είναι πρόγονος (περιέχει) ltree1.IsAncestorOf(ltree2) | '1.3'::ltree @> '1.3.7'::ltree → αληθινό | <@ Είναι απόγονος του (που περιέχεται από) ltree1.IsDescendantOf(ltree2) | '1.3.7'::ltree <@ '1.3'::ltree → αληθινό | ~ Ταιριάζει με το μοτίβο lquery ltree.MatchesLQuery(pattern) | '1.3.7'::ltree ~ '1.*'::lquery → αληθινό | @ Σπίρτα ltxtquery ltree.MatchesLTxtQuery(query) | '1.3.7'::ltree @ '3 & 7'::ltxtquery → αληθινό | || Ολοκληρωτικά μονοπάτια (χρησιμοποιήστε τη συμβολοσφραγίδα) '1.3'::ltree || '7'::ltree → '1.3.7' | | <, >, <=, >= □ Σύγκριση □ Τυπικοί χειριστές για τη διαλογή

Πρόσθετες ιδιότητες και μέθοδοι μεταφράσιμων LINQ:

  • ltree.NLevelnlevel(ltree) - αριθμός ετικετών στη διαδρομή
  • ltree.Subtree(start, end)subltree(ltree, start, end) - σειρά εκχυλισμάτων ετικετών
  • ltree.Subpath(offset)subpath(ltree, offset) - επίθημα από όφσετ
  • ltree.Subpath(offset, len)subpath(ltree, offset, len) - substring
  • ltree.Index(subpath)index(ltree, subpath) - βρείτε υποπαθητική θέση
  • LTree.LongestCommonAncestor(ltree1, ltree2)lca(ltree1, ltree2) - χαμηλότερος κοινός πρόγονος

Πράξεις

Εισαγωγή ενός νέου σχολίου

public async Task<Comment> AddCommentAsync(
    int postId,
    int? parentId,
    string author,
    string content,
    CancellationToken ct = default)
{
    string path;

    if (parentId.HasValue)
    {
        // Get parent's path
        var parentPath = await context.Comments
            .Where(c => c.Id == parentId.Value)
            .Select(c => c.Path)
            .FirstOrDefaultAsync(ct);

        if (parentPath == null)
            throw new InvalidOperationException($"Parent comment {parentId} not found");

        // Create comment first to get the ID
        var comment = new Comment
        {
            PostId = postId,
            ParentCommentId = parentId,
            Author = author,
            Content = content,
            CreatedAt = DateTime.UtcNow,
            Path = string.Empty  // Temporary
        };

        context.Comments.Add(comment);
        await context.SaveChangesAsync(ct);

        // Build path: parentPath.newId
        // ltree uses periods as separators
        comment.Path = $"{parentPath}.{comment.Id}";
        await context.SaveChangesAsync(ct);

        logger.LogInformation("Added comment {CommentId} with ltree path {Path}",
            comment.Id, comment.Path);
        return comment;
    }
    else
    {
        // Root comment - path is just the ID
        var comment = new Comment
        {
            PostId = postId,
            ParentCommentId = null,
            Author = author,
            Content = content,
            CreatedAt = DateTime.UtcNow,
            Path = string.Empty
        };

        context.Comments.Add(comment);
        await context.SaveChangesAsync(ct);

        comment.Path = comment.Id.ToString();
        await context.SaveChangesAsync(ct);

        return comment;
    }
}

Αποκτήστε Άμεσα Παιδιά

Χρησιμοποιώντας ParentCommentId (απλό) ή ltree μοτίβο ταιριάζουν:

public async Task<List<Comment>> GetChildrenAsync(int commentId, CancellationToken ct = default)
{
    // Option 1: Simple ParentCommentId lookup
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.ParentCommentId == commentId)
        .OrderBy(c => c.CreatedAt)
        .ToListAsync(ct);
}

// Option 2: Using ltree pattern (demonstration)
public async Task<List<Comment>> GetChildrenLtreeAsync(int commentId, CancellationToken ct = default)
{
    // Get parent path first
    var parentPath = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (parentPath == null)
        return new List<Comment>();

    // Children match pattern: parentPath.*{1}
    // The {1} means exactly one more label (immediate children only)
    var sql = @"
        SELECT * FROM comments
        WHERE path ~ ($1 || '.*{1}')::lquery
        ORDER BY created_at";

    return await context.Comments
        .FromSqlRaw(sql, parentPath)
        .AsNoTracking()
        .ToListAsync(ct);
}

Πάρτε όλους τους προγόνους

Χρήση του LINQ με το IsAncestorOf μέθοδος (μεταφράζεται σε @> Χειριστής:

public async Task<List<Comment>> GetAncestorsAsync(int commentId, CancellationToken ct = default)
{
    var targetPath = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (targetPath == default)
        return new List<Comment>();

    // Find all nodes whose path is an ancestor of this path
    // Using IsAncestorOf which translates to @> operator
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.IsAncestorOf(targetPath) && c.Id != commentId)
        .OrderBy(c => c.Path.NLevel)
        .ToListAsync(ct);
}

Πάρτε όλους τους Απόγονους

Χρήση του LINQ με το IsDescendantOf μέθοδος (μεταφράζεται σε <@ Χειριστής:

public async Task<List<Comment>> GetDescendantsAsync(int commentId, CancellationToken ct = default)
{
    var parentPath = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (parentPath == default)
        return new List<Comment>();

    // Find all nodes whose path is a descendant of this path
    // Using IsDescendantOf which translates to <@ operator
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.IsDescendantOf(parentPath) && c.Id != commentId)
        .OrderBy(c => c.Path)
        .ToListAsync(ct);
}

Πάρτε απόγονοι στο μέγιστο βάθος

Χρήση του LINQ με NLevel για τον περιορισμό του βάθους:

public async Task<List<Comment>> GetDescendantsToDepthAsync(
    int commentId,
    int maxDepth,
    CancellationToken ct = default)
{
    var comment = await context.Comments
        .FirstOrDefaultAsync(c => c.Id == commentId, ct);

    if (comment == null)
        return new List<Comment>();

    var basePath = comment.Path;
    var baseLevel = comment.Path.NLevel;

    // NLevel property translates to nlevel() function
    // Filter descendants within maxDepth levels
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.IsDescendantOf(basePath) 
                 && c.Id != commentId
                 && c.Path.NLevel - baseLevel <= maxDepth)
        .OrderBy(c => c.Path)
        .ToListAsync(ct);
}

// If you need the depth value in results, you can project it:
public async Task<List<CommentWithDepth>> GetDescendantsWithDepthAsync(
    int commentId,
    int maxDepth,
    CancellationToken ct = default)
{
    var comment = await context.Comments
        .FirstOrDefaultAsync(c => c.Id == commentId, ct);

    if (comment == null)
        return new List<CommentWithDepth>();

    var basePath = comment.Path;
    var baseLevel = comment.Path.NLevel;

    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.IsDescendantOf(basePath) 
                 && c.Id != commentId
                 && c.Path.NLevel - baseLevel <= maxDepth)
        .OrderBy(c => c.Path)
        .Select(c => new CommentWithDepth
        {
            Id = c.Id,
            Content = c.Content,
            Author = c.Author,
            CreatedAt = c.CreatedAt,
            PostId = c.PostId,
            ParentCommentId = c.ParentCommentId,
            Path = c.Path.ToString(),
            Depth = c.Path.NLevel - baseLevel
        })
        .ToListAsync(ct);
}

Ερωτήματα Ταιριάζουν με το Μοτίβο

Itree υποστηρίζει ισχυρά μοτίβα lquery. MatchesLQuery στο LINQ:

// Find all comments at exactly depth 2 under comment 1
public async Task<List<Comment>> GetAtDepthAsync(int commentId, int depth, CancellationToken ct = default)
{
    var path = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (path == default) return new List<Comment>();

    // Pattern: path.*{depth} matches exactly 'depth' more levels
    var pattern = $"{path}.*{{{depth}}}";
    
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.MatchesLQuery(pattern))
        .OrderBy(c => c.Path)
        .ToListAsync(ct);
}

// Find all paths matching a pattern like "1.*.7" (any path through 1 ending in 7)
public async Task<List<Comment>> MatchPatternAsync(string pattern, CancellationToken ct = default)
{
    // MatchesLQuery translates to the ~ operator
    return await context.Comments
        .AsNoTracking()
        .Where(c => c.Path.MatchesLQuery(pattern))
        .OrderBy(c => c.Path)
        .ToListAsync(ct);
}

Διαγραφή ενός υποδέντρου

Μπορείτε να χρησιμοποιήσετε το LINQ για να επιλέξετε το subtree και στη συνέχεια να διαγράψετε:

public async Task DeleteSubtreeAsync(int commentId, CancellationToken ct = default)
{
    var path = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (path == default)
        throw new InvalidOperationException($"Comment {commentId} not found");

    // Delete all descendants (nodes where path is descendant of this path)
    // Note: ExecuteDeleteAsync requires EF Core 7+
    var deleted = await context.Comments
        .Where(c => c.Path.IsDescendantOf(path))
        .ExecuteDeleteAsync(ct);

    logger.LogInformation("Deleted {Count} comments with path prefix {Path}", deleted, path);
}

Μετακινήστε ένα Subtree

Itree παρέχει λειτουργίες για να βοηθήσει με τη χειραγώγηση της διαδρομής:

public async Task MoveSubtreeAsync(
    int commentId,
    int newParentId,
    CancellationToken ct = default)
{
    await using var transaction = await context.Database.BeginTransactionAsync(ct);

    try
    {
        var node = await context.Comments.FirstOrDefaultAsync(c => c.Id == commentId, ct);
        var newParent = await context.Comments.FirstOrDefaultAsync(c => c.Id == newParentId, ct);

        if (node == null || newParent == null)
            throw new InvalidOperationException("Node or parent not found");

        // Prevent cycles
        if (newParent.Path.StartsWith(node.Path))
            throw new InvalidOperationException("Cannot move under own descendant");

        var oldPath = node.Path;
        var newPath = $"{newParent.Path}.{node.Id}";

        // Update all descendants: replace old path prefix with new one
        // subpath(path, nlevel(oldPath)) gets the suffix after oldPath
        // We concatenate newPath with that suffix
        var sql = @"
            UPDATE comments
            SET path = $2::ltree || subpath(path, nlevel($1::ltree))
            WHERE path <@ $1::ltree";

        await context.Database.ExecuteSqlRawAsync(
            sql,
            new object[] { oldPath, newPath },
            ct);

        // Update parent reference
        node.ParentCommentId = newParentId;
        await context.SaveChangesAsync(ct);

        await transaction.CommitAsync(ct);

        logger.LogInformation("Moved subtree from {OldPath} to {NewPath}", oldPath, newPath);
    }
    catch
    {
        await transaction.RollbackAsync(ct);
        throw;
    }
}

Αναφορά λειτουργιών ltree

PostgreSQL παρέχει πολλές χρήσιμες λειτουργίες ltree:

Δραστηριότητα & λειτουργία Περιγραφή Παράδειγμα & |----------|-------------|---------| | nlevel(ltree) Αριθμός ετικετών nlevel('1.3.7') → 3 | | subpath(ltree, offset) Επιφάνεια από όφσετ subpath('1.3.7', 1) → '3.7' | | subpath(ltree, offset, len) Υποδομές subpath('1.3.7', 1, 1) → '3' | | subltree(ltree, start, end) Εύρος ετικετών subltree('1.3.7', 0, 2) → '1.3' | | lca(ltree, ltree) Ο πιο χαμηλός κοινός πρόγονος lca('1.3.7', '1.3.9') → '1.3' | | text2ltree(text) Μετάτρεψε το κείμενο σε ltree text2ltree('1.3.7') | | ltree2text(ltree) Μετάτρεψε ltree στο κείμενο ltree2text('1.3.7'::ltree) |

Οραματισμός Ροής Ερωτήματος

sequenceDiagram
    participant App as Application
    participant EF as EF Core
    participant PG as PostgreSQL + ltree

    Note over App,PG: Getting Descendants (GiST index)
    App->>EF: GetDescendantsAsync(commentId)
    EF->>PG: SELECT path FROM comments WHERE id = @id
    PG-->>EF: Path "1.3"
    EF->>PG: SELECT * FROM comments WHERE path <@ '1.3'::ltree
    Note over PG: Uses GiST index - O(log n)
    PG-->>EF: All descendants
    EF-->>App: List<Comment>

    Note over App,PG: Pattern Match Query
    App->>EF: MatchPatternAsync("1.*.7")
    EF->>PG: SELECT * FROM comments WHERE path ~ '1.*.7'::lquery
    Note over PG: GiST index supports pattern matching
    PG-->>EF: Matching comments
    EF-->>App: List<Comment>

Χαρακτηριστικά επιδόσεων

Η λειτουργία είναι πολύπλοκη σημειώσεις |-----------|------------|-------| Εισάγετε το ένθετο Ο (1) Απλά ορίστε τη συμβολοσειρά του μονοπατιού □ Αποκτήστε τα παιδιά □ O Πάρτε τους προγόνους σας @> χειριστής με δείκτη GIST Πάρτε τους απογόνους σας @@ operator with GIST index Ταίριασμα μοτίβο ~ O(log n) ~ Ο δείκτης GiST υποστηρίζει lquery ~ Μετακίνηση subtree O(s) Επικαιροποίηση απόγονων μονοπατιών Διαγράψτε το subtree O

Με GIST δείκτες, ltree ερωτήματα είναι εξαιρετικά αποτελεσματική - συνήθως O(log n) ανεξάρτητα από το βάθος των δέντρων.

Πλεονεκτήματα και Κατάρες

Πως θα το κάνουμε αυτό; |------|------| Βελτιστοποίηση βάσης δεδομένων μόνο PostgreSQL Δείκτης GIST για όλα τα ερωτήματα ιεραρχίας Δυνατό μοτίβο που ταιριάζει με τις ετικέτες που περιορίζονται σε αλφαριθμητικά Ενσωματωμένες λειτουργίες χειρισμού μονοπατιών Οι αναδρομικές CTE απαιτούν ωμό SQL Ο (1) πρόγονος/απόγονος ερωτήματα Λιγότερο φορητές από τις καθαρές λύσεις πυρήνα EF

  • Συμπαγής αποθήκευση Υποστήριξη LINQ μέσω Npgsql LTree type

Πότε να χρησιμοποιήσετε ltree

Επιλέξτε ltree όταν:

  • Είσαι αφοσιωμένος στο PostgreSQL
  • Η απόδοση είναι κρίσιμη για ερωτήματα ιεραρχίας
  • Χρειάζεστε ταίριασμα μοτίβο (βρείτε όλα τα X.*.Y μονοπάτια)
  • Θέλετε το καλύτερο των υλοποιημένων μονοπατιών
  • Θέλετε υποστήριξη LINQ για τις περισσότερες επιχειρήσεις ιεραρχίας

Αποφύγετε το δέντρο όταν:

  • Χρειάζεστε φορητότητα βάσης δεδομένων (SQL Server, MySQL, κλπ.)
  • Η ομάδα σας δεν είναι εξοικειωμένη με τις επεκτάσεις PostgreSQL
  • Οι ετικέτες χρειάζονται μη-αλφαβητικούς χαρακτήρες
  • Χρειάζεστε αναδρομικά CTE και θέλετε να αποφύγετε οποιαδήποτε ωμή SQL

Σύγκριση με την Υλοποιημένη Διαδρομή

Φανταστείτε την Υλοποιημένη Διαδρομή |--------|-------------------|-------| Δείκτης τύπου Δ-Δέντρο (μόνο πρόθεμα) Γκιστ (όλα τα μοτίβα) Μοιάζει με 'πρόθεμα%' μόνο με πλήρη wildcards Συγκριτική συμβολοσειράς Native @>, <@, ~ Οποιαδήποτε βάση δεδομένων Μόνο PostgreSQL Στήριγμα πυρήνα EF Πλήρες LINQ LINQ μέσω LTree τύπος (CTEs χρειάζονται ακατέργαστο SQL) □ Εντάξει με το ευρετήριο. Εξαιρετικά με το GIST. > Λειτουργίες > Καμία (εγχειρητική ανάλυση) > Πλούσια βιβλιοθήκη λειτουργιών

Παράδειγμα: Πλήρης ερώτηση δέντρου

Βάζοντας όλα μαζί - πάρτε ένα ολόκληρο δέντρο σχολίων με βάθος για ένα blog post:

public async Task<List<CommentTreeItem>> GetPostCommentTreeAsync(
    int postId,
    int maxDepth = 5,
    CancellationToken ct = default)
{
    // Get all comments for the post with calculated depth
    // nlevel() counts the labels in the path
    var sql = @"
        WITH root_comments AS (
            -- Find root comments for this post (no dot in path = root)
            SELECT path, nlevel(path) as root_level
            FROM comments
            WHERE post_id = $1 AND path !~ '*.*'
        )
        SELECT
            c.id,
            c.content,
            c.author,
            c.created_at,
            c.post_id,
            c.parent_comment_id,
            c.path::text as path,
            nlevel(c.path) - COALESCE(
                (SELECT root_level FROM root_comments r
                 WHERE c.path <@ r.path
                 ORDER BY nlevel(r.path) DESC LIMIT 1),
                nlevel(c.path)
            ) as depth
        FROM comments c
        WHERE c.post_id = $1
          AND nlevel(c.path) <= $2 + 1  -- +1 because depth is 0-indexed
        ORDER BY c.path";  -- Perfect depth-first order!

    return await context.Database
        .SqlQueryRaw<CommentTreeItem>(sql, postId, maxDepth)
        .ToListAsync(ct);
}

public class CommentTreeItem
{
    public int Id { get; set; }
    public string Content { get; set; } = string.Empty;
    public string Author { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public int PostId { get; set; }
    public int? ParentCommentId { get; set; }
    public string Path { get; set; } = string.Empty;
    public int Depth { get; set; }
}

Πλοήγηση σειράς

Ποιο είναι το επόμενο;

Αυτή η σειρά έχει καλύψει πέντε προσεγγίσεις σε ιεραρχικά δεδομένα χρησιμοποιώντας EF Core. Μέρος 2 θα εξερευνήσει χρησιμοποιώντας ωμό SQL και Dapper για ακόμα περισσότερο έλεγχο των ερωτημάτων ιεραρχίας - έρχονται σύντομα!

Finding related posts...
logo

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