首页 文章

“附加类型为T的实体失败,因为同一类型的另一个实体已经具有相同的主键值”

提问于
浏览
3

我有一个 Language 模型定义如下:

public class Language
{
    [JsonProperty("iso_639_1")]
    public string Iso { get; set; }

    [JsonProperty("name")]
    public string Name { get; set; }

    public override bool Equals(object obj)
    {
        if (!(obj is Language))
        {
            return false;
        }

        return ((Language)obj).Iso == Iso;
    }

    public override int GetHashCode()
    {
        return Iso.GetHashCode();
    }
}

这在模型 Movie 中用作 ICollection<Language> SpokenLanguages . 我正在使用我收集的信息播种我的数据库 . 当多部电影使用相同的语言时,我显然想重新使用 Languages 表中的现有条目 .

以下通过重用现有类型并添加新类型来实现:

var localLanguages = context.Languages.ToList();
var existingLanguages = localLanguages.Union(movie.SpokenLanguages);
var newLanguages = localLanguages.Except(existingLanguages).ToList();
newLanguages.AddRange(existingLanguages);
movie.SpokenLanguages = newLanguages;

这有效,但显然这是相当丑陋,而不是EF友好 . 我正在考虑将现有模型附加到EF并让它自动重用它但我似乎无法让它工作 - 我最终得到这个错误消息:

附加“Models.Movies.Language”类型的实体失败,因为同一类型的另一个实体已具有相同的主键值 . 如果图中的任何实体具有冲突的键值,则在使用“附加”方法或将实体的状态设置为“未更改”或“已修改”时,可能会发生这种情况 . 这可能是因为某些实体是新的并且尚未收到数据库生成的键值 . 在这种情况下,使用“添加”方法或“已添加”实体状态来跟踪图形,然后根据需要将非新实体的状态设置为“未更改”或“已修改” .

有问题的代码是这样的:

var localLanguages = context.Languages.ToList();
foreach (var language in movie.SpokenLanguages)
{
    if (localLanguages.Contains(language))
    {
        context.Languages.Attach(language);
        // no difference between both approaches
        context.Entry(language).State = EntityState.Unchanged;
    }
}

将状态设置为 UnchangedModified 没有任何区别 . 我收到的JSON响应是

{
    "iso_639_1": "en",
    "name": "English"
}

这两个字段与数据库中存在的值完全相同 .

数据库中的每个插入都会创建一个新的上下文并对其进行处理 .

如何让EF重新使用现有的语言条目而不必自己筛选它们?

2 回答

  • 1

    我已经编辑了模型,因此它现在包含一个字段 Id 并将其用作主键 . 其他一切,包括平等比较,都保持不变 . 我现在收到一条不同的错误消息,可能会对此问题有所了解:

    {“INSERT语句与FOREIGN KEY约束冲突”FK_dbo.MovieLanguages_dbo.Languages_LanguageId“ . 冲突发生在数据库”MoviePicker“,表”dbo.Languages“,列'Id' . 语句已被终止 . ”}其他信息:保存不公开其关系的外键属性的实体时发生错误 . EntityEntries属性将返回null,因为无法将单个实体标识为异常的来源 . 通过在实体类型中公开外键属性,可以更轻松地在保存时处理异常 . 有关详细信息,请参阅InnerException .

    我在datacontext中记录了SQL语句,这是最后执行的语句:

    INSERT [dbo].[MovieLanguages]([MovieId], [LanguageId])
    VALUES (@0, @1)
    
    -- @0: '2' (Type = Int32)  
    -- @1: '0' (Type = Int32)
    

    这表明没有填写 LanguageId (表 Language 中的字段 Id ) . 这是有道理的,因为它默认为0,而我所做的就是将它附加到EF配置 . 这不会尝试创建对ID为0且不存在的条目的引用 .

    知道了这一点,我就选择了我所拥有的和我的目标 . 首先,我看一下该语言是否已经在数据库中 . 如果不是,一切都保持正常,我只需插入它 . 如果它已经在那里,我将其ID分配给新的 Language 对象,分离现有对象并附加新对象 .

    基本上我交换了EF跟踪的对象 . 如果它在注意到对象平等时会自己做这件事会非常有帮助但是直到它做到这一点,这是我想出的最好的 .

    var localLanguages = _context.Languages.ToList();
    foreach (var language in movie.SpokenLanguages)
    {
        var localLanguage = localLanguages.Find(x => x.Iso == language.Iso);
    
        if (localLanguage != null)
        {
            language.Id = localLanguage.Id;
            _context.Entry(localLanguage).State = EntityState.Detached;
            _context.Languages.Attach(language);
        }
    }
    
  • 0

    尝试在 Language 实体上实现 IEquatable<T> 接口(我假设 Iso 是实体主键):

    public class Language : IEquatable<Language>
    {
        [JsonProperty("iso_639_1")]
        public string Iso { get; set; }
    
        [JsonProperty("name")]
        public string Name { get; set; }
    
        public override bool Equals(object obj)
        {
            return Equals(other as Language);
        }
    
        public bool Equals(Langauge other)
        {
            // instance is never equal to null
            if (other == null) return false;
    
            // when references are equal, they are the same object
            if (ReferenceEquals(this, other)) return true;
    
            // when either object is transient or the id's are not equal, return false
            if (IsTransient(this) || IsTransient(other) ||
                !Equals(Iso, other.Iso)) return false;
    
            // when the id's are equal and neither object is transient
            // return true when one can be cast to the other
            // because this entity could be generated by a proxy
            var otherType = other.GetUnproxiedType();
            var thisType = GetUnproxiedType();
            return thisType.IsAssignableFrom(otherType) ||
                otherType.IsAssignableFrom(thisType);
        }
    
        public override int GetHashCode()
        {
            return Iso.GetHashCode();
        }
    
        private static bool IsTransient(Language obj)
        {
            // an object is transient when its id is the default
            // (null for strings or 0 for numbers)
            return Equals(obj.Iso, default(string));
        }
    
        private Type GetUnproxiedType()
        {
            return GetType(); // return the unproxied type of the object
        }
    }
    

    现在用这个,再试一次:

    var localLanguages = context.Languages.ToList(); // dynamic proxies
    foreach (var language in movie.SpokenLanguages) // non-proxied
    {
        if (localLanguages.Any(x => x.Equals(language)))
        {
            context.Entry(language).State = EntityState.Modified;
        }
    }
    

    由于EF对从上下文加载的实体实例使用动态代理,我想知道 Contains 是否作为意外的 false 值返回 . 我相信 Contains 只会进行参考比较,而不是 Equals 比较 . 由于从上下文检索的实体是动态代理实例,并且 movie.SpokenLanguages 不是, Contains 可能没有按预期进行比较 .

    参考:https://msdn.microsoft.com/en-us/library/ms131187(v=vs.110).aspx

    在Contains,IndexOf,LastIndexOf和Remove等方法中测试相等性时,IEquatable接口由泛型集合对象(如Dictionary,List和LinkedList)使用 . 它应该针对可能存储在泛型中的任何对象实现采集 .

相关问题