EF Core - 同时保存同一实体会创建多个实体

3
问题
我的概念是,当用户创建带有标签的帖子时,服务器首先检查标签名称是否已经存在,如果存在,则计数器会递增,否则将创建一个新的标签。

问题在于,当多个用户同时创建一个带有新标签(比如 new_tag)的帖子时,数据库中会持久化多个相同名称的标签,而不是带有计数器=#使用此标签的用户数量 的1个标签。

可以看到,对于每个用户,在数据库中都会创建一个新的标签记录。

--------------------------------
|  id  |  tagName  |  counter  |
|------|-----------|-----------|
|   1  |  new_tag  |    1      |
|   2  |  new_tag  |    1      |
|   3  |  new_tag  |    1      |
|   4  |  new_tag  |    1      |
--------------------------------

What I expect:

--------------------------------
|  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   
            }
        }


这是我的ER图的一部分:

This

值得一提的是,如果一个用户首先创建了标签,那么其他用户只需递增计数器并使用相同的标签,即可按预期工作。

这可能是一个愚蠢的问题,但是PostTags条目的数量实际上不代表您正在寻找的计数器吗?但是,您可能需要查看使用EF的锁定技术,例如处理并发冲突 - Gene
在处理器时间中,_context.Tags.SingleOrDefaultAsync...CommitTransaction 之间存在一个时代。这种类型的冲突只能通过唯一的数据库索引和捕获异常来解决。 - Gert Arnold
@Gene 从帖子标签中我可以技术上计算标签出现的次数,但出于性能原因,我决定坚持这种实现方式。 - Gicu Mironica
2
“所有线程都看到这个标签不存在” - 这就是我的意思。他们有足够的时间得出这个结论。因此,是的,索引是必要的,以使其最终安全。 - Gert Arnold
2
我认为手动保持计数是不必要的,使用索引,在按tagid过滤的PostTags中计数即使有数百万行也只需要极少的时间。此外,在TagName上创建唯一索引可以避免重复。你只需要处理异常情况即可。 - Diego658
显示剩余7条评论
1个回答

1
你正在寻找一个原子UPSERT语句(一个合并的UPDATE或INSERT)。
EF Core不支持UPSERT。请见:https://github.com/dotnet/efcore/issues/4526 然而,如果你愿意放弃变更跟踪,你可以直接使用SQL合并语句,例如:
    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)");
    } 
}

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