Entity Framework中检查实体是否存在的通用方法是什么?

66

类似于在Entity Framework中检查对象是否存在的最佳方法?

我正在寻找一种通用的方法来检查DbSet中是否存在实体。例如,像这样不起作用的代码:

private DbContext DbContext { get; set; }

private DbSet<T> DbSet { get; set; }

public Boolean Exists(T entity) {
    return ((from item in this.DbSet
             where item == entity
             select item).Count() > 0);
}

在LINQ to SQL中,where item == entity是可行的,但在LINQ to Entities中似乎不行。由于实体可能具有不同的键,我不能让它们都从具有已知键的公共抽象类继承以进行比较。

我可以这样做,但我担心捕获异常作为验证过程的性能问题 这也行不通,因为只要实体被分离,就无法获取OriginalValues属性:

public Boolean Exists(T entity) {
    try {
        var current = this.DbContext.Entry(entity).OriginalValues;
        // Won't reach this line if the entity isn't in the database yet
        return true;
    }
    catch (Exception ex) {
        return false;
    }
}

2
为了澄清想要一个通用的Exists()方法的原因,我想能够创建一个Save()方法,在其中可以确定实体是需要添加到上下文(INSERT)还是附加(UPDATE)。我在问题中没有提到这一点,因为一旦有了Exists()Save()就变得微不足道了。 - Yuck
2个回答

100

您是否需要一种通用的方式来检查实体是由上下文加载还是通用的查询数据库的方式来检查实体是否存在?

对于前一种情况,请使用以下方法:

public bool Exists<T>(T entity) where T: class
{
    return this.Set<T>().Local.Any(e => e == entity);
}

对于后一种情况,请使用以下方法(它还将检查已加载的实体):
public bool Exists<T>(params object[] keys)
{
    return this.Set<T>().Find(keys) != null;
}

编辑:

EF Code First不应访问这种信息,但可以获取实体键的名称。我认为以下代码可能有效:

var objContext = ((IObjectContextAdapter)dbContext).ObjectContext;
var objSet = objContext.CreateObjectSet<T>();
var keyNames = objSet.EntitySet.ElementType.KeyMembers.Select(m => m.Name);

但这一切都没有意义。你希望采用通用方法,但是你的实体没有共享必要的信息以允许通用方法。现在你甚至说你不知道关键值。使用这种“通用”方法将需要反射和手动构建表达树。


5
使用context.Set<T>().Local.SingleOrDefault(e => e == entity);可以实现,但这当然需要将整个集合读入内存——在大多数情况下,这是不现实的。而context.Set<T>().Find(keys);可以实现,但正如我所提到的,我并不总是知道传入实体的键。除非有一种方法可以询问EF使用哪些属性作为键? - Yuck
4
有些答案不仅回答了你的问题,还会指引你走向一个之前不知道存在的新领域。我之前就知道Local属性,现在感谢您让我更深入地了解它。 - Alireza
1
应该是这样的吧 - public bool Exists<T>(T entity) where T: class?或者不是class而是其他数据类型,只是想知道。 - Mr. Blond
“this.Set<T>().Find(keys)”不会加载整个实体吗?如果这在紧密循环中,这可能会对性能产生严重影响。特别是如果你只需要检查键是否存在。 - Douglas Gaskell
如果您正在使用InMemory DbContext,则可能会出现问题,因为将不存在的实体设置为已修改可能会导致它在抛出错误之前被添加到内部存储器中。因此,如果您在catch中进行存在性检查,如果您首先尝试将其设置为已修改,则该实体可能会显示为存在。 - Francisco Chavez-Tejeda

20

感谢@Ladislav的帮助,让我找到正确的方向。这里是通用的Exists()方法的代码。

需要注意的是,此方法不需要反射,而且表现良好。唯一令我不满意的是TryGetObjectByKey()会附加找到的实体,因为我不希望Exists()有这种意外的结果,所以如果找到了实体,我必须将其分离。

public Boolean Exists(T entity) {
    var objContext = ((IObjectContextAdapter)this.DbContext).ObjectContext;
    var objSet = objContext.CreateObjectSet<T>();
    var entityKey = objContext.CreateEntityKey(objSet.EntitySet.Name, entity);

    Object foundEntity;
    var exists = objContext.TryGetObjectByKey(entityKey, out foundEntity);
    // TryGetObjectByKey attaches a found entity
    // Detach it here to prevent side-effects
    if (exists) {
        objContext.Detach(foundEntity);
    }

    return (exists);
}

3
但是,AttachSaveChanges不会保存任何更改,因为Attach将实体保留在“未更改”的状态。在Attach后,您必须将状态设置为“已修改”。但是这种方法效果不佳,因为所有列都将被更新,无论它们是否真的有所改变。要避免这种情况,您需要在上下文中加载原始数据,并在属性级别应用更改。但是,这样做就不需要使用Exists,因为这只会导致将实体加载两次。如果实体具有导航属性并且相关对象需要更新或不需要更新,那么该怎么办?我对通用的“保存”方法存在疑虑。 - Slauma
2
@Yuck:我只是在谈论“if”情况,而不是“else”情况。我不明白这怎么能节省任何东西,它甚至不会向数据库发出任何SQL命令。引用自这里:http://blogs.msdn.com/b/adonet/archive/2011/01/29/using-dbcontext-in-ef-feature-ctp5-part-4-add-attach-and-entity-states.aspx ...请注意,如果在未对附加实体进行任何其他操作的情况下调用SaveChanges,则不会对数据库进行任何更改。这是因为实体处于未更改状态... 我可以从我的测试中确认这一点。 - Slauma
3
@Yuck:不对,那是错误的。它不会更新。如果你在数据库中有一个Id=1、Name="Paul"的User,然后附加一个Id=1、Name="Mary"的User实体到上下文中,然后直接调用SaveChanges,数据库中Id=1的User仍然拥有名字"Paul"。(我也在谈论EF 4.1,但这不重要,这种行为是EF核心相关的。) - Slauma
1
事实证明,我有一些令人讨厌的副作用,这使得对象以这种方式保存。我将编辑掉我的 Save(),因为它与原始问题无关。我将开始一个新的问题来跟踪进展,并希望得到更多的评论。感谢您的帮助 @Slauma。 - Yuck

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接