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中的计数将花费很少的时间,即使它们是数百万行。标记名上的唯一索引也可以避免重复。您应该只处理异常。