如何在.NET Entity Framework中插入或更新多对多表

5

这似乎应该很明显,但是实体框架的某些内容让我感到困惑,我无法使其正常工作。

简单来说,我有三个表,其中Id值是自增列:

Users (userId, username)

Categories (categoryId, categoryName)

JoinTable (UserId, CategoryId) 复合主键

在实体设计器中(这是.NET 4.0),当我导入这些表时,如预期的那样,连接表不会出现,但是用户和类别显示了关系。以下代码:

var _context = new MyContext();
var myUser = new User();
myUser.UserName = "joe";

var myCategory = new Category();
myCategory.CategoryName = "friends";

_context.Users.AddObject(myUser);  
myUser.Categories.Add(myCategory);

var saved = _context.SaveChanges();

返回错误(尽管未向数据库添加任何内容):
An item with the same key has already been added.

如果在保存之前添加以下内容:
_context.Categories.AddObject(myCategory);
myCategory.Users.Add(myUser);

我遇到了同样的错误,并且没有保存到数据库。如果在尝试关联它们之前保存myUser和myCategory对象,它们就都会被保存,但第二个保存操作会抛出错误,并且在join表中没有添加任何内容:

Cannot insert the value NULL into column 'UserId', table '...dbo.JoinTable'; column does not  allow nulls. INSERT fails. The statement has been terminated.

我显然没有理解如何插入多对多关系。我错过了什么吗?


你的ID属性是什么类型? - Craig Stuntz
仍在努力弄清楚这个问题。通过向连接表添加一个虚假的第三个字段(DateCreated),我可以强制实体设计器显示连接表。然后我可以定义关系: var myJoin = new JoinTable {UserId = myUser.UserId, CategoryId = myCategory.CategoryId};然后,当保存时,以下内容将正确插入: _context.Categories.AddObject(myCategory); myCategory.JoinTables.Add(myJoin); myJoin.User = myUser; - Gene Reddick
删除第三个字段,实体设计器将重新生成不带连接的表,然后我又会遇到“已添加具有相同键的项”错误。 - Gene Reddick
请检查User.UserId的映射。它似乎没有被识别为标识。在SSDL中是否设置了StoreGeneratedValue? - Craig Stuntz
3个回答

5

在将用户和类别实体添加到数据库后,您确实需要调用SaveChanges(),然后设置它们之间的关联。

然而,这里真正的问题是您列出的第二个异常。如果您查看SqlProfiler或调试器中的ADO.NET分析器,您会发现在第二个SaveChanges调用期间,它看起来像这样:

insert [dbo].[JoinTable]([UserId]) values (@0) select [CategoryId] from
[dbo].[JoinTable] where @@ROWCOUNT > 0 and [UserId] = @0 and [CategoryId] = scope_identity()

显然,如果您正确编写了JoinTable(两列的复合PK),这将不起作用。
如果我通过模型浏览器查看EntityModel存储,它显示JoinTable内的CategoryId列确实将StoreGeneratedPattern设置为Identity,而UserId则设置为None。在生成阶段存在复合PK时,EF为什么要这样做超出了我的理解范围。我将向微软发布此问题的错误报告,但同时您可以在生成后手动编辑edmx / ssdl文件以删除Identity指定符。找到EntityType标记下的Property标记中的StoreGeneratedPattern =“Identity”字符串并将其删除:
更改:
<Property Name="CategoryId" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />

致:

<Property Name="CategoryId" Type="int" Nullable="false" />

然后当您运行代码时,您将获得更好的插入查询(不再出现异常!):

insert [dbo].[JoinTable]([UserId], [CategoryId]) values (@0, @1)

1
我做这件事的方式是首先生成一个带有实体键的有效类别实体。
Category myCategory = _context.Categories.First(i => i.CategoryID == categoryIDToUse);

或者你可以尝试创建一个实体作为存根以减少对数据库的访问:

Category myCategory = new Category{CategoryID = categoryIDToUse };

然后使用AttachTo方法将该实体添加到ObjectContext上的实体集(CategorySet)中(您可能需要检查它是否已经附加)。然后,您可以使用Add方法将该类别添加到用户实体中。就像这样:

myUser.Categories.Add(myCategory);

调用SaveChanges()。这对我有用。


也许这是4.0版本的更改,但在我的用户下没有'categoryset'对象,但有'categories':myUser.Categories.Add(myCategory); 但是我已经在上面的代码示例中使用了它。不过,关于“使用实体键生成有效类别”的部分,我不确定你具体指的是什么? - Gene Reddick
我正在使用EF v1.0。我更新了我的回答。通过“categoryset”,我指的是您为类别实体设置的任何EntitySet名称。我看到它是“Categories”,我已经更新了代码示例。希望这可以帮助你。 - DaveB
很遗憾,这就是我使用上述结果所做的事情。使用:myCategory.Users.Add(myUser) AND/OR myUser.Categories.Add(myCategory) 会返回“已经添加了具有相同键的项”的错误信息。 - Gene Reddick

0

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