C# EF Core-同一实体的并发保存会创建多个
问题C# EF Core-同一实体的并发保存会创建多个,c#,postgresql,entity-framework,asp.net-core,entity-framework-core,C#,Postgresql,Entity Framework,Asp.net Core,Entity Framework Core,问题 我的概念是,当用户创建带有一些标记的帖子时,服务器首先检查标记名是否已经存在,如果已经存在,则其计数器将递增,否则将创建一个新标记 当多个用户同时使用一个新标记创建一篇文章时,问题就出现了,比如说new\u-tag,然后相同名称的多个标记被保存在数据库中,而不是使用此标记的用户的计数器为1的标记 如您所见,每个用户都会在数据库中创建一个新的标记记录: -------------------------------- | id | tagName | counter | |---
我的概念是,当用户创建带有一些标记的帖子时,服务器首先检查标记名是否已经存在,如果已经存在,则其计数器将递增,否则将创建一个新标记
当多个用户同时使用一个新标记创建一篇文章时,问题就出现了,比如说
new\u-tag
,然后相同名称的多个标记被保存在数据库中,而不是使用此标记的用户的计数器为1的标记如您所见,每个用户都会在数据库中创建一个新的标记记录:
--------------------------------
| id | tagName | counter |
|------|-----------|-----------|
| 1 | new_tag | 1 |
| 2 | new_tag | 1 |
| 3 | new_tag | 1 |
| 4 | new_tag | 1 |
--------------------------------
我所期望的是:
--------------------------------
| id | tagName | counter |
|------|-----------|-----------|
| 1 | new_tag | 4 |
--------------------------------
这段代码显示了我是如何实现持久性的:
PostRepository
public async Task<bool> AddAsync(Post entity)
{
await AddNewTagsAsync(entity);
_context.Attach(entity.Event);
await _context.AddAsync(entity);
await _context.Database.BeginTransactionAsync();
var result = await _context.SaveChangesAsync();
_context.Database.CommitTransaction();
return result > 0;
}
public async Task AddNewTagsAsync(Post post)
{
// store tags name in lower case
if ((post.PostTags == null) || (post.PostTags.Count==0))
return;
post.PostTags.ForEach(pt => pt.Tag.TagName = pt.Tag.TagName.ToLower());
for(var i =0; i<post.PostTags.Count; i++)
{
var postTag = post.PostTags[i];
// here lays the main problem, when many concurrent users check for tag existence
// all get null and new tag will be created, workaround needed!
var existingTag = await _context.Tags.SingleOrDefaultAsync(x => x.TagName == postTag.Tag.TagName);
// if tag exists, increment counter
if (existingTag != null)
{
existingTag.Counter++;
postTag.Tag = existingTag;
continue;
}
// else the new Tag object will be peristed
}
}
公共异步任务AddAsync(Post实体)
{
等待AddNewTagsAsync(实体);
_上下文。附加(实体。事件);
wait_context.AddAsync(实体);
wait_context.Database.BeginTransactionAsync();
var result=await_context.SaveChangesAsync();
_context.Database.CommitTransaction();
返回结果>0;
}
公共异步任务AddNewTagsAsync(Post)
{
//以小写形式存储标签名称
if((post.PostTags==null)| |(post.PostTags.Count==0))
返回;
post.PostTags.ForEach(pt=>pt.Tag.TagName=pt.Tag.TagName.ToLower());
对于(变量i=0;i x.TagName==postTag.Tag.TagName);
//如果标记存在,则递增计数器
如果(existingTag!=null)
{
existingTag.Counter++;
Tag=existingTag;
继续;
}
//否则,新标记对象将被验证
}
}
这是我的ER图的一部分:需要指出的是,如果一个用户首先创建标记,然后其他用户只是增加计数器,并使用与您要查找的原子UPSERT语句相同的标记(组合更新或插入),那么它的效果与预期一样 EF Core不支持upsert。见: 但是,如果您愿意放弃更改跟踪,可以直接生成SQL merge语句,如下所示:
MERGE INTO dbo.Tags AS target
USING (VALUES ({TagName})) AS source (TagName)
ON target.TagName = source.TagName
WHEN MATCHED THEN
UPDATE SET Counter = Counter + 1
WHEN NOT MATCHED BY TARGET THEN
INSERT (TagName, Counter) VALUES (TagName, 1);
public async Task AddNewTagsAsync(Post post)
{
foreach (var tag in post.PostTags)
{
await _context.Database.ExececuteInterpolatedAsync($@"
MERGE INTO dbo.Tags AS target
USING (VALUES ({tag.TagName})) AS source (TagName)
ON target.TagName = source.TagName
WHEN MATCHED THEN
UPDATE SET Counter = Counter + 1
WHEN NOT MATCHED BY TARGET THEN
INSERT (TagName, Counter) VALUES (TagName, 1)");
}
}
你可以这样称呼它:
MERGE INTO dbo.Tags AS target
USING (VALUES ({TagName})) AS source (TagName)
ON target.TagName = source.TagName
WHEN MATCHED THEN
UPDATE SET Counter = Counter + 1
WHEN NOT MATCHED BY TARGET THEN
INSERT (TagName, Counter) VALUES (TagName, 1);
public async Task AddNewTagsAsync(Post post)
{
foreach (var tag in post.PostTags)
{
await _context.Database.ExececuteInterpolatedAsync($@"
MERGE INTO dbo.Tags AS target
USING (VALUES ({tag.TagName})) AS source (TagName)
ON target.TagName = source.TagName
WHEN MATCHED THEN
UPDATE SET Counter = Counter + 1
WHEN NOT MATCHED BY TARGET THEN
INSERT (TagName, Counter) VALUES (TagName, 1)");
}
}
愚蠢的问题,但是
PostTags
条目的数量实际上不代表您正在寻找的计数器吗?但是,您可能希望了解EF的锁定技术,例如,在处理器时间内,\u context.Tags.SingleOrDefaultAsync…
和CommitTransaction
之间存在一个时代。这种类型的冲突只能通过唯一的数据库索引和捕获异常来解决。@GertArnold问题是,不会引发异常。所有线程都看到这个标记不存在,并创建了它。“所有线程都看到这个标记不存在”——这就是我的意思。他们有足够的时间得出这个结论。因此,是的,索引是确保最终安全的必要条件。我认为手动保存计数是不必要的,使用索引,通过tagid过滤PostTags中的计数将花费很少的时间,即使它们是数百万行。标记名上的唯一索引也可以避免重复。您应该只处理异常。