【问题标题】:ASP.NET Core 2.1 Entity Framework Core Related Data DuplicationASP.NET Core 2.1 Entity Framework Core 相关数据复制
【发布时间】:2019-02-23 06:04:54
【问题描述】:

我正在尝试构建一个 ASP.NET Core API,它返回一个包含一些相关、连接数据的对象。

但是当我使用 Include() 调用 Article 模型时,它会由 EF Core 自动映射。这是有意的,但是.. 发生了意外的映射。

EF Core 为所有对象生成完全映射的对象,从而产生过多的数据。

例如,User 模型具有 Article 属性,当我调用 Article 模型时,User 属性也包含 Article 对象信息。结果,它返回了很多重复的数据。

第一次,我认为它来自

https://docs.microsoft.com/en-us/ef/core/querying/related-data

  1. 渴望加载
  2. 显式加载
  3. 延迟加载

但事实并非如此。

我不知道如何配置 EF Core 不映射里面的所有对象..

*数据库架构和模型

enter image description here

public class Topic
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [HiddenInput(DisplayValue = false)]
    public int TopicId { get; set; }

    [Required]
    [StringLength(20, ErrorMessage = "Title cannot be longer than 20 characters.")]
    public string Title { get; set; }

    [StringLength(200, ErrorMessage = "Description cannot be longer than 200 characters.")]
    public string Description { get; set; }

    public byte[] Picture { get; set; }

    public string PictureMimeType { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:MM-dd-yyyy}", ApplyFormatInEditMode = true)]
    public DateTime PostDate { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:MM-dd-yyyy}", ApplyFormatInEditMode = true)]
    public DateTime ModifyDate { get; set; }

    [Display(Name = "Show")]
    public bool ShowFlag { get; set; }

    public int UserId { get; set; }

    public virtual User User { get; set; }

    public virtual ICollection<Article> Articles { get; set; }
}

public class Article
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [HiddenInput(DisplayValue = false)]
    public int ArticleId { get; set; }

    [Required]
    [StringLength(100, ErrorMessage = "Title cannot be longer than 100 characters.")]
    public string Title { get; set; }

    public string Content { get; set; }

    [StringLength(10, ErrorMessage = "Category cannot be longer than 10 characters.")]
    public string Category { get; set; } = "Free";

    public byte[] Picture { get; set; }

    public string PictureMimeType { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:MM-dd-yyyy}", ApplyFormatInEditMode = true)]
    public DateTime PostDate { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:MM-dd-yyyy}", ApplyFormatInEditMode = true)]
    public DateTime ModifyDate { get; set; }

    public int ReadCount { get; set; }

    [Required]
    [Display(Name = "Show")]
    public bool ShowFlag { get; set; }

    public int TopicId { get; set; }

    public virtual Topic Topic { get; set; }

    public int UserId { get; set; }

    public virtual User User { get; set; }
}

