Back to "Στοιχεία Ιεραρχίες Μέρος 1.3: Υλοποιημένη Διαδρομή με πυρήνα EF"

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

EF Hierarchies Entity Framework PostgreSQL

Στοιχεία Ιεραρχίες Μέρος 1.3: Υλοποιημένη Διαδρομή με πυρήνα EF

Saturday, 06 December 2025

Υλοποιημένα μονοπάτια αποθηκεύουν την πλήρη καταγωγή ως μια περιωρισμένη συμβολοσειρά - όπως /1/3/7/ Ιδανικό για την παραγωγή φρυγανιάς και την αποσφαλμάτωση, αν και κινούμενα υποδεέστερα σημαίνει ενημέρωση της συμβολοσειράς του κάθε απόγονου.

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


Τι είναι το Υλοποιημένο Μονοπάτι;

Το Materialised μοτίβο διαδρομής (αποκαλείται επίσης "Path Equerization" in Τα Δέντρα και οι Ιεραρχίες του Τζο Κέλκο) αποθηκεύει την πλήρη καταγωγή του κάθε κόμβου ως μια περιορισμένη συμβολοσειρά - όπως ένα μονοπάτι αρχείο ή ταχυδρομική διεύθυνση. Αντί της αποθήκευσης ακριβώς "ο γονιός μου είναι κόμβος 5," αποθηκεύουμε "Είμαι επιτευχθεί μέσω κόμβους 1 → 3 → 5 → 7" άμεσα στη σειρά.

Σκεφτείτε το σαν να αποθηκεύετε το πλήρες URL αντί μόνο το όνομα της σελίδας. /blog/posts/2024/my-article Σου λέει ακριβώς πού είσαι στην ιεραρχία, δεν χρειάζεται να ψάξεις.

Βασική διορατικότητα: Η καταγωγή αποκανονίζεται σε κάθε σειρά, αλλά αυτό κάνει τις ερωτήσεις των προγόνων ασήμαντες.

Η Έννοια Οραματίστηκε

flowchart TD
    subgraph "Comment Tree"
        C1["Comment 1<br/>Path: /1/"]
        C2["Comment 2<br/>Path: /1/2/"]
        C3["Comment 3<br/>Path: /1/3/"]
        C4["Comment 4<br/>Path: /1/3/4/"]
    end

    C1 --> C2
    C1 --> C3
    C3 --> C4

    subgraph "What the paths tell us"
        P1["Comment 4's path /1/3/4/ means:<br/>• Ancestors are 1, 3 (parse the path)<br/>• Depth is 3 (count separators - 1)<br/>• Root is 1 (first element)"]
    end

    style C1 stroke:#6366f1,stroke-width:2px
    style C2 stroke:#8b5cf6,stroke-width:2px
    style C3 stroke:#8b5cf6,stroke-width:2px
    style C4 stroke:#a855f7,stroke-width:2px

Το μονοπάτι είναι αυτοαποκαλούμενο:

  • Διαβάζοντας τους προγόνους: Αναζήτηση /1/3/4/ → Οι πρόγονοι είναι [1, 3, 4]
  • Βρίσκοντας απόγονους: Ερώτηση WHERE path LIKE '/1/3/%' → παίρνει όλα κάτω από κόμβο 3
  • Υπολογισμός βάθους: Μετρήστε τους διαχωριστές μείον ένα
  • Βρίσκω αδέρφια: Ερώτηση WHERE path LIKE '/1/3/_/' (άμεσα παιδιά 3 ετών)

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

