Best practice to store graph with ordered edges (entity with ordered collection navigation) in Entity Framework Core

huangapple go评论86阅读模式
英文:

Best practice to store graph with ordered edges (entity with ordered collection navigation) in Entity Framework Core

问题

将具有已排序边的图结构(类似于树中的父子关系)保存到使用 EF Core 的数据库中涉及在从数据库加载实体后保持子节点的顺序不变,而这不幸的是常规关系数据库关系不提供的功能。已经有一些与此相关的问题。(#9067 #30551 #2919)

对于来自官方文档的交错两种类型图示例,对我来说最有希望的方式是将关系更改为两个单向的多对多关系,为两个映射实体添加排序号。然而,我没有找到一个好的事件或拦截器来在将实体保存到数据库时自动生成ICollection字段的排序号。

所期望的事件应该是这样的:

dbContext.SavingChangesOfEntity += fun;

private void fun(object? sender, SomeArg arg) 
{
    if (sender is Post{} post)
    {
        foreach(var (tag, i) in post.Tags.Zip(Enumerable.Range(0, 0x7fffffff))) 
        {
            (dbContext.FindMappingEntity(post, tag) as PostTag).OrderNumber = i;
        }
    } 
    else if (sender is Tag{} tag)
    {
        foreach(var (post, i) in tag.Postss.Zip(Enumerable.Range(0, 0x7fffffff))) 
        {
            (dbContext.FindMappingEntity(tag, post) as TagPost).OrderNumber = i;
        }
    }
}

public class Post 
{
    public int Id { get; set; }
    public List<Tag> Tags { get; } = new List<Tag>();
}

public class Tag 
{
    public int Id { get; set; }
    public List<Post> Posts { get; } = new List<Post>();
}

public class PostTag 
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public int OrderingNumber { get; set; }
}

public class TagPost 
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public int OrderingNumber { get; set; }
}

将图存储为自引用类型也很常见,如下所示:

dbContext.SavingChangesOfEntity += fun;

private void fun(object? sender, SomeArg arg) 
{
    if (sender is RichText{} richtext)
    {
        foreach(var (child, i) in richtext.Children.Zip(Enumerable.Range(0, 0x7fffffff))) 
        {
            (dbContext.FindMappingEntity(richtext, child) as RichTextBlogObject).OrderNumber = i;
        }
    }
}

public abstract class BlogObject 
{
    public int Id { get; set; }
}

public class Text : BlogObject { ... }
public class Image : BlogObject { ... }
public class Link : BlogObject { ... }

public class RichText : BlogObject 
{
    public List<BlogObject> Children { get; set; } = new List<BlogObject>();
}

public class RichTextBlogObject 
{
    public int RichTextId { get; set; }
    public int BlogObjectId { get; set; }
    public int OrderingNumber { get; set; }
}

我认为在这个问题的情况下,这两种方案看起来是相同的,可以共享一个通用的解决方案。

所以,对于当前的 EF Core 是否可以实现这一目标,或者是否有其他方法可以完成任务?或者也许更好的做法是放弃这个想法,转向其他想法,比如只存储相关实体的 ID,并手动从数据库加载它,丢弃任何结构化的关系?

PS: 这基本上与此问题相同,尽管作者已经不得不手动设置顺序,而这个问题并非如此。

英文:

Saving a graph structure which edges are ordered (like parent-child relationship in trees) into database with EF Core involves with keeping order of child nodes unchanged after loading back entity from database, which is unfortunately not provided by normal relationship of relational database. There are already some issues about it. (#9067 #30551 #2919)

For an interleaved two type graph example from official document, the most promising way to me is to change the relationship to two unidirectional many-to-many one, add an ordering number to both mapping entities. However I didn't find a good event or interceptor to auto generate the ordering number from the ICollection field when the entity is saved to database.

The supposed event should be like:

dbContext.SavingChangesOfEntity += fun;

private void fun(object? sender, SomeArg arg) 
{
    if (sender is Post{} post)
    {
        foreach(var (tag, i) in post.Tags.Zip(Enumerable.Range(0, 0x7fffffff))) 
        {
            (dbContext.FindMappingEntity(post, tag) as PostTag).OrderNumber = i;
        }
    } 
    else if (sender is Tag{} tag)
    {
        foreach(var (post, i) in tag.Postss.Zip(Enumerable.Range(0, 0x7fffffff))) 
        {
            (dbContext.FindMappingEntity(tag, post) as TagPost).OrderNumber = i;
        }
    }
}

public class Post 
{
    public int Id { get; set; }
    public List&lt;Tag&gt; Tags { get; } = new();
}

public class Tag 
{
    public int Id { get; set; }
    public List&lt;Post&gt; Posts { get; } = new();
}

public class PostTag 
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public int OrderingNumber { get; set; }
}

public class TagPost 
{
    public int PostId { get; set; }
    public int TagId { get; set; }
    public int OrderingNumber { get; set; }
}

