【问题标题】:"Attaching an entity of type T failed because another entity of the same type already has the same primary key value"“附加类型 T 的实体失败,因为同一类型的另一个实体已经具有相同的主键值”
【发布时间】:2015-02-16 15:44:31
【问题描述】:

我有一个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”的实体失败,因为同一类型的另一个实体已经具有相同的主键值。如果图中的任何实体具有冲突的键值,则在使用“Attach”方法或将实体的状态设置为“Unchanged”或“Modified”时,可能会发生这种情况。这可能是因为某些实体是新实体,尚未收到数据库生成的键值。在这种情况下,使用“Add”方法或“Added”实体状态来跟踪图形,然后将非新实体的状态设置为“Unchanged”或“Modified”视情况而定。

有问题的代码是这样的:

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 重新使用现有的语言条目,而不必自己筛选它们?

【问题讨论】:

标签: c# entity-framework entity-framework-6


【解决方案1】:

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

{“INSERT 语句与FOREIGN KEY 约束“FK_dbo.MovieLanguages_dbo.Languages_LanguageId”冲突。冲突发生在数据库“MoviePicker”、表“dbo.Languages”、列“Id”中。该语句已终止。"}

附加信息:保存不为其关系公开外键属性的实体时发生错误。 EntityEntries 属性将返回 null,因为无法将单个实体标识为异常源。通过在实体类型中公开外键属性,可以更轻松地在保存时处理异常。有关详细信息,请参阅 InnerException。

我在数据上下文中记录了 SQL 语句,这是最后执行的语句:

INSERT [dbo].[MovieLanguages]([MovieId], [LanguageId])
VALUES (@0, @1)

-- @0: '2' (Type = Int32)  
-- @1: '0' (Type = Int32)

这表明LanguageId(表Language 中的字段Id)未填写。这是有道理的,因为它默认为0,我所做的只是将它附加到EF 配置。这不会使其假定已存在对象的值,从而导致 FK 约束错误,因为它试图创建对 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);
    }
}

【讨论】:

  • 为了避免此异常,在 EF 实体上公开 FK 属性通常是一种好习惯。不过,您仍然应该在您的实体上实现 IEquatable<T> 接口,因为它处理了很多集合和动态代理,这意味着 ReferenceEquals 比较将返回 false。
【解决方案2】:

尝试在您的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

IEquatable 接口被通用集合对象使用,例如 测试时作为 Dictionary、List 和 LinkedList 在 Contains、IndexOf、LastIndexOf 和 消除。应该为可能存储的任何对象实现它 在一个通用集合中。

【讨论】:

  • 这没什么区别。我也不知道它会如何,因为默认情况下会添加一种新语言。这个关于现有 PK 的错误仍然存​​在。请注意,我的模型没有字段Id,而是使用string,因此这里不适合使用速记。
  • @JeroenVannevel 我已经更新了我的答案。我想知道动态代理是否与此有关。尝试在实体上实现IEquatable&lt;T&gt;,然后尝试Equals 比较而不是Contains
  • 如果我在if 语句中放置一个断点,它会针对数据库中已经存在的语言进行命中,因此它确实可以工作。
  • 我相信我已经找到了问题的根源并进行了修复。感谢您的思考!
猜你喜欢
  • 2015-01-08
  • 2014-07-18
  • 1970-01-01
  • 1970-01-01
  • 2017-05-13
  • 1970-01-01
  • 1970-01-01
  • 2014-06-05
  • 2017-08-10
相关资源
最近更新 更多