Η οντότητα προσθέτει μια ενιαία στήλη διαδρομής:

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!;

    // ========== MATERIALISED PATH ==========

    // The complete path from root to this node
    // Format: /ancestor1/ancestor2/.../thisNode/
    // Examples:
    //   Root comment: "/1/"
    //   Child of 1: "/1/5/"
    //   Grandchild: "/1/5/12/"
    //
    // The leading and trailing slashes make pattern matching easier:
    // - LIKE '/1/%' finds all descendants of 1 (includes /1/ itself)
    // - LIKE '/1/5/%' finds all descendants of 5 under 1
    public string Path { get; set; } = string.Empty;

    // We still keep ParentCommentId for:
    // 1. Quick "who is my parent" without parsing
    // 2. EF Core navigation properties
    // 3. Data integrity (can validate path matches parent relationship)
    public int? ParentCommentId { get; set; }
    public Comment? ParentComment { get; set; }
    public ICollection<Comment> Children { get; set; } = new List<Comment>();

    // ========== COMPUTED HELPERS ==========

    // Parse ancestors from path - not stored, computed on demand
    public IEnumerable<int> GetAncestorIds()
    {
        if (string.IsNullOrEmpty(Path)) yield break;

        // Split "/1/3/4/" into ["", "1", "3", "4", ""]
        var parts = Path.Split('/', StringSplitOptions.RemoveEmptyEntries);

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

    // Calculate depth from path
    public int GetDepth()
    {
        if (string.IsNullOrEmpty(Path)) return 0;
        // Count segments: "/1/3/4/" has 3 segments, depth is 2 (0-indexed from root)
        return Path.Split('/', StringSplitOptions.RemoveEmptyEntries).Length - 1;
    }
}

Ρύθμιση πυρήνα 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 ==========
        // Set a reasonable max length - this limits your tree depth
        // /1/12345/12346/12347/...
        // Each segment is up to ~7 chars (ID + slash), so 1000 chars ≈ 140 levels
        builder.Property(c => c.Path)
            .IsRequired()
            .HasMaxLength(1000);

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

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

        // ========== INDEXES ==========

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

        // PATH INDEX - Critical for performance!
        // This makes LIKE 'prefix%' queries efficient
        // PostgreSQL can use a B-tree index for prefix LIKE patterns
        // (but NOT for '%suffix' or '%contains%' patterns)
        builder.HasIndex(c => c.Path);

        // For PostgreSQL, a text_pattern_ops index is even better for LIKE:
        // CREATE INDEX ix_comments_path ON comments (path text_pattern_ops);
        // You may want to add this via a raw migration
    }
}

Βάση δεδομένων Schema

erDiagram
    COMMENT {
        int id PK
        string content
        string author
        datetime created_at
        int post_id FK
        int parent_comment_id FK "optional"
        string path "e.g. /1/3/7/"
    }

    BLOG_POST {
        int id PK
        string title
        string content
    }

    BLOG_POST ||--o{ COMMENT : "has"
    COMMENT ||--o{ COMMENT : "parent-child"

Πράξεις

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

Η εισαγωγή απαιτεί την οικοδόμηση της διαδρομής από το μονοπάτι του γονέα:

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 to extend it
        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");
        }

        // We need the ID first, so we'll update the path after saving
        // (Chicken-and-egg: path contains our ID, but we don't have ID until saved)

        var comment = new Comment
        {
            PostId = postId,
            ParentCommentId = parentId,
            Author = author,
            Content = content,
            CreatedAt = DateTime.UtcNow,
            Path = string.Empty  // Temporary - will update after save
        };

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

        // Now we have the ID - build the real path
        // Parent path "/1/3/" + our ID "7" = "/1/3/7/"
        comment.Path = $"{parentPath}{comment.Id}/";
        await context.SaveChangesAsync(ct);

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

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

        comment.Path = $"/{comment.Id}/";
        await context.SaveChangesAsync(ct);

        logger.LogInformation("Added root comment {CommentId} with path {Path}", comment.Id, comment.Path);
        return comment;
    }
}

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

Χρησιμοποιώντας τη σχέση γονέα-παιδιού (κρατήσαμε ParentCommentId για ευκολία):

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

    // Option 2: Use path pattern (demonstrates path power)
    // var parentPath = await context.Comments
    //     .Where(c => c.Id == commentId)
    //     .Select(c => c.Path)
    //     .FirstOrDefaultAsync(ct);
    //
    // if (parentPath == null) return new List<Comment>();
    //
    // // Find paths that extend parent by exactly one segment
    // // Parent: /1/3/  Children: /1/3/X/ where X is one number
    // var childPathPattern = $"{parentPath}%";
    //
    // return await context.Comments
    //     .AsNoTracking()
    //     .Where(c => EF.Functions.Like(c.Path, childPathPattern)
    //              && c.Path != parentPath
    //              && c.ParentCommentId == commentId)  // Ensures immediate children only
    //     .ToListAsync(ct);
}

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