It's also common to store a graph as self-reference type, like:

dbContext.SavingChangesOfEntity += fun;

private void fun(object? sender, SomeArg arg) 
{
    if (sender is RichText{} richtext)
    {
        foreach(var (child, i) in richtext.Children.Zip(Enumerable.Range(0, 0x7fffffff))) 
        {
            (dbContext.FindMappingEntity(richtext, child) as RichTextBlogObject).OrderNumber = i;
        }
    }
}

public abstract class BlogObject 
{
    public int Id { get; set; }
}

public class Text : BlogObject { ... }
public class Image : BlogObject { ... }
public class Link : BlogObject { ... }

public class RichText : BlogObject 
{
    public List&lt;BlogObject&gt; Children { get; set; } = new();
}

public class RichTextBlogObject 
{
    public int RichTextId { get; set; }
    public int BlogObjectId { get; set; }
    public int OrderingNumber { get; set; }
}

I think both scheme looks the same in the scenario of this question, and could share a common solution.

So is it achievable for current EF Core, or are there some other practices that can do the job? Or maybe it's better to give up on the idea, and turn to some other ideas, such as only storing IDs of related entity and manually load it from database, throwing away any structured relations?

PS: It's basically the same as this question, although the author already have to set the order manually, which is not the case of this question.

答案1

得分: 1

只需要一个连接表来处理PostTag或TagPost。它只是两个实体之间的连接表。尝试同时使用两者可能会导致错误或更糟。

你并没有描述一个树结构,树是一种自引用结构,意味着一个帖子包含其他帖子,形成一个帖子包含更多帖子的树结构。相反,具有标签的帖子,其中您希望保持标签的添加顺序(或排列顺序)只是一对多的有序关系。

public class Post
{
    public int Id { get; set; }
    public virtual ICollection<PostTag> PostTags { get; protected set; } = new List<PostTag>();

    [NotMapped]
    public IReadOnlyCollection<Tag> Tags
    {
        get => PostTags
            .OrderBy(x => x.OrderBy)
            .Select(x => x.Tag)
            .ToList()
            .AsReadOnly();
    }
}

public class Tag
{
    public int Id { get; set; }
    // Optional if you intend to load tags and want their posts.
    public virtual ICollection<PostTag> PostTags { get; protected set; } = new List<PostTag>();

    [NotMapped]
    public IReadOnlyCollection<Post> Posts
    {
        get => PostTags
            .Select(x => x.Post)
            .ToList()
            .AsReadOnly();
    }
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }  // Both need to be configured as a composite key
    public int OrderBy { get; set; }
    [ForeignKey(nameof(PostId))]
    public virtual Post Post { get; set; }
    [ForeignKey(nameof(TagId))]
    public virtual Tag Tag { get; set; }
}

除非在查询标签时加载与标签关联的帖子,否则建议保持导航单向,删除Tag.TagPosts / Tag.Posts,并在Post配置中使用.HasMany(post => post.PostTags).WithMany()而不是.HasMany(post => post.PostTags).WithMany(tag => tag.PostTags)。您仍然可以使用Post.PostTags来查询与标签关联的所有帖子。请注意,对于Post.Tags(如果需要的话)的[NotMapped]集合仅供方便使用。您只需确保在查询被实体化之前不要尝试在Linq表达式中使用此集合。您需要在Linq2SQL表达式中使用PostTags集合。还要注意,OrderBy子句适用于Post.Tags,但在Tag.Posts中不会使用,因为它暗示了帖子内标签的顺序。对Tag.Posts进行排序将根据标签在Post.Tags中的位置返回帖子,因此具有该标签作为第一个标签的帖子将在具有该标签在第二位置的帖子之前返回。(除非您打算进行排序)如果帖子应该被排序,那么帖子还应该具有SortOrder类型的列,或者按照某种方式对它们进行排序,例如按创建日期。

对于使用多对一TPH(表对层次结构)模型创建的普通树层次结构:

public abstract class BlogObject
{
    public int Id { get; set; }
    public int? ParentId { get; set; }
    public int OrderingNumber { get; set; } = 0;
}

public class Text : BlogObject { ... }
public class Image : BlogObject { ... }
public class Link : BlogObject { ... }

public class RichText : BlogObject
{
    public List<BlogObject> Children { get; set; } = new();
}

对于您创建的每个项目,无论是Text、Image、Link还是RichText,都要在一个级别上创建(即在RichText的Children中,或在顶级Document或其他对象的集合中),您都需要设置一个OrderingNumber值来表示您希望它们出现的顺序。所以,如果我有3个顶级的RichText,它们可能是1、2和3。然后,当我添加各种其他元素时,在每个RichText下面,我为它们分配适当的OrderingNumbers,例如1、2、...或其他值,值无关紧要,只有顺序重要。

