使用实体框架 Code First 时将外键设置为 null。

45
我正在将Entity Framework Code First的数据库优先实现用作项目的数据层,但遇到了问题。我需要能够将外键设置为null,以便从数据库中删除一个关联。我有两个对象,其中一个叫做Project。
public class Project
{
    public int ProjectId {get; set;}
    public Employee Employee {get;set;}
}

public class Employee
{
    public int EmployeeId {get; set;}
    public string EmployeeName {get;set;}
}

这与我在数据库中的内容相匹配:

CREATE TABLE Project(
    ProjectId int IDENTITY(1,1) NOT NULL,
    EmployeeId int NULL
)

CREATE TABLE Project(
    EmployeeId int IDENTITY(1,1) NOT NULL,
    EmployeeName varchar(100) NULL
)

我可以为一个项目分配一个员工,但是我想能够从项目中删除一个员工,并使员工字段为空。在我的用户界面中,这将显示为“未分配任何员工”。

然而,除了直接使用 SQL 查询外,在实体框架 4.1 中似乎找不到这样做的方法。

我尝试过:

public void RemoveEmployeeFromProject(int projectId)
{
    var project = Context.Projects.FirstOrDefault(x => x.ProjectId == projectId);
    project.Employee = null;
    Context.SaveChanges();
}

但这并没有起到任何作用。

有人有什么想法吗?

6个回答

73

我认为问题在于,就上下文而言,您实际上没有改变任何东西。

您可以使用先前建议的懒加载方法,使用virtual,但由于您尚未请求加载Employee对象,因此它仍然为空。您可以尝试以下方法:

var forceLoad = project.Employee;
project.Employee = null; // Now EF knows something has changed
Context.SaveChanges();

或者,您可以在原始请求中明确包含它:

var project = Context.Projects.Include(x => x.Employee).FirstOrDefault(x => x.ProjectId == projectId);
project.Employee = null;
Context.SaveChanges();

顺便提一下,如果没有匹配给定id的ProjectFirstOrDefault将返回null。如果您知道该项目存在,可以直接使用First。甚至可以使用Single,它会断言只有一个这样的项目。如果您继续使用FirstOrDefault,我建议在使用project之前检查是否为null


1
是的,那就是问题所在。谢谢! - mccow002
2
遇到了同样的问题,非常有帮助,给你点赞。 - Paul Stovell
2
我认为这是EF中的一个错误。请参阅问题https://entityframework.codeplex.com/workitem/2074和相关的SO问题https://dev59.com/tnzaa4cB1Zd3GeqPO1hX - Rob Kent
懒加载是罪魁祸首,浪费了一点时间,但这已经被解释和修复了。 - ransems
@David Ruttka,当我们使用存储过程时,这个怎么工作呢? var list = _db.Database.SqlQuery<Project>("spGetProjectInfo @EmployeeID", sqlParams).ToListAsync()。我得到的list.Employee = null,我该如何获取项目中的员工? - Pritish
显示剩余3条评论

14

你可以这样做,这意味着你不必加载相关实体。

context.Entry(Project).Reference(r => r.Employee).CurrentValue = null;

我喜欢这个解决方案。虽然有点啰嗦,但它自我记录了正在发生的事情。强制进行延迟加载或急切加载,然后清除属性的被接受答案对我来说有点糊弄人,并且由于不必要的加载而可能带来显着的惩罚。谢谢。 - Jason Tyler

4
答案很简单。根据提供的信息,EF 无法推断出类型。
请改用以下方式:
public void RemoveEmployeeFromProject(int projectId)
{
    var project = Context.Projects.FirstOrDefault(x => x.ProjectId == projectId);
    project.EmployeeId = (int?)null;
    Context.SaveChanges();
}

并且它将会工作。


0

如果您通过将员工属性设置为虚拟属性来启用延迟加载,它是否有效?

public class Project
{
    public int ProjectId {get; set;}
    public virtual Employee Employee {get;set;}
}

我还建议将删除方法封装为您的 POCO 类的一部分,以使其含义更清晰。请参阅this文章以获取更多详细信息。
public class Project
{
    public int ProjectId {get; set;}
    public virtual Employee Employee {get;set;}
    public void RemoveEmployee()
    {
        Employee = null;
    }
}

很遗憾,不行。我认为部分原因是启用了延迟加载后,您没有请求的值将被设置为 null。因此,当您调用 Context.SaveChanges 时,它必须忽略空值,以便它不会用未加载的值替换实际值。这很有道理,直到您想要将一个值实际设置为 null。 - mccow002
我想这与延迟加载有关。 - David Wick

0
作为另一个解决方法,我把两个方法编译成了扩展方法:
public static void SetToNull<TEntity, TProperty>(this TEntity entity, Expression<Func<TEntity, TProperty>> navigationProperty, DbContext context = null)
    where TEntity : class
    where TProperty : class
{
    var pi = GetPropertyInfo(entity, navigationProperty);

    if (context != null)
    {
        //If DB Context is supplied, use Entry/Reference method to null out current value
        context.Entry(entity).Reference(navigationProperty).CurrentValue = null;
    }
    else
    {
        //If no DB Context, then lazy load first
        var prevValue = (TProperty)pi.GetValue(entity);
    }

    pi.SetValue(entity, null);
}

static PropertyInfo GetPropertyInfo<TSource, TProperty>(    TSource source,    Expression<Func<TSource, TProperty>> propertyLambda)
{
    Type type = typeof(TSource);

    MemberExpression member = propertyLambda.Body as MemberExpression;
    if (member == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a method, not a property.",
            propertyLambda.ToString()));

    PropertyInfo propInfo = member.Member as PropertyInfo;
    if (propInfo == null)
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a field, not a property.",
            propertyLambda.ToString()));

    if (type != propInfo.ReflectedType &&
        !type.IsSubclassOf(propInfo.ReflectedType))
        throw new ArgumentException(string.Format(
            "Expression '{0}' refers to a property that is not from type {1}.",
            propertyLambda.ToString(),
            type));

    return propInfo;
}

如果您有一个DbContext,那么这将允许您提供它,此时它将使用最有效的方法并将Entry Reference的CurrentValue设置为null。

entity.SetToNull(e => e.ReferenceProperty, dbContext);

如果没有提供DBContext,它将首先进行延迟加载。
entity.SetToNull(e => e.ReferenceProperty);

0

你需要在linq查询中包含要分配的属性,使用它在Project类中的相同名称:

var project = Context.Projects.Include("Employee").FirstOrDefault(x => x.ProjectId == projectId);

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