使用反射访问特性

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

方案一:重新赋值
在TerryLee、Anytao和Ding 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的源代码。

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


资料引用:http://www.knowsky.com/542040.html

在学习LINQ时,我几乎被一个困难所击倒,这就是你从标题中看到的更新数据库的操作。下面我就一步步带你走入这泥潭,请准备好砖头和口水,Follow me。

从最简单的情况入手
我们以Northwind数据库为例,当需要修改一个产品的PRoductName时,可以在客户端直接写下这样的代码:

// List 0NorthwindDataContext db = new NorthwindDataContext();
Product product = db.Products.Single(p => p.ProductID == 1);
product.ProductName = "Chai Changed";
db.SubmitChanges();
测试一下,更新成功。不过我相信,在各位的项目中不会出现这样的代码,因为它简直没法复用。好吧,让我们对其进行重构,提取至一个方法中。参数应该是什么呢?是新的产品名称,以及待更新的产品ID。嗯,好像是这样的。

public void UpdateProduct(int id, string productName)
{
    NorthwindDataContext db = new NorthwindDataContext();
    Product product = db.Products.Single(p => p.ProductID == id);
    product.ProductName = productName;
    db.SubmitChanges();
}在实际的项目中,我们不可能仅仅只修改产品名称。Product的其他字段同样也是修改的对象。那么UpdateProduct方法的签名将变成如下的形式:

public void UpdateProduct(int id,
    string productName,
    int suplierId,
    int categoryId,
    string quantityPerUnit,
    decimal unitPrice,
    short unitsInStock,
    short unitsOnOrder,
    short reorderLevel)当然这只是简单的数据库,在实际项目中,二十、三十甚至上百个字段的情况也不少见。谁能忍受这样的方法呢?这样写,还要Product对象干什么呢?

对啊,把Product作为方法的参数,把恼人的赋值操作抛给客户代码吧。同时,我们将获取Product实例的代码提取出来,形成GetProduct方法,并且将与数据库操作相关的方法放到一个专门负责和数据库打交道的ProductRepository类中。哦耶,SRP!

// List 1
// ProductRepository
public Product GetProduct(int id)
{
    NorthwindDataContext db = new NorthwindDataContext();
    return db.Products.SingleOrDefault(p => p.id == id);
}

public void UpdateProduct(Product product)
{
    NorthwindDataContext db = new NorthwindDataContext();
    db.Products.Attach(product);
    db.SubmitChanges();
}

// Client code
ProductRepository repository = new ProductRepository();
Product product = repository.GetProduct(1);
product.ProductName = "Chai Changed";
repository.UpdateProduct(product);
在这里我使用了Attach方法,将Product的一个实例附加到其他的DataContext上。对于默认的Northwind数据库来说,这样做的结果就是得到下面的异常:

// Exception 1 NotSupportException:
已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。
An attempt has been made to Attach or Add an entity that is not new,
perhaps having been loaded from another DataContext. This is not supported查看MSDN我们知道,在将实体序列化到客户端时,这些实体会与其原始DataContext分离。DataContext不再跟踪这些实体的更改或它们与其他对象的关联。这时如果要更新或者删除数据,则必须在调用SubmitChanges之前使用Attach方法将实体附加到新的DataContext中,否则就会抛出上面的异常。

而在Northwind数据库中,Product类包含三个与之相关的类(即外键关联):Order_Detail、Category和Supllier。在上面的例子中,我们虽然把Product进行了Attach,但却没有Attach与其相关联的类,因此抛出NotSupportException。

那么如何关联与Product相关的类呢?这看上去似乎十分复杂,即便简单地如Northwind这样的数据库亦是如此。我们似乎必须先获取与原始Product相关的Order_Detail、Category和Supllier的原始类,然后再分别Attach到当前的DataContext中,但实际上即使这样做也同样会抛出NotSupportException。

那么究竟该如何实现更新操作呢?为了简便起见,我们删除Northwind.dbml中的其他实体类,只保留Product。这样就可以从最简单的情况开始入手分析了。

问题重重
删除其他类之后,我们再次执行List 1中的代码,然而数据库并没有更改产品的名称。通过查看Attach方法的重载版本,我们很容易发现问题所在。