或者,您还可以利用诸如SQL Server的HierarchyId来组织项目的层次结构,而无需依赖于像ParentId这样的东西,甚至可以管理排序,因为层次结构号不需要反映正在组织的项目的ID。您可以在这里看到示例:https://www.meziantou.net/using-hierarchyid-with-entity-framework-core.htm

public abstract class BlogObject
{
    public int Id { get; set; }
    public HierarchyId Hierarchy { get; set; } // /1   /2  /3  /1/1  /1/2 ..
}

层次结构中的数字表示该级别上项目的唯一顺序,而不是Id/parentId等。

英文:

You only need one joining table for PostTag or TagPost. It is merely the joining table between the two entities. Attempting to have both would likely lead to errors or worse.

You aren't describing a tree, which is a self-referencing structure, meaning a post contains other posts which forms a tree where those posts contain more posts. Instead a Post with tags where you want to maintain the order of the tags as they were added (or arranged) is just an ordered many to many.

public class Post
{
    public int Id {get; set;}
    public virtual ICollection&lt;PostTag&gt; PostTags { get; protected set;} = new List&lt;PostTag&gt;();

    [NotMapped]
    public IReadOnlyCollection&lt;Tag&gt; Tags 
    {
        get =&gt; PostTags
            .OrderBy(x =&gt; x.OrderBy)
            .Select(x =&gt; x.Tag)
            .ToList()
            .AsReadOnly();
    }
}

public class Tag
{
    public int Id {get; set;}
    // Optional if you intend to load tags and want their posts.
    public virtual ICollection&lt;PostTag&gt; PostTags { get; protected set;} = new List&lt;PostTag&gt;();

    [NotMapped]
    public IReadOnlyCollection&lt;Post&gt; Posts 
    {
        get =&gt; PostTags
            .Select(x =&gt; x.Post)
            .ToList()
            .AsReadOnly();
    }
}

public class PostTag
{
    public int PostId { get; set; }
    public int TagId { get; set; }  // Both need to be configured as a composite key
    public int OrderBy { get; set; }
    [ForeignKey(nameof(PostId))]
    public virtual Post Post { get; set; }
    [ForeignKey(nameof(TagId))]
    public virtual Tag Tag { get; set; }
}

Unless it makes sense to load Posts associated to a Tag when querying tags I'd recommend leaving the navigation unidirectional, removing Tag.TagPosts / Tag.Posts and using a .HasMany(post =&gt; post.PostTags).WithMany() on the Post configuration rather than .HasMany(post =&gt; post.PostTags).WithMany(tag =&gt; tag.PostTags). You can still get all Posts for a Tag querying posts using Post.PostTags. Note the [NotMapped] collection for Post.Tags (and Tag.Posts if needed) can be provided for convenience. You just need to ensure that you don't try and use this collection in Linq expressions prior to a query being materialized. You need to use the PostTags collection for Linq2SQL expressions. Also note that the OrderBy clause is applied to Post.Tags, but would not be used in Tag.Posts as it implies the order of tags within a post. Ordering Tag.Posts would return Posts by the position of the tag within Post.Tags so a post with that tag as the first tag would be returned before a Post with that tag in the second spot.(Unless that is an ordering you intend) If Posts should be ordered then Post should also have a SortOrder type column or order them by something like Created Date.

Edit a normal Tree hierarchy with a many-to-one TPH (table-per-hierarchy) model:

public abstract class BlogObject 
{
    public int Id { get; set; }
    public int? ParentId { get; set; }
    public int OrderingNumber { get; set; } = 0;
}

public class Text : BlogObject { ... }
public class Image : BlogObject { ... }
public class Link : BlogObject { ... }

public class RichText : BlogObject 
{
    public List&lt;BlogObject&gt; Children { get; set; } = new();
}

For each item you create, whether Text, Image, Link, or RichText that you create at a level (I.e. within Children of a RichText, or within a collection of a top level Document or whatever object) you would set an OrderingNumber value to represent the order you want them to appear.

So if I have 3x top level RichText, they might be 1, 2, and 3. Then when I add various other elements, under each RichText I give those appropriate OrderingNumbers of 1, 2, ... or whatever, the value doesn't matter, only the order.

Alternatively you can also utilize something like SQL Server's HierarchyId to both organize the hierarchy of items without relying on things like ParentId, and even manage sorting as the hierarchy #'s do not need to reflect the IDs of the items being organized. You can see examples of this at: https://www.meziantou.net/using-hierarchyid-with-entity-framework-core.htm

public abstract class BlogObject 
{
    public int Id { get; set; }
    public HierarchyId Hierarchy { get; set; } // /1   /2  /3  /1/1  /1/2 ..
}

The numbers in a Hierarchy represent the unique order of the item at that level rather than the Id/parentId etc.

huangapple
  • 本文由 发表于 2023年7月20日 10:28:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/76726304.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定