Files
myAi/Apis/cv-matcher-data/Repositories/EfMatcherRepository.cs
T
claude 87de7d3f77 Fix duplicate key violation in CvMatchResults by updating unique constraint to 3 columns
The Results table had a unique constraint on (CvDocumentId, JobDocumentId) but the code
expects uniqueness on (CvDocumentId, JobDocumentId, Language). When matching the same CV
against the same job in different languages, this caused duplicate key violations.

Changes:
- Updated CvMatcherDbContext to define 3-column unique index including Language
- Generated proper EF Core migration to drop 2-column index and create 3-column index
- Updated ModelSnapshot to reflect new 3-column index definition
- Added exception handling in SaveMatchAsync to gracefully handle any race conditions
  where duplicate key violations could occur between the existence check and insert

The migration will be automatically applied on container startup via db.Database.Migrate().

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-01 16:13:58 +03:00

100 lines
3.7 KiB
C#

using System.Text.Json;
using CvMatcher.Data;
using CvMatcher.Data.Entities;
using CvMatcher.Data.Repositories.Contracts;
using CvMatcher.Models.Responses;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CvMatcher.Data.Repositories;
public sealed class EfMatcherRepository : IMatcherRepository
{
private readonly CvMatcherDbContext _db;
private readonly ILogger<EfMatcherRepository> _logger;
public EfMatcherRepository(CvMatcherDbContext db, ILogger<EfMatcherRepository> logger)
{
_db = db;
_logger = logger;
}
public async Task InitializeAsync(CancellationToken ct)
{
_logger.LogInformation("Ensuring CV matcher database schema exists using EF Core");
//await _db.Database.EnsureCreatedAsync(ct);
}
public async Task<JobMatchResponse?> GetMatchAsync(string cvDocumentId, string jobDocumentId, string language, CancellationToken ct)
{
var json = await _db.CvMatchResults
.AsNoTracking()
.Where(x => x.CvDocumentId == cvDocumentId && x.JobDocumentId == jobDocumentId && x.Language == language)
.Select(x => x.ResultJson)
.FirstOrDefaultAsync(ct);
if (string.IsNullOrWhiteSpace(json)) return null;
var result = JsonSerializer.Deserialize<JobMatchResponse>(json, new JsonSerializerOptions(JsonSerializerDefaults.Web));
if (result is not null) result.Cached = true;
return result;
}
public async Task SaveMatchAsync(string cvDocumentId, string jobDocumentId, string language, JobMatchResponse response, CancellationToken ct)
{
var exists = await _db.CvMatchResults.AnyAsync(
x => x.CvDocumentId == cvDocumentId && x.JobDocumentId == jobDocumentId && x.Language == language,
ct);
if (exists) return;
try
{
_db.CvMatchResults.Add(new CvMatchResultEntity
{
Id = Guid.NewGuid().ToString("N"),
CvDocumentId = cvDocumentId,
JobDocumentId = jobDocumentId,
Language = language,
ResultJson = JsonSerializer.Serialize(response, new JsonSerializerOptions(JsonSerializerDefaults.Web)),
Score = response.Score,
CreatedAt = DateTime.UtcNow
});
await _db.SaveChangesAsync(ct);
}
catch (DbUpdateException ex) when (ex.InnerException?.Message.Contains("IX_Results_CvDocumentId_JobDocumentId_Language") == true
|| ex.InnerException?.Message.Contains("unique") == true)
{
// Duplicate key violation: record was inserted between the AnyAsync check and SaveChangesAsync.
// This is safe to ignore — the match result already exists in the database.
}
}
public async Task<string?> GetChatCompletionAsync(string cacheKey, CancellationToken ct)
{
return await _db.CvMatcherChatCache
.AsNoTracking()
.Where(x => x.CacheKey == cacheKey)
.Select(x => x.ResponseText)
.FirstOrDefaultAsync(ct);
}
public async Task SaveChatCompletionAsync(string cacheKey, string model, decimal temperature, string responseText, CancellationToken ct)
{
var exists = await _db.CvMatcherChatCache.AnyAsync(x => x.CacheKey == cacheKey, ct);
if (exists) return;
_db.CvMatcherChatCache.Add(new CvMatcherChatCacheEntity
{
CacheKey = cacheKey,
Model = model,
Temperature = temperature,
ResponseText = responseText,
CreatedAt = DateTime.UtcNow
});
await _db.SaveChangesAsync(ct);
}
}