Attach(entity)方法默认调用Attach(entity, false)重载,它将以未修改的状态附加相应实体。如果Product对象没有被修改,那么我们应该调用该重载版本,将Product对象以未修改的状态附加到DataContext,以便后续操作。而此时的Product对象的状态是“已修改”,我们只能调用Attach(entity, true)方法。

于是我们将List 1的相关代码改为Attach(product, true),看看发生了什么?

// Exception 2 InvalidOperationException:
如果实体声明了版本成员或者没有更新检查策略,则只能将它附加为没有原始状态的已修改实体。
An entity can only be attached as modified without original state
if it declares a version member or does not have an update check policy.
LINQ to SQL使用RowVersion列来实现默认的乐观式并发检查,否则在以修改状态向DataContext附加实体的时候,就会出现上面的错误。实现RowVersion列的方法有两种,一种是为数据库表定义一个timestamp类型的列,另一种方法是在表主键所对应的实体属性上,定义IsVersion=true特性。注意,不能同时拥有TimeStamp列和IsVersion=true特性,否则将抛出InvalidOprationException:成员“System.Data.Linq.Binary TimeStamp”和“Int32 ProductID”都标记为行版本。在本文中,我们使用timestamp列来举例。

为Products表建立名为TimeStamp、类型为timestamp的列之后,将其重新拖拽到设计器中,然后执行List 1中的代码。谢天谢地,终于成功了。

现在,我们再向设计器中拖入Categories表。这次学乖了,先在Categories表中添加timestamp列。测试一下,居然又是Exception 1中的错误!删除Categories的timestamp列,问题依旧。天哪,可怕的Attach方法里究竟干了什么?

哦,对了,Attach方法还有一个重载版本,我们来试一下吧。

public void UpdateProduct(Product product)
{
    NorthwindDataContext db = new NorthwindDataContext();
    Product oldProduct = db.Products.SingleOrDefault(p => p.ProductID == product.ProductID);
    db.Products.Attach(product, oldProduct);
    db.SubmitChanges();
}还是Exception 1的错误!

我就倒!Attach啊Attach,你究竟怎么了?

探索LINQ to SQL源代码
我们使用Reflector的FileDisassembler插件,将System.Data.Linq.dll反编译成cs代码,并生成项目文件,这有助于我们在Visual Studio中进行查找和定位。

什么时候抛出Exception 1?
我们先从System.Data.Linq.resx中找到Exception 1所描述的信息,得到键“CannotAttachAddNonNewEntities”,然后找到System.Data.Linq.Error.CannotAttachAddNonNewEntities()方法,查找该方法的所有引用,发现在两个地方使用了该方法,分别为StandardChangeTracker.Track方法和InitializeDeferredLoader方法。

我们再打开Table.Attach(entity, bool)的代码,不出所料地发现它调用了StandardChangeTracker.Track方法(Attach(entity, entity)方法中也是如此):

trackedObject = this.context.Services.ChangeTracker.Track(entity, true);在Track方法中,抛出Exception 1的是下面的代码:

if (trackedObject.HasDeferredLoaders)
{
    throw System.Data.Linq.Error.CannotAttachAddNonNewEntities();
}于是我们将注意力转移到StandardTrackedObject.HasDeferredLoaders属性上来:

internal override bool HasDeferredLoaders
{
    get
    {
        foreach (MetaAssociation association in this.Type.Associations)
        {
            if (this.HasDeferredLoader(association.ThisMember))
            {
                return true;
            }
        }
        foreach (MetaDataMember member in from p in this.Type.PersistentDataMembers
            where p.IsDeferred && !p.IsAssociation
            select p)
        {
            if (this.HasDeferredLoader(member))
            {
                return true;
            }
        }
        return false;
    }
}从中我们大致可以推出,只要实体中存在延迟加载的项时,执行Attach操作就会抛出Exception 1。这正好符合我们发生Exception 1的场景——Product类含有延迟加载的项。

那么避免该异常的方法也浮出水面了——移除Product中需要延迟加载的项。如何移除呢?可以使用DataLoadOptions立即加载,也可以将需要延迟加载的项设置为null。但是第一种方法行不通,只好使用第二种方法了。

