You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core自引用多对多添加新关联时触发重复键错误

EF Core自引用多对多关系更新时重复插入关联报错

我定义了一个名为Image的EF Core实体,通过如下配置实现自引用多对多关系:

实体类代码

public class Image {
    public int Id { get; set; }
    // ... 其他列 ...

    public List<Image> AlternativeImages { get; set; }
}

EF Core实体配置

modelBuilder.Entity<Image>()
    .HasMany(i => i.AlternativeImages)
    .WithMany()
    .UsingEntity(j => j.ToTable("ImageAlternatives"));

问题现象

添加带有任意数量替代图片的Image实体时一切正常,但为已有替代图片的Image实体添加新替代图片时(原有实体无替代图片则无问题),EF Core会尝试重新插入旧关联到连接表,触发唯一键冲突错误:

Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while saving the entity changes.
---> Npgsql.PostgresException (0x80004005): 23505: duplicate key value violates unique constraint "PK_ImageAlternatives"
DETAIL: Key ("AlternativeImagesId", "ImageId")=(..., ...) already exists.

场景示例

现有ImageA、ImageB、ImageC,若ImageA已将ImageB作为替代图片,当向ImageA的替代图片列表中添加ImageC(同时维护双向引用以保持关系对称),会抛出(ImageAId, ImageBId)键已存在的异常。尽管ImageA和ImageB的跟踪状态均为Unchanged,仅ImageC为Added状态,且用于替代图片列表的ImageA和ImageB实例是上下文已跟踪的同一实例,未被分离或重新创建。


补充说明1:复现代码

实际实现更复杂(Image属于领域模型中的另一聚合根),以下是简化后的更新操作代码可重现该错误:

public async Task UpdateAsync()
{
    using var dbContext = await _dbContextFactory.CreateDbContextAsync();
    using var transaction = await dbContext.Database.BeginTransactionAsync();
    
    try
    {
        var existingImage = await dbContext.Images
            .Include(i => i.AlternativeImages)
            .AsSplitQuery()
            .FirstOrDefaultAsync(s => s.Id == 10217);

        var tmp = new Image { ImageUrl = "..." };
        existingImage.AddAlternativeImage(tmp);

        foreach (var entry in dbContext.ChangeTracker.Entries<Image>())
        {
                Console.WriteLine("");
        }

        await dbContext.SaveChangesAsync();

        //await transaction.RollbackAsync();
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
}

AddAlternativeImage方法用于确保双向关系一致性,会将新图片添加到所有现有替代图片的列表中,同时将现有图片添加到新图片的替代列表:

internal bool AddAlternativeImage(Image image)
{
    if (image == null)
        return false;

    bool addedSomething = false;

    // Initialize the collection if it is null.
    AlternativeImages ??= new List<Image>();

    // If the provided image is NOT the main one, try to add it.
    if (!image.ImageUrl.Equals(this.ImageUrl, StringComparison.OrdinalIgnoreCase))
    {
        if (!AlternativeImages.Any(existing => existing.ImageUrl.Equals(image.ImageUrl, StringComparison.OrdinalIgnoreCase)))
        {
            AlternativeImages.Add(image);
            addedSomething = true;
        }
    }

    // Process the alternative images of 'image' regardless of whether 'image' is the same as the main one.
    if (image.AlternativeImages != null)
    {
        foreach (var alt in image.AlternativeImages)
        {
            if (!alt.ImageUrl.Equals(this.ImageUrl, StringComparison.OrdinalIgnoreCase))
            {
                if (!AlternativeImages.Any(existing => existing.ImageUrl.Equals(alt.ImageUrl, StringComparison.OrdinalIgnoreCase)))
                {
                    AlternativeImages.Add(alt);
                    addedSomething = true;
                }
            }
        }
    }

    // Unified list of all alternative images.
    var unionImages = AlternativeImages.ToList();

    foreach (var alt in unionImages)
    {
        var newAlternatives = new List<Image>();
        alt.AlternativeImages ??= new List<Image>();

        // Include the main image.
        if (!this.ImageUrl.Equals(alt.ImageUrl, StringComparison.OrdinalIgnoreCase))
            newAlternatives.Add(this);

        // Add the other alternative images.
        foreach (var other in unionImages)
        {
            if (!other.ImageUrl.Equals(alt.ImageUrl, StringComparison.OrdinalIgnoreCase))
                newAlternatives.Add(other);
        }

        // Update on the same list to avoid problems with ef core tracking.
        if (alt.AlternativeImages == null)
            alt.AlternativeImages = newAlternatives;
        else
        {
            alt.AlternativeImages?.Clear();
            alt.AlternativeImages!.AddRange(newAlternatives);
        }

    }

    return addedSomething;
}

补充说明2:连接表SQL定义

-- auto-generated definition
create table "ImageAlternatives"
(
    "AlternativeImagesId" integer not null
        constraint "FK_ImageAlternatives_Images_AlternativeImagesId"
            references "Images"
            on delete cascade,
    "ImageId"             integer not null
        constraint "FK_ImageAlternatives_Images_ImageId"
            references "Images"
            on delete cascade,
    constraint "PK_ImageAlternatives"
        primary key ("AlternativeImagesId", "ImageId")
);

内容的提问来源于stack exchange,提问作者Alex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 14:13:17