LINQ to SQL: Join一例

LINQ No Comments »

LINQ to SQL当中的灵活的查询操作是其一个很大的有点, 但是当编写较复杂的链接时有时候需要注意一些细节, 下面从我最近参与的一个项目中挑选了一个很有代表性的表关联操作来稍微做一下解释, 如下:

   1: ConsoleDataContext dc = DataContextFactory.Create();
   2: DataLoadOptions dl = new DataLoadOptions();
   3: dl.LoadWith<InstanceStatus>(t => t.StartedUser);
   4: dl.LoadWith<InstanceStatus>(t => t.LastPerformer);
   5: dl.LoadWith<InstanceStatus>(t => t.NextPerformer);
   6: dl.LoadWith<InstanceStatus>(t => t.Application);
   7: dc.LoadOptions = dl;
   8:  
   9: var q = from st in dc.InstanceStatus.Where(t => (t.StatusId == (int)WorkItemStatus.Started || t.StatusId == (int)WorkItemStatus.Running))
  10:         join d in dc.Delegations
  11:         on new 
  12:         {
  13:             ApplicationId = st.ApplicationId,
  14:             NextPerformerId = st.NextPerformerId
  15:         }
  16:         equals new
  17:         {
  18:             ApplicationId = d.ApplicationId,
  19:             NextPerformerId = d.PrincipalId
  20:         }
  21:         into g
  22:         from o in g.Where(t => t.StartDate <= DateTime.Today && (t.EndDate > DateTime.Today || t.EndDate == null) && !t.Expired).DefaultIfEmpty()
  23:         where o.TrusteeId == nextPerformerId || st.NextPerformerId == nextPerformerId
  24:         select st;
  25: //其他省略
  1. 这里面首先要说的第一点是DataLoadOptions可以和表达式操作配合使用而没有任何问题, 很明显如果全部使用表达式整个语句将更加庞大和复杂, DataLoadOptions适用于有明确外键关联的表连接.
  2. 多字段关联, 常规的join…on…equals语句只是适用于单一字段的关联, 如果是多个字段的关联, 则应该使用匿名类的做法, 如上所示, 并且它们的字段名以及类型必须要完全一致, 常犯的错误就是Nullable类型和非Nullable类型的关联(如int?和int), 这也是要注意的地方
  3. Left Join. 在Linq to SQL当中做Left Join第一要素就是要调用DefaultIfEmpty(), 但关键的地方在于Where查询, 很多时候你需要的Where过滤条件在关联表那端, 也就是说你是要关联一个带过滤条件的表, 而不是关联后再过滤! 这个时候需要使用into关键字生成新的范围变量, 然后对其进行过滤, 而且DefaultIfEmpty必须要在Where执行之后再调用, 可以通过SQL Profiler观察它们生成的SQL语句的不同.

C# in Depth-Part3 读书笔记3

C# in Depth No Comments »

简化的初始化

面向对象的编程语言通常都拥有流线型的对象创建过程, 毕竟, 在你准备开始使用一个对象时, 不管是通过代码的直接调用还是工厂方法或者其他的方式你都必须要先创建它. 在C# 2中有少数新的特性让初始化过程变得简单了一点. 然而如果要做的无法通过构造器参数完成, 很不幸——你需要创建对象, 然后手工初始化设置各个属性值.

当你想一次初始化一序列对象的时候这可能会令人有点厌烦, 例如在一个数组或者集合中——没有一个”单个表达式”的做法可以用来初始化一个对象, 你必须被迫使用局部变量来做临时的处理, 或者创建一个帮助方法并基于参数来执行适当的初始化.

C# 3的到来提供了多个解决方法.

定义我们的案例类型

在本节中我们将要使用的表达式称为对象初始化器. 这仅仅是用于指定当一个对象被创建后应该执行的初始化过程. 你可以设置属性, 或者属性的属性, 也可以添加元素到那些可以通过属性访问到的集合. 为了演示, 我们将再次使用Person类. 一开始, 这里依然有我们之前使用过的name和age字段, 其通过可写的属性暴露出来. 我们将会同时提供无参构造函数以及只接受name作为参数的另一构造函数. 我们还增加了一个friends的集合以及类型为Location的home属性, 这两个都是只读的, 但依然可以通过处理返回的对象来更新. 另外, Location类提供了Country和Town属性用于表示Person的家庭地址. 完整的代码如下:

   1: public class Person
   2: {
   3:     public int Age { get; set; }
   4:     public string Name { get; set; }
   5:     List<Person> friends = new List<Person>();
   6:     public List<Person> Friends { get { return friends; } }
   7:     Location home = new Location();
   8:     public Location Home { get { return home; } }
   9:     public Person() { }
  10:     public Person(string name)
  11:     {
  12:         Name = name;
  13:     }
  14: }
  15:  
  16: public class Location
  17: {
  18:     public string Country { get; set; }
  19:     public string Town { get; set; }
  20: }

