使用LINQ to SQL更新数据库(中):几种解决方案

前一篇文章中,我提出了在使用LINQ to SQL进行更新操作时可能会遇到的几种问题。其实这并不是我一个人遇到的问题,当我在互联网上寻找答案时,我发现很多人都对这个话题发表过类似文章。但另我无法满足的是,他们尽管提出了问题,却没有进行详细的剖析,只给出了解决方案(如添加RowVersion列、去除关联等),但却没有说明为什么必须这么做。这也是我写上篇的初衷,希望通过对LINQ to SQL源代码的分析,来一步一步找出解决问题的办法。本文将对这些方法一一进行讨论。

方案一:重新赋值

TerryLeeAnytaoDing Xue等人的开源框架Ezsocio中,有些地方采取了重新赋值的方法。在Update方法内部,根据主键获取数据库中的实体,然后与参数中的实体对其属性一一赋值。

public void UpdateProfile(Profile p)
{
    using (RepositoryContext db = new RepositoryContext())
    {
        var profile = db.GetTable<Profile>().First<Profile>(u => u.ID == p.ID);
        profile.Birthday = p.Birthday;
        profile.Gender = p.Gender;
        profile.Hometown = p.Hometown;
        profile.MSN = p.MSN;
        profile.NickName = p.NickName;
        profile.PhoneNumber = p.PhoneNumber;
        profile.QQ = p.QQ;
        profile.State = p.State;
        profile.TrueName = p.TrueName;
        profile.StateRefreshTime = p.StateRefreshTime;
        profile.Avatar = p.Avatar;
        profile.Website = p.Website;
        db.SubmitChanges();
    }
}

杨过兄也同样给出了该方案的反射方法,实现属性值的自动拷贝。

但我个人认为这是一种避实就虚的方案,没有使用LINQ to SQL提供的用于更新操作的API,而采取了一种迂回的策略。这其实是一种妥协,难道因为Attach方法“不好用”,我们就不用了吗?呵呵。

方案二:禁用对象跟踪

对此,lea提出可以通过将DataContext的ObjectTrackingEnabled属性设置为false,来达到正确更新的目的。

public Product GetProduct(int id)
{
    NorthwindDataContext db = new NorthwindDataContext();
    db.ObjectTrackingEnabled = false;
    return db.Products.SingleOrDefault(p => p.ProductID == id);
}

其他的代码没有任何变化。

为什么禁用对象跟踪之后,就能正常更新了呢?我们还是从源代码中来寻找答案吧。

public bool ObjectTrackingEnabled
{
    get
    {
        this.CheckDispose();
        return this.objectTrackingEnabled;
    }
    set
    {
        this.CheckDispose();
        if (this.Services.HasCachedObjects)
        {
            throw System.Data.Linq.Error.OptionsCannotBeModifiedAfterQuery();
        }
        this.objectTrackingEnabled = value;
        if (!this.objectTrackingEnabled)
        {
            this.deferredLoadingEnabled = false;
        }
        this.services.ResetServices();
    }
}

原来设置ObjectTrackingEnabled为false时,会同时将DeferredLoadingEnabled设置为false。这样,在执行查询时,将不会为实体加载任何需延迟查询的数据,因此Attach时也不会抛出异常(见上篇的分析)。

在MSDN中我们还得到下面这条有用的信息:将ObjectTrackingEnable属性设置为false,可以提高检索时的性能,因为这样可以减少要跟踪的项目。这真是一个很有诱惑的特性。

但禁用对象跟踪时,要特别注意两点:(1)必须在执行查询前禁用。(2)禁用之后不能再调用Attach和SubmitChanges方法。否则都将引发异常。

方案三:移除关联

前一篇文章中已经介绍一个蹩脚的方法,即在GetProduct方法中手动设置与Product关联的Category为null。我们可以把这部分代码提取出来,放入一个Detach方法中。因为这个Detach是实体的方法,可以使用分部类:

public partial class Product
{
    public void Detach()
    {
        this._Category = default(EntityRef<Category>);
    }
}

public partial class Category
{
    public void Detach()
    {
        foreach (var product in this.Products)
        {
            product.Detach();
        }
    }
}

但是这种对每个实体都定义Detach的方法过于繁琐。随着实体的增多,关系越来越复杂,很容易出现漏掉的属性。张逸提出了一个非常优雅的方法,利用反射对该逻辑进行抽象:

private void Detach(TEntity entity)
{
    foreach (FieldInfo fi in entity.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance))
    {
        if (fi.FieldType.ToString().Contains("EntityRef"))
        {
            var value = fi.GetValue(entity);
            if (value != null)
            {
                fi.SetValue(entity, null);
            }
        }
        if (fi.FieldType.ToString().Contains("EntitySet"))
        {
            var value = fi.GetValue(entity);
            if (value != null)
            {
                MethodInfo mi = value.GetType().GetMethod("Clear");
                if (mi != null)
                {
                    mi.Invoke(value, null);
                }
                fi.SetValue(entity, value);
            }
        }
    }
}

也有人认为在Detach时应该把PropertyChanging和PropertyChanged事件设置为null,但总体的思路是一样的。

方案四:使用委托

这是ZC29同学在我上一篇文章的评论里给出的方法,我个人认为非常值得借鉴。

public void UpdateProductWithDelegate(Expression<Func<Product, bool>> predicate, Action<Product> action)
{
    NorthwindDataContext db = new NorthwindDataContext();
    var product = db.Products.SingleOrDefault(predicate);
    action(product);
    db.SubmitChanges();
}
// Client code
ProductRepository repository = new ProductRepository();
repository.UpdateProductWithDelegate(p => p.ProductID == 1, p =>
    {
        p.ProductName = "Changed";
    });

