"Attaching an entity of type T failed because another entity of the same type already has the same primary key value"

"Attaching an entity of type T failed because another entity of the same type already has the same primary key value"

我有一个 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 table.

中的现有条目

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

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 并让它自动重新使用它,但我似乎无法让它工作——我最终收到此错误消息:

Attaching an entity of type 'Models.Movies.Language' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.

有问题的代码是这样的:

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

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

The IEquatable interface is used by generic collection objects such as Dictionary, List, and LinkedList when testing for equality in such methods as Contains, IndexOf, LastIndexOf, and Remove. It should be implemented for any object that might be stored in a generic collection.

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

{"The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.MovieLanguages_dbo.Languages_LanguageId". The conflict occurred in database "MoviePicker", table "dbo.Languages", column 'Id'. The statement has been terminated."}

Additional information: An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.

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

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

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

这说明LanguageId(字段Id in table Language)没有填写,这是有道理的,因为默认是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);
    }
}