上面的代码是相当直观的, 但其没有什么价值. 当Person被创建的时候, Friends和Home都是以一种”空”的方式被创建的, 而不是null. 在以后这是相当重要的——不过先在我们只要先留意表示Person的name和age的属性值.

设置简单属性

现在我们已经拥有了Person类型, 我们希望使用C# 3的一些新的特性来创建实例. 首先我们将先关注Name和Age属性, 然后再考虑其他的元素.

实际上, 对象初始化器并没有限制你必须使用属性. 所有的语法糖同样适用于字段, 只不过多数的时间你应该使用属性. 在一个封装良好的系统中, 你不应该可以直接访问字段, 除非创建类型实例并使用类型内自己的代码.

假设我们创建一个Person实例叫做Tom, 4岁. 对于C# 3, 这里有两种方式可以完成这个工作:

   1: Person tom1 = new Person();
   2: tom1.Name = "Tom";
   3: tom1.Age = 4;
   4: Person tom2 = new Person("Tom");
   5: tom2.Age = 4;

第一个版本使用无参构造函数然后给两个属性赋值. 第二个版本使用构造函数的一个重载来初始化Name, 直接给Age赋值. 这两种做法在C# 3中都是可行, 然而, C# 3还提供了另外的一个选择:

   1: Person tom3 = new Person() { Name="Tom", Age=4 };
   2: Person tom4 = new Person { Name="Tom", Age=4 };
   3: Person tom5 = new Person("Tom") { Age = 4 };

在每行中的大括号之后都是对象初始化器, 再次申明, 这仅仅是编译器一个花招. 通常, tom3和tom4的IL代码是一致的, 而且实际上它们与tom1产生的IL也是大致一样的. 同样, 可以预见到, tom5的IL代码与tom2也是基本一致的. 注意, tom4我们省略了构造器后面的括号, 你可以使用这种针对无参扩展函数的快捷方式, 其会在编译后的代码当中被调用.

在构造器被调用之后, 相应的属性将会按照对象初始化器的顺序被显式赋值, 而且你只能针对这些属性赋值一次——例如你不能对Name属性赋值两次(实际上, 你可以这么做, 调用构造器并且使用一个name参数, 然后再对Name属性赋值. 这样做没有什么意义, 但是编译器并不阻止你这样做.) 那些被作为值赋给属性的表达式可以是任何本身不是Assignment的表达式——你可以调用方法, 创建新的对象(可能使用对象初始化器), 几乎可以是任何的表达式. 你可以能奇怪这到底有多少用处——我们已经省掉了1,2 行代码, 但这并不是一个足够好的理由而让语言本身变得更复杂, 不是吗? 这里有一个很微妙的观点, 尽管我们刚刚用一行代码创建了一个对象——我们使用一个表达式来创建它.这里的不同之处是非常重要的, 假设你想使用一个预定义的数据创建一个Person类型的数组, 即使不使用隐式数组, 其代码也是相当简洁并具有很好的可读性:

   1: Person[] family = new Person[]
   2: {
   3:     new Person { Name="Holly", Age=31 },
   4:     new Person { Name="Jon", Age=31 },
   5:     new Person { Name="Tom", Age=4 },
   6:     new Person { Name="William", Age=1 },
   7:     new Person { Name="Robin", Age=1 }
   8: };

在C# 1 和2 中, 在一个像上述这样简单的例子中, 我们也可以编写一个带有name和age参数的构造函数, 然后使用它来初始化数组. 然而, 正确的构造函数并不总数存在, 如果一个构造函数带有多个参数, 通常从它所在的位置并不能明确每一个参数表示什么意思, 如果一个构造函数带有5,6个参数, 通常可能要更多的依赖于智能提示, 在这类的例子中, 使用属性赋值可以带来更佳的可读性.

这种形势的对象构造器可能会是你最常使用的, 然而, 还存在另外两种其他形式——一个是子属性的赋值, 另外一个是加入到集合.

对嵌入对象设置属性值

目前为止我们可以发现对Name和Age属性赋值是非常简单的, 但我们不能使用相同的方法对Home进行赋值——因为它是只读的. 不过, 我们可以首先读取Home属性, 然后再其结果值上面设置属性值. 为了更清楚说明问题, 我们首先看一下在C# 1当中的代码:

   1: Person tom = new Person("Tom");
   2: tom.Age = 4;
   3: tom.Home.Country = "UK";
   4: tom.Home.Town = "Reading";