public class User
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [HiddenInput(DisplayValue = false)]
    public int UserId { get; set; }

    [StringLength(20, ErrorMessage = "Name cannot be longer than 20 characters.")]
    public string Name { get; set; }

    [StringLength(255, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [DataType(DataType.EmailAddress)]
    [EmailAddress]
    public string Email { get; set; }

    [StringLength(20, ErrorMessage = "Title cannot be longer than 20 characters.")]
    public string Title { get; set; }

    [StringLength(20, ErrorMessage = "Phone number cannot be longer than 20 characters.")]
    [DataType(DataType.PhoneNumber)]
    public string Phone { get; set; }

    [StringLength(100, ErrorMessage = "Address cannot be longer than 100 characters.")]
    public string Address { get; set; }

    [StringLength(100, ErrorMessage = "Introduction cannot be longer than 100 characters.")]
    public string Introduction { get; set; }

    [DataType(DataType.Date)]
    public DateTime? Birthdate { get; set; }

    public byte[] Picture { get; set; }

    public string PictureMimeType { get; set; }

    public virtual ICollection<Topic> Topics { get; set; }

    public virtual ICollection<Article> Articles { get; set; }
}

*控制器代码

   var article = await this._context.Articles
     .Include(a => a.User)
     .Include(a => a.Topic)
     .SingleOrDefaultAsync(a => a.ArticleId == id);

*重复数据

{
"articleId": 5,
"title": "Does TaskAll() really wait for all in any case?",
"content": null,
"category": "Threading",
"picture": null,
"pictureMimeType": null,
"postDate": "2018-06-18T07:00:00",
"modifyDate": "2018-06-20T07:47:42.1854485",
"readCount": 101,
"showFlag": true,
"topicId": 1,
"topic": {
    "topicId": 1,
    "title": "C#",
    "description": "About C#",
    "picture": null,
    "pictureMimeType": null,
    "postDate": "2018-05-28T07:00:00",
    "modifyDate": "2018-05-28T23:36:04.701311",
    "showFlag": true,
    "userId": 1,
    "user": {
        "userId": 1,
        "name": "",
        "password": null,
        "email": "",
        "title": "Junior Programmer!!",
        "phone": "",
        "address": "",
        "introduction": "Hey, I'm Jason, most motivated person to be a fullstack programmer! ",
        "birthdate": null,
        "picture": null,
        "pictureMimeType": null,
        "permissionId": 0,
        "permission": null,
        "topics": [],
        "articles": [
            {
                "articleId": 5,
                "title": "Does TaskAll() really wait for all in any case?",
                "content": null,
                "category": "Threading",
                "picture": null,
                "pictureMimeType": null,
                "postDate": "2018-06-18T07:00:00",
                "modifyDate": "2018-06-20T07:47:42.1854485",
                "readCount": 101,
                "showFlag": true,
                "topicId": 1,
                "userId": 1
            }
        ]
    },
    "articles": [
        {
            "articleId": 5,
            "title": "Does TaskAll() really wait for all in any case?",
            "content": null,
            "category": "Threading",
            "picture": null,
            "pictureMimeType": null,
            "postDate": "2018-06-18T07:00:00",
            "modifyDate": "2018-06-20T07:47:42.1854485",
            "readCount": 101,
            "showFlag": true,
            "topicId": 1,
            "userId": 1,
            "user": {
                "userId": 1,
                "name": "",
                "password": null,
                "email": "",
                "title": "Junior Programmer!!",
                "phone": "",
                "address": "",
                "introduction": "Hey, I'm Jason, most motivated person to be a fullstack programmer! ",
                "birthdate": null,
                "picture": null,
                "pictureMimeType": null,
                "topics": [],
                "articles": []
            }
        }
    ]
},
"userId": 1,
"user": {
    "userId": 1,
    "name": "",
    "password": null,
    "email": "",
    "title": "Junior Programmer!!",
    "phone": "",
    "address": "",
    "introduction": "Hey, I'm Jason, most motivated person to be a fullstack programmer! ",
    "birthdate": null,
    "picture": null,
    "pictureMimeType": null,
    "topics": [
        {
            "topicId": 1,
            "title": "C#",
            "description": "About C#",
            "picture": null,
            "pictureMimeType": null,
            "postDate": "2018-05-28T07:00:00",
            "modifyDate": "2018-05-28T23:36:04.701311",
            "showFlag": true,
            "userId": 1,
            "articles": [
                {
                    "articleId": 5,
                    "title": "Does TaskAll() really wait for all in any case?",
                    "content": null,
                    "category": "Threading",
                    "picture": null,
                    "pictureMimeType": null,
                    "postDate": "2018-06-18T07:00:00",
                    "modifyDate": "2018-06-20T07:47:42.1854485",
                    "readCount": 101,
                    "showFlag": true,
                    "topicId": 1,
                    "userId": 1
                }
            ]
        }
    ],
    "articles": [
        {
            "articleId": 5,
            "title": "Does TaskAll() really wait for all in any case?",
            "content": null,
            "category": "Threading",
            "picture": null,
            "pictureMimeType": null,
            "postDate": "2018-06-18T07:00:00",
            "modifyDate": "2018-06-20T07:47:42.1854485",
            "readCount": 101,
            "showFlag": true,
            "topicId": 1,
            "topic": {
                "topicId": 1,
                "title": "C#",
                "description": "About C#",
                "picture": null,
                "pictureMimeType": null,
                "postDate": "2018-05-28T07:00:00",
                "modifyDate": "2018-05-28T23:36:04.701311",
                "showFlag": true,
                "userId": 1,
                "articles": []
            },
            "userId": 1
        }
    ]
}

}

【问题讨论】:

  • 使用数据传输对象

标签: entity-framework-core asp.net-core-2.0 asp.net-core-webapi asp.net-core-2.1


【解决方案1】:

实际上,没有“重复”的数据。

当执行以下查询时:

var article = await this._context.Articles
     .Include(a => a.User)
     .Include(a => a.Topic)
     .SingleOrDefaultAsync(a => a.ArticleId == id);

幕后的 sql 将是:

SELECT TOP(1) 
    [a].[ArticleId], [a].[Category], [a].[Content], [a].[ModifyDate], [a].[Picture], [a].[PictureMimeType], [a].[PostDate],[a].[ReadCount], [a].[ShowFlag], [a].[Title], 
    [a].[TopicId], [a].[UserId], 

    [a.Topic].[TopicId], [a.Topic].[Description], [a.Topic].[ModifyDate], [a.Topic].[Picture], [a.Topic].[PictureMimeType], [a.Topic].[PostDate], [a.Topic].[ShowFlag], [a.Topic].[Title],[a.Topic].[UserId], 

    [a.User].[UserId], [a.User].[Address], [a.User].[Birthdate], [a.User].[Email], [a.User].[Introduction], [a.User].[Name], [a.User].[Password], [a.User].[Phone], [a.User].[Picture], [a.User].[PictureMimeType], [a.User].[Title]
FROM [Article] AS [a]
INNER JOIN [Topics] AS [a.Topic] ON [a].[TopicId] = [a.Topic].[TopicId]
INNER JOIN [Users] AS [a.User] ON [a].[UserId] = [a.User].[UserId]
WHERE [a].[ArticleId] = @__id_0

SQL 不会加载任何额外的记录。假设一个用户有两篇文章:

Article1 : { ArticleId=1, UserId=1}
Article3 : { ArticleId=3, UserId=1}

上面的查询只会加载一篇文章。

当记录从服务器加载到内存中时,EFCore 知道 User 和 Articles 之间存在1-to-many 的关系,并开始将 user 对象设置为 article 的导航属性。用户实体和主题实体之间也是如此。所有这些都在记忆中完成。

如您所见,没有从数据库中检索到额外记录,内存中也没有重复数据。

让你感到困惑的是序列化到客户端的数据。不过,这确实有道理。由于纯字符串没有引用/指针的概念,因此很难用 json 表示对象之间的关系。

顺便说一句,没有必要用两个几乎相同的属性来装饰你的 User 的 Email 属性:

[DataType(DataType.EmailAddress)]
[EmailAddress]
public string Email { get; set; }

迁移数据库时会出现“两个相同的自定义属性”的错误。

【讨论】:

  • 正确,我还检查了 SQL Profiler,它只加入每个表一次。这意味着,EF Core 只从数据库中读取一次,但问题是,它被多次映射,其中包含图像数据、图片属性,这导致 API 返回数据时数据量很大。有什么办法可以避免吗?
  • @JasonKim 我认为最好编写一个继承自 NonEmbeddingResult&lt;T&gt; 的新类 IActionResult ,使用反射来递归检测类型和 id 。每当您想返回一个“精炼”的对象时,只需返回一个新的NonEmbeddingResult&lt;T&gt;
  • 你的意思是用反射,把父母已经有的数据都去掉?
  • @JasonKim 是的。连同一个 id 。
猜你喜欢
  • 2020-06-16
  • 2018-03-19
  • 1970-01-01
  • 2021-12-03
  • 1970-01-01
  • 2023-04-02
  • 2018-09-29
  • 2017-08-01
  • 1970-01-01
相关资源
最近更新 更多