如何使用带有代码优先实体框架的 UnitOfWork 模式防止重复条目?

Posted

技术标签:

【中文标题】如何使用带有代码优先实体框架的 UnitOfWork 模式防止重复条目?【英文标题】:How do I prevent duplicate entries using the UnitOfWork pattern with code first Entity Framework? 【发布时间】:2012-01-19 02:06:40 【问题描述】:

我正在使用工作单元和通用存储库模式。这是检查重复条目的语句:

int id = int.Parse(beer.id); //id comes from the item we're hoping to insert

if (_unitOfWork.BeerRepository.GetByID(id) == null)
     \\create a new model br
     _unitOfWork.BeerRepository.Insert(br);
     _unitOfWork.save();

显然这无法检查啤酒是否已经在数据库中,因为我得到了这个内部异常:

违反主键约束“PK_啤酒_3214EC2703317E3D”。 无法在对象“dbo.Beers”中插入重复键。\r\n语句 已终止。

我也收到这条消息:

保存不暴露外部的实体时出错 他们关系的关键属性。 EntityEntries 属性 将返回 null,因为无法将单个实体标识为 异常的来源。保存时的异常处理可以是 通过在实体类型中公开外键属性变得更容易。 有关详细信息,请参阅 InnerException。

UnitOfWork 类具有实现 DbContext 的 BeerRecommenderContext,并且 UnitOfWork 具有每个实体的通用存储库:

namespace BeerRecommender.Models

public class GenericRepository<TEntity> where TEntity : class

    internal BeerRecommenderContext context;
    internal DbSet<TEntity> dbSet;

    public GenericRepository(BeerRecommenderContext context)
    
        this.context = context;
        this.dbSet = context.Set<TEntity>();
    

    public virtual IEnumerable<TEntity> Get(
        Expression<Func<TEntity, bool>> filter = null,
        Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
        string includeProperties = "")
    
        IQueryable<TEntity> query = dbSet;

        if (filter != null)
        
            query = query.Where(filter);
        

        foreach (var includeProperty in includeProperties.Split
            (new char[]  ',' , StringSplitOptions.RemoveEmptyEntries))
        
            query = query.Include(includeProperty);
        

        if (orderBy != null)
        
            return orderBy(query).ToList();
        
        else
        
            return query.ToList();
        
    

    public virtual TEntity GetByID(object id)
    
        return dbSet.Find(id);
    

    public virtual void Insert(TEntity entity)
    
        dbSet.Add(entity);
    

    public virtual void Delete(object id)
    
        TEntity entityToDelete = dbSet.Find(id);
        Delete(entityToDelete);
    

    public virtual void Delete(TEntity entityToDelete)
    
        if (context.Entry(entityToDelete).State == EntityState.Detached)
        
            dbSet.Attach(entityToDelete);
        
        dbSet.Remove(entityToDelete);
    

    public virtual void Update(TEntity entityToUpdate)
    
        dbSet.Attach(entityToUpdate);
        context.Entry(entityToUpdate).State = EntityState.Modified;
    

    

【问题讨论】:

您是否将新模型br的主键属性设置为id 我的模型类的主键是“id”。创建新啤酒时,我使用给定的 id 这应该确实有效。你做了一些非常简单的检查吗?例如:1) 记下您正在测试的id,例如5。 2) 查看数据库中是否存在 key = 5 的啤酒。 3) 检查GetByID 的结果 -> 如果DB 中有5 号啤酒但GetByID 返回null,则停止使用EF 并喝完数据库中的所有啤酒。如果没有 5 号啤酒并且 GetByID 返回 null(这是预期的),则在调用 insert 之前,如果新啤酒的 ID 确实为 5,请在调试器中检查。等等等等等等。 【参考方案1】:

我有类似的使用代码优先的存储库用法。有时,我会看到像您描述的那样的冲突。我的问题是跨多个流程的变更跟踪。您是否在一个进程内将项目插入数据库(使用单个实体上下文)?

如果您是,您应该查看实体框架提供的合并选项。如果您使用默认的合并选项(AppendOnly),那么您可以查询内存中的上下文而不是去数据库。这可能会导致您所描述的行为。

不幸的是,据我所知,所有的合并选项还没有暴露给 Code-First。你可以选择默认的(AppendOnly)或者NoTracking,每次都会去数据库。

希望这会有所帮助, 达文

【讨论】:

我只使用一个进程来执行此插入。正如您所说,它似乎正在查询内存中的上下文。在我开始使用我的 AdminController 之前,我在开发的早期没有遇到这个问题。我的 AdminController 在我的 BeersController 中调用 UpdateBeers 方法。每个控制器都有自己的 UnitOfWork。我尝试将 UnitOfWork 从 AdminController 传递到 BeersController 但这没有帮助。每次插入后都需要保存吗?我试过了,但没有帮助。 NoTracking 有什么作用? NoTracking 合并选项实际上会告诉 EF 您对使用内存缓存不感兴趣。所以每次都会去数据库查询。 EF 中还有其他可用的合并选项,但所有选项在 4.1 中尚未公开。所以,目前我们只得到 AppendOnly 和 NoTracking。这是 Web 还是 Windows 进程? UnitOfWork 应该在同一个 DbContext 上工作,以便将所有内容一起提交。因此,如果您正在进行跨控制器调用,您可能希望以某种方式全局管理上下文(singeton 等)。

以上是关于如何使用带有代码优先实体框架的 UnitOfWork 模式防止重复条目?的主要内容,如果未能解决你的问题,请参考以下文章

使用带有实体框架代码优先和 ASP.NET MVC 3 和 mvc miniprofiler 的 SQL Server CE 时出现问题

实体框架代码优先 - 为 SqlQuery 配置映射

我应该如何设置我的集成测试以使用带有实体框架的测试数据库?

实体框架代码优先迁移 - 我可以针对以前的迁移

如何使用实体框架代码优先从数据库中删除所有相关实体

具有代码优先迁移的实体框架多个项目