当我们构建home location的时候, 每一个声明语句都通过get取得一个Location实例, 然后在其上设置相关的属性值. 这里没有任何新鲜的东西, 但是让我们慢下来仔细看一下, 这是值得的, 否则, 我们很容易就会错过幕后所发生的一切.

C# 3中允许我们在一行代码当中完成同样的事情, 如下所示:

   1: Person tom = new Person("Tom")
   2: {
   3: Age = 4,
   4: Home = { Country="UK", Town="Reading" }
   5: };

编译后这两个代码片段是完全一致的. 编译器完全可以分辨到Home等号后面跟着的是另外一个对象初始化器, 并且会正确的设置相应的属性值到此嵌入对象上.

这里在初始化Home部分缺少的new关键字是非常重要的. 如果你想知道哪里编译器将会创建一个新的对象, 哪里编译器又将会是针对已有对象赋值, 只需要观察一下初始化器部分new是否出现. 每次我们创建一个新的对象, new关键字总是会在某一个地方出现的.

我们已经处理了Home属性——但是Tom的friends呢? 有几个属性我们可以直接在List<Person>设置, 但是没有一个可以用于将entries加入到这个列表当中. 现在是时候来了解一下下一个新的特性——集合初始化器.

集合初始化器

使用一些初始化值来创建一个集合是一个非常平常的任务. 在C# 3之前, 唯一能够带来一点帮助的语言特性是数组的创建过程, 而且其在很多情况下是比较笨拙的. C# 3拥有集合初始化器, 这允许你使用与数组初始化器相同的语法, 然而适用于任意的集合并且更加的灵活.

假设我们想构建一系列的包含一些名字的字符串列表, 在C# 2当中我们可以使用如下的做法:

   1: List<string> names = new List<string>();
   2: names.Add("Holly");
   3: names.Add("Jon");
   4: names.Add("Tom");
   5: names.Add("Robin");
   6: names.Add("William");

而在C# 3中, 要达到同样的目的只需要更少的代码:

   1: var names = new List<string>
   2: {
   3: "Holly", "Jon", "Tom",
   4: "Robin", "William"
   5: };

除了减少了几行代码之外, 集合初始化器主要带来了两个好处:

  1. 创建和初始化都在同一个表达式中
  2. 减少了代码中的混乱

当你想使用一个集合作为方法的参数或者作为另外一个大的集合的元素的时候, 第一点将变得更加重要. 尽管这相对来说发生的几率较小——不过对我来说才第二点才是真正杀手级的特性. 如果你观察一下代码的右边, 你可以找到你所需要的信息, 而且每一个部分都只编写了一次. 变量只使用了一次, 类型只是使用了一次, 么一个元素也只出现了一次. 所有这一些都极其简单, 而且比C# 2更加清晰. 另外集合初始化器不仅仅被限制在List部分, 你可以将其用于任何实现了IEnumerable, 并且有适当的公共Add方法以便应用于初始化器当中的每一个元素. 你也可以使用包含多个参数的Add方法, 做法是将其对应值包含在一对大括号中. 例如, 我们想要创建一个映射到name和age的Dictionary, 可以使用下面的代码:

   1: Dictionary<string,int> nameAgeMap = new Dictionary<string,int>
   2: {
   3:     {"Holly", 31},
   4:     {"Jon", 31},
   5:     {"Tom", 4}
   6: };

在这个例子中, Add(string, int)方法将会被调用3次, 如果有不同的Add重载存在, 不同元素的初始化器会调用对应的重载. 如果没有找到合适的重载, 编译将会失败. 这里有两个比较有趣的设计决定:

  1. 类型必须实现IEnumerable, 但编译器从未使用过它
  2. Add方法是通过名字被发现的——没有任何的接口要求

要求类型必须实现IEnumerable是一个合理的要求 以便确认该类型是某种集合类型, 而使用任何公共的Add方法(而不是要求一个精确的签名)则允许简单初始化器的使用(例如前面的例子). 非公开的重载, 包括那些显式的接口实现, 都没有被使用. 这和对象初始化器是有点不同的, 其内部属性也是可见的.(在同一个Assembly中).

在早期的指导说明书(specification)中要求必须实现ICollection<T>, 并实现单一参数的Add方法(因为接口的限制), 而不是多个重载. 这看起来更加纯粹, 但实现IEnumerable的类型远比实现ICollection的类型要多得多——而且使用单一参数的Add方法也是不方便的. 例如, 在我们上面的例子中, 我们将不得不显式为每个元素的初始化器创建一个KeyValuePair<string,int>的实例. 牺牲一点纯正的学院血统使得语言本身在真实世界中更加有用了.