Αυτό είναι όπου υλοποιημένα μονοπάτια λάμπουν - ανάλυση του μονοπατιού, δεν χρειάζεται αναζήτηση βάσης δεδομένων:

public async Task<List<Comment>> GetAncestorsAsync(int commentId, CancellationToken ct = default)
{
    // Step 1: Get the path (single query)
    var path = await context.Comments
        .Where(c => c.Id == commentId)
        .Select(c => c.Path)
        .FirstOrDefaultAsync(ct);

    if (string.IsNullOrEmpty(path))
        return new List<Comment>();

    // Step 2: Parse ancestor IDs from path
    // Path "/1/3/7/" -> split -> ["1", "3", "7"] -> take all but last -> [1, 3]
    var ancestorIds = path
        .Split('/', StringSplitOptions.RemoveEmptyEntries)
        .SkipLast(1)  // Exclude self
        .Select(int.Parse)
        .ToList();

    if (!ancestorIds.Any())
        return new List<Comment>();

    // Step 3: Fetch ancestors (single query, uses primary key index)
    var ancestors = await context.Comments
        .AsNoTracking()
        .Where(c => ancestorIds.Contains(c.Id))
        .ToListAsync(ct);

    // Step 4: Order by position in path (root first)
    return ancestorIds
        .Select(id => ancestors.First(a => a.Id == id))
        .ToList();
}

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

Χρήση ΟΠΩΣ με το πρόθεμα διαδρομής:

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

    if (string.IsNullOrEmpty(path))
        return new List<Comment>();

    // LIKE 'path%' finds all paths that START with this path
    // Path "/1/3/" matches "/1/3/", "/1/3/5/", "/1/3/5/9/", etc.
    // Using EF.Functions.Like for proper SQL generation
    return await context.Comments
        .AsNoTracking()
        .Where(c => EF.Functions.Like(c.Path, $"{path}%") && c.Id != commentId)
        .OrderBy(c => c.Path)  // Gives us depth-first order!
        .ToListAsync(ct);
}

Αποκτήστε Απόγονους με Βάθος

Μπορούμε να υπολογίσουμε το βάθος από το μονοπάτι:

public async Task<List<CommentWithDepth>> GetDescendantsWithDepthAsync(
    int commentId,
    int? maxDepth = null,
    CancellationToken ct = default)
{
    var comment = await context.Comments
        .AsNoTracking()
        .FirstOrDefaultAsync(c => c.Id == commentId, ct);

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

    var basePath = comment.Path;
    var baseDepth = basePath.Split('/', StringSplitOptions.RemoveEmptyEntries).Length;

    // Get all descendants
    var query = context.Comments
        .AsNoTracking()
        .Where(c => EF.Functions.Like(c.Path, $"{basePath}%") && c.Id != commentId);

    var descendants = await query.ToListAsync(ct);

    // Calculate relative depth and filter if needed
    var result = descendants
        .Select(d =>
        {
            var absoluteDepth = d.Path.Split('/', StringSplitOptions.RemoveEmptyEntries).Length;
            var relativeDepth = absoluteDepth - baseDepth;
            return new CommentWithDepth
            {
                Id = d.Id,
                Content = d.Content,
                Author = d.Author,
                CreatedAt = d.CreatedAt,
                PostId = d.PostId,
                ParentCommentId = d.ParentCommentId,
                Path = d.Path,
                Depth = relativeDepth
            };
        })
        .Where(d => !maxDepth.HasValue || d.Depth <= maxDepth.Value)
        .OrderBy(d => d.Path)
        .ToList();

    return result;
}

public class CommentWithDepth
{
    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; }
}

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

Απλό με ταίριασμα διαδρομής:

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 (string.IsNullOrEmpty(path))
    {
        throw new InvalidOperationException($"Comment {commentId} not found");
    }

    // Delete all comments whose path starts with this path
    // This includes the comment itself and ALL descendants
    var deleted = await context.Comments
        .Where(c => EF.Functions.Like(c.Path, $"{path}%"))
        .ExecuteDeleteAsync(ct);

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

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

