无法添加类型为T的实体,因为另一个具有相同主键值的实体已经存在。

3

我有一个定义如下的语言模型:

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


语言与电影之间在MovieLanguages表中有多对多的关系。如果我简单地省略了一种语言,它将不会被记录在这个表中,而这正是在我的Movie模型中拥有该导航属性的全部意义所在。 - Jeroen Vannevel
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)

这表明Language表中的Id字段(LanguageId)未填写。这是有道理的,因为默认情况下它为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);
    }
}

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - danludwig

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/zh-cn/library/ms131187(v=vs.110).aspx

IEquatable接口用于泛型集合对象(如Dictionary、List和LinkedList)在测试等式时使用Contains、IndexOf、LastIndexOf和Remove等方法。任何可能存储在泛型集合中的对象都应该实现它。


这没有任何区别。我也不明白它会有什么区别,因为新的语言默认就被添加了。这个现有PK的错误仍然存在。请注意,我的模型没有一个名为 "Id" 的字段,而是使用了 "string",因此在这里不适用速记法。 - Jeroen Vannevel
@JeroenVannevel 我更新了我的答案。我在想动态代理是否与此有关。尝试在实体上实现 IEquatable<T>,然后尝试使用 Equals 比较而不是 Contains - danludwig
如果我在“if”语句中设置断点,对于已经存在于数据库中的语言,它会被触发,因此它实际上是有效的。 - Jeroen Vannevel
我相信我已经找到了问题的根源并且创建了一个修复方案。感谢你的思考! - Jeroen Vannevel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接