目前为止我们看到的集合初始化器都是以独立的方式来创建整个集合. 实际上他们也可以和对象初始化器捆绑使用来填充内嵌的集合, 如下所示:

   1: Person tom = new Person
   2: {
   3:     Name = "Tom",
   4:     Age = 4,
   5:     Home = { Town="Reading", Country="UK" },
   6:     Friends =
   7:         {
   8:             new Person { Name = "Phoebe" },
   9:             new Person("Abi"),
  10:             new Person { Name = "Ethan", Age = 4 },
  11:             new Person("Ben")
  12:             {
  13:                 Age = 4,
  14:                 Home = { Town = "Purley", Country="UK" }
  15:             }
  16:         }
  17: };

以上的例子演示了我们提到的关于对象初始化器和集合初始化器的所有特性. 这其中比较有趣的部分是集合初始化器部分, 其本身内部又使用了对象初始化器. 这里我们并不没有像我们创建独立的集合那样, 我们没有创建一个新的集合, 而是将元素加入到一个已有的集合中.我们还可以观察得更远一些, 指定Friends的Friends, 等等. 使用这种语法不能做的事你不能指定Ben是Tom的friend——因为当你不能访问一个正在初始化的对象. 这在一些情况下会有些尴尬, 但多数时候不会成为一个问题. 对于集合初始化器当中的每一个元素, 集合的getter操作将会被调用, 然后调用适当的Add方法, 在元素被加入之前该集合不会被清除. 例如, 在使用这个集合初始化器之前已经有些元素被加入, 那么之后那些额外的(不在集合中)的元素才会被加入到集合中去. 待续!

MS SQL SERVER 2008 正式发布了!

SQL SERVER No Comments »

不是我不明白, 这世界变化快, SQL SERVER 2005还没得急用熟, 2008版本又来了. 当前发布的包括Developer / Enterprise / Standard / Web / Workgroup 五个版本, 大小都为3100M左右, MSDN订阅帐号已可下载. 这也意味着ADO.NET Entity Framework也已经准备好了, VS 2008 SP1可能会很快发布. 现在, 当你选择微软的数据访问解决方案的时候, 如果数据库是SQL SERVER, 你将会有至少三种选择, 原生的ADO.NET / Linq to SQL / Entity Framework, 选择多了是好事, 但要了解的东西也更多了, 这样才能根据项目的具体情况来选择最合适的技术.

升级到WordPress 2.6!

Non-Tech No Comments »

WP的升级还真是快, 2.5版本才用了没有多久又出了新的2.6版本了, 耐不住后台当中一直提示升级的诱惑, 虽然没有多少PHP的经验也未曾升级过Wordpress, 不过还是凭借着文档一步一步走过来了, 偷偷说一声我没有备份 -_!! 还好一切都还算顺利, 虽然在wordpress的development blog上面写了一大堆的升级的东东并且推荐大家升级, 不过那些似乎我都没有怎么用过, 反正升了也就升了, 至少不会再看到那讨厌的升级提示了:)

LINQ to SQL: 更新的烦恼

LINQ No Comments »

只要你使用过LTS当中的Attach, 你肯定不会对下面的这个错误信息陌生:

System.NotSupportedException: 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.

尤其是当你的Entity包含有其他Association的时候, 估计你一定Google了N次了吧。老实说直到现在我也没有一个很好的解决办法, 只不过我还是想说说关于这个问题我曾经试过的解决办法.

早在2月份的时候我开始在项目中使用LINQ to SQL, 使用的做法是Request-scoped DataContext, 这种架构的好处是每个DataContext实例是based on每一个Request的, 在这个Request之内任何的CRUD操作都不会成为问题, 因为它们所使用的DataContext是同一个, 这就类似于很多例子中2层结构的做法, 打开一个DataContext然后你可以干任何事情. 这里面不会再有N层架构中的那种完全问题, 通常返回数据中间层使用的是一个DataContext, 而发回的数据到中间层使用的是另外一个DataContext. 一切顺风顺水, 然后最大的问题是LoadOptions无法被正确使用, 一旦你设置了DataLoadOptions实例到DataContext的LoadOptions属性上, 在整个生命周期内, 它都不再被允许修改. 而这是很难保证的, 谁都无法保证一个Request不会同时请求两个商业方法并且他们要加载不同的LoadOptions, 一旦这种情况出现, 异常将会抛出, 请求失败. 我花了不少时间查找了很多的blog, 也留言跟这个解决方案的作者提出过这个问题, 然后一直过了很久都没有一个好的解决办法. 尽管有人使用Refelection去掉了LoadOptions只能赋值一次的限制. (不知道为什么该post现在无法访问) 即便如此, 依然无法完全解决问题, Steve Sanderson也有一篇Post讨论过在这种解决方案, 最后也是不了了之, 之后他的post全部都转到MVC方面了,  因此也只好放弃这种架构了.

