"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;
}
}
将状态设置为 Unchanged
或 Modified
没有区别。我收到的 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);
}
}
我有一个 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;
}
}
将状态设置为 Unchanged
或 Modified
没有区别。我收到的 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 theFOREIGN 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);
}
}