使用Entity Framework直接更新虚拟属性而不创建新记录

4

这里是一个简单的实体:

public class Customer : Entity
{
    public virtual Location Location { get; set; }
}

现在假设我们已经有了一个客户:
var customer = new Customer() {Location = new Location("China")};

现在我们想要更新他的位置:

var customer = context.Customers.First(x => x.Location.Country == "China");
customer.Location = new Location("America");
context.SaveChanges();

现在当我查看数据库时,位置记录“中国”并没有被删除:数据库现在有两个与一个客户记录相关联的位置记录。
这个问题的原因是我在Customer.Location属性上使用了virtual关键字,当我从数据库查询客户实体时,我没有使用Include方法来加载Location属性,也没有使用任何访问来进行延迟加载。因此,EF无法跟踪和知道应该删除China Location实体。
我认为我用于更新虚拟属性的方法符合直觉。我想要更新一个属性,那么只需要使用一个更新指令“entity.xxx=...”,强制使用一些属性或方法调用来加载“entity.xxx”并不直观。
因此,我正在寻找一些更好的方法来直接替换实体的虚拟属性。有什么建议吗?

解决方案更新

我找到了两种简单的方法来完成这个任务,

第一种方法是使用识别关系推荐)。

另一种方法是使用ObjectContext.DeleteObject方法,以下是示例代码:

public static class EFCollectionHelper
{
    public static void UpdateCollection<T, TValue>(this T target, 
Expression<Func<T, IEnumerable<TValue>>> memberLamda, TValue value)where T : Entity
    {
        var memberSelectorExpression = (MemberExpression)memberLamda.Body;
        var property = (PropertyInfo)memberSelectorExpression.Member;

        var oldCollection = memberLamda.Compile()(target);
        oldCollection.ClearUp();

        property.SetValue(target, value, null);
    }

    public static void ClearUp<T>(this IEnumerable<T> collection)
    {
        //Convert your DbContext to IObjectContextAdapter
        var objContext = ((IObjectContextAdapter) Program.DbContext).ObjectContext;
        for (int i = 0; i < collection.Count(); i++)
        {
            objContext.DeleteObject(collection.ElementAt(i));
        }
    }
}

然后你可以简单地编写如下代码:
customer.UpdateCollection(x => x.Locations, null);

当你说“位置记录‘中国’未被删除:数据库现在有两个位置记录”时,你是指数据库现在有两个客户记录,一个是中国,一个是美国作为位置吗?还是你的意思是现在有两个位置记录,因为你似乎是这样说的。如果现在有两个位置记录,那并不一定是错误的。如果你的位置表是一种查找类型的表,应该有可能的位置,那么你现在在表中有两个可能的位置,并且你的客户记录现在是正确的,因为它指向了美国(American)。 - DWright
@DWright,不好意思让你感到困惑了。现在数据库中有两个与一个客户相关联的位置记录,是的,customer.Location将指向美国,但每当我使用“customer.Location = new Location()”更新客户位置时,它将在数据库中生成一个位置记录。 - 小广东
另一种方法是,如果虚拟属性是一个集合,那么使用 "customer.collection=xxx..." 将会在数据库中插入一个新的集合,下次从数据库获取客户时,"customer.collection" 实际上将会有两个项目。 - 小广东
2个回答

4

我不完全确定你想要什么,但这是我得到的信息。

你现在获取两个位置的原因是因为你使用了new Location("American");,实际上你添加了一个新位置的引用(EF不知道China是否被其他客户使用,并且在这种类型的查询中永远不会删除它)

如果你这样说:

customer.Location.Country = "America"

由于我们正在处理特定 位置 的属性,因此 中国 将被 美国 覆盖。

阅读问题评论以获取一些额外信息。

如果您想完全更新位置(new Location("Some new location")),则可以像这样操作。

Location oldLocation = customer.Location;
Location newLocation = new Location("America");
//Check if the new location country !exist
if(!context.Locations.Any(a=> a.Country == newLocation.Country))
{
    //If it don't exist then add it (avoiding the location duplicate)
    customer.Location = newLocation;
    //If its important to delete the old location then do this
    //(important to do after you removed the dependency, 
    //thats why its after the new location is added)
    context.Locations.Remove(oldLocation)
    //Finally Save the changes
    context.SaveChanges();
}

是的,我已经尝试了你建议的方法,实际上它运行良好。但是我真正想要的是“替换”,而不仅仅是“更新”。如果我想要更新客户的集合属性,因为新的集合项计数可能与旧的集合不相等,这个解决方案将无法工作。 - 小广东
一个数据库不能替换,这需要数据库找到数据并运行删除SQL查询。 - Thomas Andreè Wang
没错!那么,当我使用“entity.xxxx = ...”时,有没有办法让EF生成删除SQL查询? - 小广东
修改了答案。在更改位置和之后删除实体之前,您可以先将其暂存。 (有点未经测试,只是从我的头脑中写出来的,但应该可以工作) - Thomas Andreè Wang
是的,它应该可以工作,现在我正在尝试研究更简洁的方法来完成这个,非常感谢 :) - 小广东

1
使用Entry.OriginalValues.SetValues方法是更新实体的另一种方式:
var currentLocation = context.Customers.First(x => x.Location.Country == "China").Select(c => c.Location);
context.Entry(currentLocation).OriginalValues.SetValues(newLocation) ;
context.SaveChanges();

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