使用Lambda表达式将GetProduct的逻辑植入UpdateProduct中,并且使用委托将更新逻辑也延缓执行,这样巧妙地将查找和更新放进了一个DataContext里,从而绕开了Attach。但是这种方法API有些过于复杂,对客户端编程人员的水平要求过高。而且在Update里还要执行一遍Get的逻辑,尽管性能上的损失微乎其微,但看上去总多多少少给人一种不够DRY的感觉。

方案五:使用UPDATE语句

Ezsocio的源代码中,我发现了RepositoryBase.UpdateEntity方法。在方法内部进行SQL语句的拼接,并且将只更新发生更改的列。由于此处已经不再使用ITable,并且需要完整的框架支持,因此不再进行过多的评述。详情请参考Ezsocio的源代码。

总结

本文列举了近几天我在互联网上找到的几种解决方案,它们各有利弊,孰优孰劣,见仁见智。在下篇中,我将对这几种方法进行性能上的比较,从而找出最优方案。

1
0
(请您对文章做出评价)
« 上一篇:使用LINQ to SQL更新数据库(上):问题重重
» 下一篇:使用LINQ to SQL更新数据库(下):性能测试
posted @ 2010-01-22 09:29 麒麟.NET 阅读(1663) 评论(17)  编辑 收藏 所属分类: [07] LINQ之美

  回复  引用  查看    
#1楼2010-01-22 09:51 | 麒麟      
学习了,学习这种剖根问底的方法
  回复  引用  查看    
#2楼[楼主]2010-01-22 09:57 | 麒麟.NET      
KK昨天也提出了一个解决方案,与方案二类似
http://www.cnblogs.com/chwkai/archive/2010/01/21/linq-sql-attach.html

  回复  引用  查看    
#3楼2010-01-22 10:01 | 四有青年      
个人比较倾向用第二种和第三种
  回复  引用  查看    
#4楼2010-01-22 10:49 | 我想我是风      
我觉得方法4好

因为最终客户给的实体可能不是数据库自动生成的这些实体,还是要转换一次的,正好通过它转换


ProductRepository repository = new ProductRepository();
repository.UpdateProductWithDelegate(p => p.ProductID == 1, p =>
{
p.ProductName = 外部实体.ProductName ;
});

  回复  引用  查看    
#5楼2010-01-22 10:54 | 海南K.K      
我觉得用委托来实现更新的方法挺有趣的,赞一个。
  回复  引用  查看    
#6楼2010-01-22 11:09 | 我想我是风      
http://www.infoq.com/cn/articles/expression-compiler

方法4 有一个展望,非常不错

// imaginary code; this doesn’t work

myDatabase.Customers

.Where(c => c.Region == "North")

.Update(c => {

c.Manager = "Fred";

c.Priority = c.Priority + 10;

});

  回复  引用  查看    
#7楼2010-01-22 11:14 | Lovell      
总觉得LINQ TO SQL不适合做分层 小型的应用很快捷 稍微分下层就要多出好多代码 CRUD操作几乎都要写额外的代码 失去了自动ORM的作用 小弟不才 多多指教
  回复  引用  查看    
#8楼2010-01-22 11:36 | 暗香浮动      
第四种方案很不错的想法
  回复  引用  查看    
#9楼2010-01-22 14:11 | rad      
个人感觉反射重新赋值从易用的角度来说比较好....
不过反射时需要考虑多点,例如只更新Column的属性以及新值为空等等

  回复  引用  查看    
#10楼2010-01-22 14:15 | 轩辕      
我现在的做法是把Linq to sql所生成的类文件中所有field的UpdateCheck属性改成Never,不知道会不会出现问题。
  回复  引用  查看    
#11楼2010-01-29 11:05 | Silent Void      
貌似MSDN上有一句话:
在尝试进行更新之前,不要将从数据库中检索数据作为一种获取原始值的方式。

  回复  引用  查看    
#12楼[楼主]2010-01-29 11:19 | 麒麟.NET      
@Silent Void
那如果要修改数据库中的数据,应该如何获取原始对象?

  回复  引用  查看    
#13楼2010-01-29 13:16 | Silent Void      
@麒麟.NET
N 层应用程序中的数据检索和 CUD 操作 (LINQ to SQL)
http://msdn.microsoft.com/zh-cn/library/bb546187.aspx

我个人觉得加时间戳最简单,写条SQL,把所有的表都加上时间戳列/:)

  回复  引用  查看    
#14楼[楼主]2010-01-29 13:24 | 麒麟.NET      
  回复  引用  查看    
#15楼2010-01-29 15:20 | Silent Void      
恩,加上时间戳,然后在引用上下面这个原则:
"在尝试进行更新之前,不要将从数据库中检索数据作为一种获取原始值的方式。"

可以过滤掉文中的几个解决方案。

  回复  引用  查看    
#16楼[楼主]2010-01-29 17:00 | 麒麟.NET      
@Silent Void
还是不太明白,如果遵循"在尝试进行更新之前,不要将从数据库中检索数据作为一种获取原始值的方式。",那么究竟应该如何获取原始值?

  回复  引用  查看    
#17楼2010-01-29 17:22 | Silent Void      
@麒麟.NET
不要获取原始值,譬如方案5中的Update语句;
或者由“上层”传进来,至于“上层”从哪儿得到原始值,可能从缓存中,也可能是读数据库获取的,或者其它途径获取的;中间的这个更新接口里面要做的工作只是更新,而不是获取原始值后再更新,不出现“从数据库中检索数据作为原始值”.
个人理解。