首页 > 解决方案 > 实体框架核心在命中 api 端点时不检查实体是否存在

问题描述

当点击 API 端点以插入实体时,EF 会将任何填充的导航属性添加到相应的表中。这很好,除非该实体已经存在,因此它会引发重复记录错误。

我正在为另一种语言创建一个单词数据库,这些单词有关系。假设我有一个 Noun 类、一个 Meaning 类、一个链接 NounMeaning 类和一个用于添加名词的 api 控制器。

    public class Noun
    {
        public int Id { get; set; }
        public string Value { get; set; }

        public List<NounMeaning> NounMeanings { get; set; }
    }
    public class Meaning
    {
        public int Id { get; set; }
        public string Value { get; set; }

        public List<NounMeaning> NounMeanings { get; set; }
    }
    public class NounMeaning
    {
        public int NounId { get; set; }
        public int MeaningId { get; set; }

        public Noun Noun { get; set; }
        public Meaning Meaning { get; set; }
    }
public class LanguageContext : DbContext
    {
        public LanguageContext(DbContextOptions<LanguageContext> options)
           : base(options)
        { }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<NounMeaning>().HasKey(nm => new { mm.NounId, nm.MeaningId });

            modelBuilder.Entity<Meaning>().HasIndex(m => m.Value).IsUnique();
        }

        public DbSet<Noun> Nouns { get; set; }
        public DbSet<Meaning> Meanings { get; set; }
    }
public async Task<IActionResult> PostNoun([FromBody] Noun noun)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            _context.Nouns.Add(noun);
            await _context.SaveChangesAsync();
{
    "Value": "Word",
    "NounMeanings": [
        { 
            "Meaning":  {
                "Value": "boy"
            } 
        }
    ]
}

如果我用上面的 JSON 命中端点,我第一次得到一个有效的响应。名词被添加到名词表中,连同含义表和链接表中的含义一起,正确添加了名词含义。

如果我再次点击端点仅更改 JSON 中的名词值,例如使用 Word2 的值,我期望发生的是名词被添加到名词表中,意义表保持不变,因为含义已经存在,并且NounMeanings 表插入另一行(例如,2、1 表示列)。

取而代之的是返回一个错误,说在含义表中会出现重复,因为它试图再次输入含义(这是来自 Postman 的错误消息):

<h2 class="stackerror">SqlException: Cannot insert duplicate key row in object
                        &#x27;dbo.Meanings&#x27; with unique index &#x27;IX_Meanings_Value&#x27;. The duplicate key
                        value is (boy).&#xD;&#xA;The statement has been terminated.</h2>
                    <ul>

我本来希望 EF 足够聪明,可以在添加记录之前检查含义是否存在。

相反,我必须添加以下逻辑以使其正确通过(请注意,下面的代码是测试它是否有效,因此是硬编码索引)。

public async Task<IActionResult> PostNoun([FromBody] Noun noun)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            Meaning meaning = _context.Meanings.SingleOrDefault(m => m.Value == noun.NounMeanings[0].Meaning.Value);

            if(meaning != null)
            {
                noun.NounMeanings = new List<NounMeaning>();
                noun.NounMeanings.Add(new NounMeaning { Meaning = meaning });
            }

            _context.Nouns.Add(noun);
            await _context.SaveChangesAsync();

这不是很好,因为这些检查需要更多时间(因为实际数据库更复杂,所以需要更多时间)并且需要额外的数据库调用。

我不记得必须在 EF 框架中执行此操作,但是自从我从头开始编写数据库以来已经很长时间了,所以也许我遗漏了一些东西。

谢谢你的帮助。这让我整个周末都很沮丧。

标签: c#entity-framework-core

解决方案


Entity Framework 从未有过您所期望的行为 - DbSet.Add 方法仅用于插入记录。如果您尝试更新现有记录,您应该附加一个实体。这适用于 EF6,但逻辑适用:https ://docs.microsoft.com/en-us/ef/ef6/saving/change-tracking/entity-state

这属于“观点”,而不是冷酷的事实,但使用数据模型(为数据库表建模的类)作为视图模型(用于在更高级别绑定数据的类,例如作为 UI)-很少(根据我的经验)在 UI 中需要与数据源所需的对象图相同的对象图。

例如,您已将其显示为数据库模型:

public class Noun
{
    public int Id { get; set; }
    public string Value { get; set; }

    public List<NounMeaning> NounMeanings { get; set; }
}

public class Meaning
{
    public int Id { get; set; }
    public string Value { get; set; }

    public List<NounMeaning> NounMeanings { get; set; }
}

public class NounMeaning
{
    public int NounId { get; set; }
    public int MeaningId { get; set; }

    public Noun Noun { get; set; }
    public Meaning Meaning { get; set; }
}

该类将作为数据库表存在,NounMeaning因此您可以进行多对多联接。在完整的框架 EF(EF6 和更少)中,您实际上并不需要它作为数据模型类 - 它足够聪明,可以进行连接。EF Core 需要连接表(EF Core 团队有理由说明为什么会出现这种情况,但您必须深入研究 Guthub 项目中的问题才能找到它们),但这并不意味着您的应用程序层需要处理用那个模型。我期待这样的事情:

public class Noun
{
    public int Id { get; set; }
    public string Value { get; set; }

    public List<Meaning> Meanings { get; set; }
}

public class Meaning
{
    public int Id { get; set; }
    public string Value { get; set; }

    public List<Noun> Nouns { get; set; }
}

这是合乎逻辑的——一个名词有多个含义,一个含义可以用多个名词来描述。开发人员可能会发现该模型在应用程序级别更易于处理,因此您将创建如上所述的 DTO(数据传输对象)类。然后,您还可以添加任何有意义的辅助方法,而不会污染您的数据模型。

实际上还有一篇 MSDN / MS Docs 文章也讨论了这个问题:https ://docs.microsoft.com/en-us/aspnet/web-api/overview/data/using-web-api-with-entity-framework /第 5 部分


推荐阅读