Αυτή είναι η ακριβή λειτουργία για υλοποιημένα μονοπάτια - πρέπει να ενημερώσουμε ΟΛΑ τα μονοπάτια απόγονων:

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

    try
    {
        // Get the node being moved
        var comment = await context.Comments
            .FirstOrDefaultAsync(c => c.Id == commentId, ct);

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

        // Get the new parent
        var newParent = await context.Comments
            .FirstOrDefaultAsync(c => c.Id == newParentId, ct);

        if (newParent == null)
            throw new InvalidOperationException($"New parent {newParentId} not found");

        // Prevent cycles: can't move under own descendant
        if (newParent.Path.StartsWith(comment.Path))
        {
            throw new InvalidOperationException("Cannot move a node under its own descendant");
        }

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

        // Get all descendants (including the node itself)
        var descendants = await context.Comments
            .Where(c => EF.Functions.Like(c.Path, $"{oldPath}%"))
            .ToListAsync(ct);

        // Update all paths by replacing the old prefix with the new one
        foreach (var descendant in descendants)
        {
            // Replace old path prefix with new one
            // Old: /1/3/7/  Node 7 moving under /2/
            // Node 7: /1/3/7/ -> /2/7/
            // Node 9 (child of 7): /1/3/7/9/ -> /2/7/9/
            descendant.Path = newPath + descendant.Path.Substring(oldPath.Length);
        }

        // Update the direct parent reference
        comment.ParentCommentId = newParentId;

        await context.SaveChangesAsync(ct);
        await transaction.CommitAsync(ct);

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

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

sequenceDiagram
    participant App as Application
    participant EF as EF Core
    participant DB as PostgreSQL

    Note over App,DB: Getting Ancestors (Path parsing)
    App->>EF: GetAncestorsAsync(commentId)
    EF->>DB: SELECT path FROM comments WHERE id = @id
    DB-->>EF: Path "/1/3/7/"
    Note over App: Parse path → [1, 3]
    EF->>DB: SELECT * FROM comments WHERE id IN (1, 3)
    DB-->>EF: Ancestor comments
    EF-->>App: List<Comment>

    Note over App,DB: Getting Descendants (LIKE query)
    App->>EF: GetDescendantsAsync(commentId)
    EF->>DB: SELECT path FROM comments WHERE id = @id
    DB-->>EF: Path "/1/3/"
    EF->>DB: SELECT * FROM comments WHERE path LIKE '/1/3/%'
    DB-->>EF: All descendants
    EF-->>App: List<Comment>

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

Η λειτουργία της πολυπλοκότητας της βάσης δεδομένων σημειώσεις |-----------|------------|------------------|-------| Εισάγετε το μονοπάτι σας Αpiοκτήστε piαιδιά Αpiό τι piεριpiτώσει αpiό τι οpiοίε piρέpiει να χρησιοpiοιήσετε το Γονικό Σχόλιο Φέρε τους προγόνους σου και φέρε τους προγόνους σου. Αποκτήστε απόγονους * Βρείτε μονοπάτι + LIKE query Μετακινήστε το subtree O(s) Διαγράψτε το subtree O(1)* Φέρτε το μονοπάτι + διαγράψτε μαζικά.

*Με τον κατάλληλο δείκτη στη στήλη διαδρομής

Δείκτης

Για PostgreSQL, σκεφτείτε τη χρήση text_pattern_ops, η οποία επιτρέπει την αποτελεσματική πρόθεμα LIKE ερωτήματα σε μη-C τοποθεσίες:

-- Standard B-tree index (works for LIKE 'prefix%')
CREATE INDEX ix_comments_path ON comments (path);

-- Better for pattern matching in PostgreSQL
CREATE INDEX ix_comments_path_pattern ON comments (path text_pattern_ops);

Προσθέστε αυτό μέσω μιας μετανάστευσης:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Sql(
        "CREATE INDEX ix_comments_path_pattern ON comments (path text_pattern_ops)");
}

Αξιολογήσεις σχήματος διαδρομής

Διαφορετικοί ορίζοντες έχουν ανταλλάγματα:

Φόρμουλα, Παράδειγμα |--------|---------|------|------| | /1/3/7/ Αυτό το άρθρο □ Καθαρισμός, URL-όπως, εύκολος αναπαράσταση □ Χρησιμοποιεί περισσότερο χώρο | 1.3.7 ~ PostgreSQL ltree style ~ Compact, works with ltree ~ Περίοδος συγκρούσεων με δεκαδικά ~ | 1,3,7 Το κόμμα στα δεδομένα θα μπορούσε να προκαλέσει προβλήματα. | 001.003.007 Εύρος ID ορίων, χώρος αποβλήτων

Η /id/ Η μορφή με κορυφαίες και ιχνηλάτες συνιστάται επειδή:

  1. ΟΡΙΣΤΙΚΑ πρότυπα λειτουργούν σωστά (/1/% σπίρτα /1/ αλλά όχι /10/)
  2. Εύκολη διάσπαση και ανάλυση
  3. Ανθρώπινο-διαγνωστό για αποσφαλμάτωση

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

Πως θα το κάνουμε αυτό; |------|------| Οι πρόγονοι που είναι διαθέσιμοι από την ανάλυση (χωρίς ερώτηση) Η μετακίνηση υποδέντρων απαιτεί ενημέρωση όλων των απογόνων Οι απόγονοι μέσω απλών ερωτημάτων δεν έχουν βάθος δέντρου Βάθος υπολογιζόμενος από το μονοπάτι Η χειραγώγηση χορδών έχει από πάνω Ο άνθρωπος-διαγνωστό για αποσφαλμάτωση ~ LIKE ερωτήματα μπορεί να είναι αργή χωρίς το σωστό δείκτη ~ Το καλό για την γενιά breadcrumb πρέπει να διατηρηθεί σε συγχρονισμό με το ParentCommentId Η προσθήκη μιας στήλης δεν μπορεί να χρησιμοποιήσει το πρότυπο B-δέντρο για ταίριασμα suffix

Πότε να χρησιμοποιήσετε υλοποιημένη διαδρομή

Επιλέξτε Υλοποιημένη Διαδρομή όταν:

  • Τα ψωμιά είναι μια κοινή απαίτηση
  • Οι πρόγονοι ρωτούν πιο συχνά από τους απογόνους.
  • Το βάθος του δέντρου περιορίζεται (δεν θα έχετε 100+ επίπεδα δέντρα)
  • Η μετακίνηση υποδέντρων είναι σπάνια.
  • Θέλετε δεδομένα ιεραρχίας που είναι κατανοητά από τον άνθρωπο για αποσφαλμάτωση

Αποφύγετε την Υλοποιημένη Διαδρομή όταν:

  • Συχνά μετακινείτε subtrees (updating όλα τα μονοπάτια είναι ακριβά)
  • Τα δέντρα μπορεί να είναι πολύ βαθιά (οι χορδές μονοπάτι γίνονται δυσκίνητες)
  • Χρειάζεστε αποτελεσματικό ταίριασμα επιθήλια (βρίσκοντας όλα τα δέντρα που καταλήγουν σε ένα μοτίβο)
  • Είστε πιο άνετα με ltree (PostgreSQL-συγκεκριμένο αλλά πιο βελτιστοποιημένο)

Σύγκριση με ltree

Αν είσαι στο PostgreSQL, σκέψου Μέρος 1.5: ltree Αντί αυτού. ltree είναι ουσιαστικά μια μητρική βάση δεδομένων, βελτιστοποιημένη υλοποιημένη διαδρομή με:

  • Υποστήριξη δείκτη GiST για αποτελεσματικά ερωτήματα
  • Ενσωματωμένοι φορείς εκμετάλλευσης (@>, <@, ~, κ.λπ.)
  • Λειτουργίες χειρισμού διαδρομής
  • Μοτίβο που ταιριάζει με Wildcards

Η ανταλλαγή είναι PostgreSQL lock-in. Σημειώστε ότι η Ο πάροχος Npgsql υποστηρίζει τώρα τις μεταφράσεις LINQ για ltree μέσω του LTree τύπος, αν και επαναλαμβανόμενες CTEs εξακολουθούν να απαιτούν ωμό SQL.

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

logo

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