// List 2

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

    public Product GetProductNoDeffered(int id)
    {
        NorthwindDataContext db = new NorthwindDataContext();
        //DataLoadOptions options = new DataLoadOptions();
        //options.LoadWith<Product>(p => p.Category);
        //db.LoadOptions = options;
        var product = db.Products.SingleOrDefault(p => p.ProductID == id);
        product.Category = null;
        return product;
    }

    public void UpdateProduct(Product product)
    {
        NorthwindDataContext db = new NorthwindDataContext();
        db.Products.Attach(product, true);
        db.SubmitChanges();
    }
}

// Client code
ProductRepository repository = new ProductRepository();
Product product = repository.GetProductNoDeffered(1);
product.ProductName = "Chai Changed";
repository.UpdateProduct(product);
什么时候抛出Exception 2?
按照上一节的方法,我们很快找到了抛出Exception 2的代码,幸运的是,整个项目中只有这一处:

if (asModified && ((inheritanceType.VersionMember == null) && inheritanceType.HasUpdateCheck))
{
    throw System.Data.Linq.Error.CannotAttachAsModifiedWithoutOriginalState();
}
可以看到,当Attach的第二个参数asModified为true、不包含RowVersion列(VersionMember=null)、且含有更新检查的列(HasUpdateCheck)时,会抛出Exception 2。HasUpdateCheck的代码如下:

public override bool HasUpdateCheck
{
    get
    {
        foreach (MetaDataMember member in this.PersistentDataMembers)
        {
            if (member.UpdateCheck != UpdateCheck.Never)
            {
                return true;
            }
        }
        return false;
    }
}这也符合我们的场景——Products表没有RowVersion列,并且设计器自动生成的代码中,所有字段的UpdateCheck特性均为默认的Always,即HasUpdateCheck属性为true。

避免Exception 2的方法就更简单了,为所有表都添加TimeStamp列或对所有表的主键字段上设置IsVersion=true字段。由于后一种方法要修改自动生成的类,并随时都会被新的设计所覆盖,因此我建议使用前一种方法。

如何使用Attach方法?
经过上面的分析,我们可以找出与Attach方法相关的两个条件:是否有RowVersion列以及是否存在外键关联(即需要延迟加载的项)。我将这两个条件与Attach的几个重载使用的情况总结出了一个表,在看下面这个表时,你需要做好充分的心理准备。

序号
Attach方法
RowVersion列
是否有关联
描述

1 Attach(entity) 否 否 没有修改
2 Attach(entity) 否 是 NotSupportException: 已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。
3 Attach(entity) 是 否 没有修改
4 Attach(entity) 是 是 没有修改。如果子集没有RowVersion列则与2一样。
5 Attach(entity, true) 否 否 InvalidOperationException:如果实体声明了版本成员或者没有更新检查策略,则只能将它附加为没有原始状态的已修改实体。
6 Attach(entity, true) 否 是 NotSupportException: 已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。
7 Attach(entity, true) 是 否 正常修改(强制修改RowVersion列会报错)
8 Attach(entity, true) 是 是 NotSupportException: 已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。
9 Attach(entity, entity) 否 否 DuplicateKeyException:不能添加其键已在使用中的实体。

10 Attach(entity, entity) 否 是 NotSupportException: 已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。
11 Attach(entity, entity) 是 否 DuplicateKeyException:不能添加其键已在使用中的实体。

12 Attach(entity, entity) 是 是 NotSupportException: 已尝试Attach或Add实体,该实体不是新实体,可能是从其他DataContext中加载来的。不支持这种操作。

Attach居然只能在第7种情况(包含RowVersion列并且无外键关联)时才能正常更新!而这种情况对于一个基于数据库的系统来说,几乎不可能出现!这是一个什么样的API啊?

总结
让我们平静一下心情,开始总结吧。

如果像List 0那样,直接在UI里写LINQ to SQL代码,则什么不幸的事也不会发生。但是如果要抽象出一个单独的数据访问层,灾难就会降临。这是否说明LINQ to SQL不适合多层架构的开发?很多人都说LINQ to SQL适合小型系统的开发,但小型不意味着不分层啊。有没有什么办法避免这么多的异常发生呢?

本文其实已经给出了一些线索,在本系列的下一篇随笔中,我将尝试着提供几种解决方案供大家选择。


-

资料引用:http://www.knowsky.com/542041.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值