在看过MSDN LINQ forum以及其他一些相关post之后, 发现似乎还得走Attach这条路才是”正道”. 如果对于小型的对象, 如果你能够接受在Update之前先读取一次数据库或者更新前的对象值, 那么问题不会太大, 你只需要应用Attach(original, true)的重载基本上就可以完成大部分的工作了, 剩下的事情就是使用一点技巧. 在一个典型的ASP.NET的更新场景中, 我们很可能只会更新Entity的某几个属性, 这些更新值会来自于页面上的某些控件的值, 很多例子都直接写在商业逻辑方法中. 例如:

   1: using(DataContext dc = new DataContext(connectionString))
   2: {
   3:     dc.Customer.Attach(originalCust);
   4:     originalCust.FirstName = "xxx";
   5:     originalCust.LastName = "xxx";
   6:     dc.SubmitChanges();
   7: }

然而, 这里的问题是很可能更新的值不仅仅是这些, 这导致这个商业方法不够稳定, 而且不够透明, 因为调用者不知道要提供哪些参数. 在看过Dropthings的源代码之后我发现了一种更好的办法, 如下:

   1: UpdateGroup(Group originalGroup, Action<Group> updateDelegate)
   2: {
   3:     using(DataContext dc = ...)
   4:     {
   5:         dc.Groups.Attach(originalGroup);
   6:         updateDelegate(originalGroup);
   7:         dc.SubmitChanges();
   8:     }
   9: }

利用委托回调来让客户端调用者决定到底哪些属性要被更新, 很好地解决了上面提到的问题. 然而, 当Entity包含其他子对象的时候情况将会越变越复杂, 也就说当有了Association之后, 一不小心异常就会抛出, 最大的问题都是在于Entity和DataContext结合太过于紧密, 更确切地说是ObjectTrackingEnabled的问题. 为了能将对象彻底从原先的DataContext当中detach出来, 要么关闭ObjectTrackingEnabled, 要么通过序列化Clone出一个新的对象, 查看这个Post, 需要注意的是Linq to SQL的Entity目前只支持DataContractSerializer, 而不支持XmlSerialization和BinaryFormatter.

另外一点就是如果你不愿意使用乐观并发模型, 则必须保证对应要更新的entity包含一个timestamp / rowversion字段或者是没有任何应用UpdateCheck策略的成员. 这里的问题是你是否愿意 / 或者说可以为每一个Table增加一个timestamp字段? 如果不行, 那么只能手工设置每个成员的UpdateCheck策略为Never, 显然这是非常麻烦的事情. 很郁闷的是SqlMetal命令行工具也不提供这种功能, 只能手工更改. 幸运的是还有一种做法可以欺骗一下Linq to SQL, 那就是直接将Primary key的Timestamp属性设为true, 尽管这是一很不正规的做法, 但是从生成的SQL语句来看似乎也是可行的.

总结目前来看要应对复杂的对象模型, 序列化似乎还是对于Disconnected场景比较好的更新设计选择.

X64: 不能正确加载一个应用程序所依赖的Assembly?

.NET No Comments »

如果你正在使用X64位的操作系统, 而且你的应用程序在别人的机器上跑完全没有问题, 那么你就应该要注意了, 很可能就是你是64机导致的问题. 看看我碰到的一个很典型的情况:

从VSS Server上面签下源代码, 然后按照installation guide配置好所有的设置, run, 结果出错, 显示无法正确加载应用程序所依赖的一个特定的Assembly, 但同样的源代码在其他机器上是没有问题的, 这个特定的Assembly明明就在bin目录下怎么就加载不到呢? 弄了半天才搞明白, 这个Commerce Server的Assembly是32位的, 是在开发机器上自动引入的, 由于其他的机器包括服务器都是32位因此一直没有问题, 而在我的机器上其所有依赖的其他Assemblies都是64位的, 最终导致引导失败. 解决办法很简单, 删除bin目录下的这个32位版本, 而统一使用GAC的版本, 问题解决. 同时这也表明将GAC当中的Assembly引入bin目录可能会引起类似这样的问题, 应该注意避免.

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS Log in