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

Entity Framework Core更新数据库对象后缓存未更新,删除操作失败求助

解决EF Core自关联类别删除时的外键约束问题

我有一个ASP.NET Core Web API项目,其中Category对象存在自关联的父子关系,当前层级为:

"Pets" -> "Pet Food" -> "Dog Food" -> "Ugly Dog Food"

我想删除"Dog Food",先通过代码将"Ugly Dog Food"的父类别改为"Pet Food"以避免其成为孤儿节点,更新操作已成功写入数据库。但执行"Dog Food"的删除操作时,触发了外键约束错误:

DETAIL: Key (Id)=(0c6b1a97-d035-40ac-aa1f-ceef7144a236) is still referenced from table "Category".

at Npgsql.Internal.NpgsqlConnector.ReadMessageLong(Boolean async, DataRowLoadingMode dataRowLoadingMode, Boolean readingNotifications, Boolean isReadingPrependedMessage)
at System.Runtime.CompilerServices.PoolingAsyncValueTaskMethodBuilder1.StateMachineBox1.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlDataReader.NextResult(Boolean async, Boolean isConsuming, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
at Npgsql.NpgsqlCommand.ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.ExecuteAsync(IRelationalConnection connection, CancellationToken cancellationToken)

Exception data:
Severity: ERROR
SqlState: 23503
MessageText: update or delete on table "Category" violates foreign key constraint "FK_Category_Category_ParentId" on table "Category"
Detail: Key (Id)=(0c6b1a97-d035-40ac-aa1f-ceef7144a236) is still referenced from table "Category".

检查数据库后确认"Ugly Dog Food"的ParentId已更新为"Pet Food",且没有其他Category条目将"Dog Food"的ID作为ParentId,因此推测是Entity Framework Core的缓存未同步。明明已经将实体状态设为Modified并执行了UnitOfWork的SaveChangesAsync,为什么缓存没有更新?


相关代码

CategoryService类

using ShopBack.Database.Repositories;
using ShopBack.Database;

namespace ShopBack.Services;

public class CategoryService
{
    private readonly ICategoryRepository _categoryRepository;
    private readonly IShopUnitOfWork _unitOfWork;

    public CategoryService(IShopUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        _categoryRepository = unitOfWork.Categories;
    }

    /// <summary>
    /// 创建类别
    /// </summary>
    /// <param name="category">类别实体</param>
    public async Task CreateCategory(Category category)
    {
        await _categoryRepository.AddAsync(category);
        await _unitOfWork.SaveAsync();
    }

    /// <summary>
    /// 获取所有类别
    /// </summary>
    public async Task<IEnumerable<Category>> GetAllCategories()
    {
        return await _categoryRepository.GetAllAsync();
    }

    /// <summary>
    /// 获取所有顶级类别
    /// </summary>
    public async Task<IEnumerable<Category>> GetAllTopCategories()
    {
        return await _categoryRepository.getAllTopLevelCategories();
    }

    /// <summary>
    /// 更新类别
    /// </summary>
    /// <param name="category">类别实体</param>
    public async Task UpdateCategory(Category category)
    {
        _categoryRepository.Update(category);
        await _unitOfWork.SaveAsync();
    }

    /// <summary>
    /// 根据ID获取类别
    /// </summary>
    /// <param name="categoryId">类别ID</param>
    public async Task<Category?> GetCategory(Guid categoryId)
    {
        return await _categoryRepository.GetByIdAsync(categoryId);
    }

    /// <summary>
    /// 删除类别
    /// </summary>
    /// <param name="category">类别实体</param>
    public async Task DeleteCategory(Category category)
    {
        //todo 处理类别已被删除的情况

        _categoryRepository.Delete(category);
        await _unitOfWork.SaveAsync();
    }
}

BaseRepository类

using Microsoft.EntityFrameworkCore;

namespace ShopBack.Database.Repositories
{
    public class BaseRepository<T> : IRepositoryBase<T> where T : ModelBase
    {
        protected readonly LibraryContext _context;

        public BaseRepository(LibraryContext context)
        {
            //context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
            _context = context;
        }

        /// <summary>
        /// 获取所有实体
        /// </summary>
        public virtual async Task<IEnumerable<T>> GetAllAsync()
        {
            return await _context.Set<T>().ToListAsync();
        }

        /// <summary>
        /// 根据ID获取实体
        /// </summary>
        public virtual async Task<T?> GetByIdAsync(Guid id)
        {
            return await _context.Set<T>().FindAsync(id);
        }

        /// <summary>
        /// 添加实体
        /// </summary>
        public virtual async Task AddAsync(T entity)
        {
            await _context.Set<T>().AddAsync(entity);
        }

        /// <summary>
        /// 更新实体
        /// </summary>
        public virtual void Update(T entity)
        {
            _context.Entry(entity).State = EntityState.Modified;
        }

        /// <summary>
        /// 删除实体
        /// </summary>
        public virtual void Delete(T entity)
        {
            _context.Set<T>().Remove(entity);
        }

        /// <summary>
        /// 检查实体是否存在
        /// </summary>
        public virtual async Task<bool> ExistsAsync(Guid id)
        {
            return await _context.Set<T>().AnyAsync(x => x.Id == id);
        }
    }
}

UnitOfWork类

using ShopBack.Database.Repositories;

namespace ShopBack.Database
{
    public class UnitOfWork : IUnitOfWork
    {
        private readonly LibraryContext _context;
        private Dictionary<Type, object> _repositories;

        /// <summary>
        /// 构造函数
        /// </summary>
        public UnitOfWork(LibraryContext context)
        {
            _context = context;
            _repositories = new Dictionary<Type, object>();
        }

        public void Dispose()
        {
            _context.Dispose();
        }

        TRepository IUnitOfWork.GetRepository<TRepository, TEntity>()
        {
            var type = typeof(TEntity);

            if (_repositories.ContainsKey(type))
            {
                return (TRepository)_repositories[type];
            }
            else
            {
                var repositoryType = typeof(TRepository);
                var repGenType = repositoryType.MakeGenericType(typeof(TEntity));
                var repositoryInstance = Activator.CreateInstance(repGenType, _context);

                _repositories.Add(type, repositoryInstance);
            }

            return (TRepository)_repositories[type];
        }

        /// <summary>
        /// 保存变更
        /// </summary>
        public async Task SaveAsync()
        {
            await _context.SaveChangesAsync();
        }
    }
}

问题原因及解决方案

核心原因

EF Core的DbContext会跟踪所有加载到内存中的实体,当你更新"Ugly Dog Food"的ParentId并调用SaveChangesAsync时,数据库中的数据确实更新了,但如果"Dog Food"实体已经被加载到当前DbContext的缓存中,它的Children集合并不会被自动反向更新——EF Core不会主动同步导航属性的双向关系。

当你尝试删除"Dog Food"时,EF Core会检查它跟踪的实体关系,发现缓存中"Dog Food"的Children集合仍然包含"Ugly Dog Food",因此会认为存在引用关系,最终导致外键约束错误。

解决方案

方案1:手动更新导航属性

在修改子类别ParentId的同时,同步更新父类别的导航属性:

// 获取相关实体
var uglyDogFood = await _categoryRepository.GetByIdAsync(uglyDogFoodId);
var dogFood = await _categoryRepository.GetByIdAsync(dogFoodId);
var petFood = await _categoryRepository.GetByIdAsync(petFoodId);

if (uglyDogFood != null && dogFood != null && petFood != null)
{
    // 更新外键
    uglyDogFood.ParentId = petFood.Id;
    // 同步更新导航属性
    dogFood.Children.Remove(uglyDogFood);
    petFood.Children.Add(uglyDogFood);
    
    await _unitOfWork.SaveAsync();
}

方案2:重新加载实体

在删除前重新从数据库加载"Dog Food"实体,获取最新的关系状态:

public async Task DeleteCategory(Guid categoryId)
{
    // 不跟踪查询,获取最新数据
    var category = await _context.Set<Category>()
        .AsNoTracking()
        .FirstOrDefaultAsync(c => c.Id == categoryId);
    
    if (category != null)
    {
        // 附加到上下文后删除
        _context.Attach(category);
        _categoryRepository.Delete(category);
        await _unitOfWork.SaveAsync();
    }
}

或者刷新已跟踪的实体:

var dogFood = await _categoryRepository.GetByIdAsync(dogFoodId);
if (dogFood != null)
{
    // 重新加载实体数据
    await _context.Entry(dogFood).ReloadAsync();
    _categoryRepository.Delete(dogFood);
    await _unitOfWork.SaveAsync();
}

方案3:确保DbContext为范围生命周期

如果你的DbContext是单例或长生命周期,会导致缓存长期累积。将DbContext注册为范围生命周期(Scoped),确保每个请求使用独立的上下文,避免跨请求的缓存冲突。

方案4:禁用实体跟踪(只读场景适用)

在查询时使用AsNoTracking(),让EF Core不跟踪实体状态,但这种方式需要手动管理实体状态,仅推荐用于只读操作:

public virtual async Task<T?> GetByIdAsync(Guid id)
{
    return await _context.Set<T>().AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 14:18:15