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

时间:2015-02-16 15:44:31

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

我有一个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”或“{{ 1}}'酌情。

有问题的代码是:

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; } } Unchanged并没有什么区别。我收到的JSON响应是

Modified

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

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

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

2 个答案:

答案 0 :(得分: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(表Id中的字段Language)未填写。这是有道理的,因为它默认为0,而我所做的就是附加它到EF配置。这不会使它假定已存在的对象的值,导致FK约束错误,因为它试图创建对ID不存在的条目的引用。

知道了这一点,我选择了我所拥有的和我的目标。首先,我看一下该语言是否已经在数据库中。如果不是,一切都保持正常,我只需插入它。如果它已经存在,我将其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);
    }
}

答案 1 :(得分:0)

尝试在IEquatable<T>实体上实施Language接口(我假设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和。等方法中实现相等   去掉。它应该针对可能存储的任何对象实现   在通用集合中。