الجزء 1-5: PostgresSQL Ltrry مع EF (العربية (Arabic))

الجزء 1-5: PostgresSQL Ltrry مع EF

Saturday, 06 December 2025

//

15 minute read

امتدادات الاشجار تعطيك مسارات مستحدثة مع قاعدة بيانات البيانات @> وقد عقد مؤتمراً بشأن <@إذا كنت ملتزماً بـ PostgreSQL وتريد أفضل أداء التسلسل الهرمي للاستفسار عن الأداء، فإن من الصعب التغلب على الشجر.

الأخبار السارة: الـ مورِد المُزوّد الرئيسي يقوم بدعم ترجمات LINQ لـ عمليات شجرة عن طريق: LTree يمكنك استخدام طرق مثل: IsAncestorOf(), IsDescendantOf()، و ، ، ، ، ، ، ، ، ، ، ، MatchesLQuery() مباشرة في استفسارات LINQ. على أي حال، EF CORE لا يدعم بعد CTEs التكرارية، لذلك سوف تحتاج الخام SQL للعمليات التي تتطلبها (مثل بناء كامل النتائج الفرعية للشراء مع الأعماق المحسوبة).

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 2 - شـايـشـي من أجل الإشارة إلى دعم ترجمة LINQ!

السلسلة


ما هو "لتري"؟

ltree هو a PostgerresSQL امتداد الذي يزوّد a محلي بيانات نوع لـ تراتبية تسمية المسار. فكّر فيه كـ مُثثثثثثثث مع القوة العظمى - قاعدة البيانات تفهم الهيكل وتوفر المشغّلين المُفضّلين، الوظائف، ودعم مؤشر جيست.

بدلاً من معاملة المسار على أنه سلسلة غبية واستخدام مثل الإستفسارات، PostgresQL يمكن:

  • المستخدمون الخاصون@> لـ "أسلافية" لـ، <@ لـ "ينحدر من")
  • تطبيق مجلدات
  • أنماط متطابقة مع البطاقات الجامدة (Top.*.Europe)
  • أداء عمليات وضع المجموعة على المسارات

البصيرة الرئيسية: فالأشجار هي أفضل ما في العالمين - أي بساطة المسارات المادية مع أفضلية قاعدة البيانات. والمقايضة هي قفل بوستغريسQL، وفي حين أن العديد من عمليات الأشجار تعمل عن طريق LINQ، فإن CTEs التكرارية لا تزال تحتاج إلى SQL الخام.

لغتري المسار هيئة

مسارات في شجرة استخدام فترات كفواصل وألفا رقمي شارات:

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

القواعد:

  • يمكن أن تحتوي الأعلام على أحرف، أرقام، و تشد
  • التدرجات حساسة للحالة
  • مُقصاطاطاطاطاط المُسند هو 25

لأنظمة التعليق، كنا نستخدم الهويات كملصقات: 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");
}

كُلْ

يتضمن مقدّم Nppggsql ما يلي: LTree نوع الخرائط مباشرة إلى شجرة PostgreSQL ويوفر طرق LINQ القابلة للترجمة:

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

إف إف احتياطي

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);
    }
}

اضف الرقم القياسي للتحريض عن طريق الهجرة:

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

المشغِّل

يترجم مقدم خدمات Npgsql EF LTree إلى هذه المشغلات:

/ المشغِّل/المعنى المقصود / طريقة اللينQ / نموذج SQL / |----------|---------|-------------|-------------| | @> ° هو سلف (حاويات) o ltree1.IsAncestorOf(ltree2) | '1.3'::ltree @> '1.3.7'::ltree □ حقا □ □ ▪ ▪ | <@ (يتبع)(ب) ltree1.IsDescendantOf(ltree2) | '1.3.7'::ltree <@ '1.3'::ltree □ حقا □ □ ▪ ▪ | ~ □ المباريات التي يتطابق معها نمط المكي ltree.MatchesLQuery(pattern) | '1.3.7'::ltree ~ '1.*'::lquery □ حقا □ □ ▪ ▪ | @ « المباريات » 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) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  • 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;
    }
}

إحصل على الأطفال المُوَجِّهين

مورِث اللّوازِلCommentID (Smil) أو ltry نمط المطابقة:

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);
}

تحصل على جميع المنجّزات

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);
}

احصل على كل المنسوسات

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);
}

ابحث إلى الأقصى عمق

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);
}

م

(لتري) يدعم الأنماط القوية للمكتيريات. 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);
}

احذف اصغر

يمكنك استخدام LineQ إلى تحديد شجرة الجزئية، ثمّ حذف:

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);
}

وتوفّر الشجر وظائف للمساعدة في التلاعب بالمسارات:

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

& شجرة المُسند

يوفر PostgreSQL العديد من وظائف الأشجار المفيدة:

الوظيفة/الوظيفة/الوصف/الوصف |----------|-------------|---------| | nlevel(ltree) □ عدد البطاقات التعريفية □ عدد البطاقات nlevel('1.3.7') → 3 | | subpath(ltree, offset) 1 خ ع (رأ) 1 خ ع (رأ) 1 خ ع (رأ) 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) ▪ نقل النص إلى شجرة text2ltree('1.3.7') | | ltree2text(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>

(أ)

□ عملية □ معقــد □ ملحوظات □ |-----------|------------|-------|

إدراج # # # # # # # فقط ضع سلسلة المسار #

  • يحصل على أطفال * O (1) يطابق نمط المطابق مع مؤشر GIST *

الحصول على أسلاف # # O (1) # @ @ # شغل مع مؤشر GIST

  • يحصل على أحفاد o O (1) / > > @ مشغِّل مع مؤشر GIST o □ خطاطة مناظرة لـ o o o o o log n □ GIST index يدعم liqury o □ حرّك سطراً فرعياً □ O(s) □ تحديث مسارات السُلُف □ تُحذف الفقرة الفرعية (أ) من الفقرة (1) من الفقرة الفرعية (أ) من الفقرة الفرعية (أ) من الفقرة الفرعية (أ) من الفقرة الفرعية (أ) من الفقرة (1) من الفقرة الفرعية (أ) من الفقرة الفرعية (أ) من الفقرة الفرعية (ب) من الفقرة الفرعية (ب) من الفقرة الفرعية (ب) من الفقرة الفرعية (ب) من الفقرة (1) من الفقرة الفرعية (د) من الفقرة الفرعية (ب) من الفقرة الفرعية (د) من الفقرة الفرعية (د) من الفقرة الفرعية (د) من الفقرة الفرعية (د) من الفقرة الفرعية (د) من الفقرة الفرعية (د) من الفقرة الفرعية (د) من الفقرة الفرعية (د) من الفقرة (د) من الفقرة الفرعية (د) من الفقرة الفرعية (د) من الفقرة الفرعية (د) من الفقرة (د) من الفقرة الفرعية (د) من الفقرة الفرعية (ب) من الفقرة (1) من الفقرة الفرعية (د) من الفقرة الفرعية (د) من الفقرة (د) من الفقرة (د) من الفقرة (د) من الفقرة (د)

مع فهرسات GIST، الإستفسارات عن شجرة عالية الكفاءة - عادة O(log n) بغض النظر عن عمق الشجرة.

بروات وكونس

▪ pros cons / |------|------| □ قاعدة بيانات البيانات القائمة على المستوى الأمثل □ PostgresSQL فقط □ مؤشر GIST لجميع استفسارات التسلسل الهرمي Labels مقصوراً على ألفا عددي الدالة الدالة الدالة الدالة الدالة الدالة الدالة الدالة الدالة الدالة الدالة الدالة CTEs التي تتطلب TEs الخام SQL o □ سين (1) الأسلاف/استفسارات المنحدرين من أصل استفسارات أقل نقالا من الحلول الأساسية لمنتدى EF النقي ○ تخزين الاتفاق دعم LINQ دعم عن طريق Npgsql LTree / / / /

متى إلى استخدام شجرة

تنفيذ::::::::::::::::::

  • "أنت ملتزم بـ "المُتَوَقّعSQL
  • الأداء ذو أهمية حاسمة بالنسبة للاستفسارات الهرمية
  • تحتاج إلى مطابقة نمطية (تجد كل X.*(المسارات)
  • تُريدُ الأفضلَ مِنْ الدُرْسِ المُصْرَفَة
  • تريد دعم LINQ لمعظم عمليات التدرج

1 - يُجنّب لغتريّات عندما:

  • تحتاج إلى نقل قاعدة بيانات (SQL خادم، MySQL، الخ.)
  • فريقك غير ملم بملحقات PostGresSQL
  • تحتاج إلى محارب غير الأحرف
  • تحتاج إلى تكراري CTEs و إلى تجنب أي خام SQL

مقارنة مع طريق مُعَرَض

▪ الآفاق : ‹ الدرب المُوَدَّدَدَدَيْن ‹ ‹ الدرب المُوَدَّدَدَة ‹ |--------|-------------------|-------| □ نوع الرقم القياسي □ باء - شجرة (السلفة فقط) □ غيست (جميع الأنماط) □ o نمط مطابقات مثل "استعراض%%" فقط كؤوس برية كاملة o □ المشغلون □ مقارنة مقارنة بين السكان الأصليين @>، <>، <@، ~ □ أي قاعدة بيانات □ PostgreSQL فقط □ الدعم الأساسي لـ EF الدعم الأساسي / كامل اللينQ / linQ عن طريق LINQ LTree النوع (الوحدات التي تحتاج إليها الأطراف التي تحتاج إلى خام SQL) □ أداء أداء جيد مع فهرس أو ممتاز مع GIST / لا يوجد (التوصيل اليدوي) مكتبة وظيفة غنية

مثال على ذلك: التعليق الكامل

وضع كل ذلك معاً - الحصول على شجرة تعليق كاملة مع عمق لكتابة مدونة:

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

السلسلة

ما هو التالي؟

وقد غطت هذه السلسلة خمسة نُهُج للبيانات الهرمية باستخدام الموارد الخارجية الأساسية. وسيستكشف الجزء الثاني استخدام المواد الخام SQL وDaper لزيادة الرقابة على الاستفسارات الهرمية - قريباً!

Finding related posts...
logo

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