Reputation: 635
The Problem
My concept is that when a user creates a post with some tags, the server first checks if the tag name already exists, if it does, it's counter is incremented, otherwise a new tag is created.
The problem comes when multiple users create a post at the same time, with a new tag, lets say new_tag
, then multiple tags of the same name get persisted in the database instead of 1 tag with counter = # of users who used this tag
As you can see for each user, a new tag record is created in the database:
--------------------------------
| 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 |
--------------------------------
This code shows how I implemented the persistence:
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
}
}
This is a piece of my ER Diagram:
Just to mention, it works as expected if one user creates the tag first, then others are just incrementing the counter and use the same tag
Upvotes: 3
Views: 637
Reputation: 356
You're looking for an atomic UPSERT statement (a combined UPDATE or INSERT).
UPSERTS are not supported by EF Core. See: https://github.com/dotnet/efcore/issues/4526
However, if you're willing to forgo change tracking, you can directly make a SQL merge statement, like this:
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);
You might call it something like this:
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)");
}
}
Upvotes: 1