LINQ快速开发设计最佳实践(二) 构建Model模型
一.摘要
第一篇文章我简要介绍了项目的设计框架和LINQ实现思想. 本篇文章将是最实际和具有技巧性的地方, 就是如何创建LINQ TO SQL 的模型对象.
二.前言
1.LINQ与LINQ TO SQL
姜敏同学提醒我要注意LINQ TO SQL和LINQ的不同.的确这两者就类似C#和.NET. 老赵曾写文章特别强调过两者的不同.这里再简单提一下.
LINQ是Language-Integrated Query的缩写, 翻译后是"集成语言查询", 我将LINQ看做是一种查询框架, 拥有自己的特定语法. 只要对象实现了LINQ框架所需要的接口, 就可以使用LINQ语法进行查询.比如:
var query = from tagCategory in dc.TagCategory where tagCategory.pkid == tagCategoryId select tagCategory
LINQ TO SQL 是一个LINQ Provider, 能够实现轻量级的ORM, ORM的作用是将数据库中的表和Model模型做映射, 比如Java中的Hiberate(.NET中也有NHiberate). LINQ TO SQL让我们可以使用LINQ的语言操作Model对象, 完成对数据库的CRUD操作.
为何我们可以使用LINQ语言查询一个Object? 因为有LINQ TO Object.
为何我们可以使用LINQ语言查询对象操作数据库? 因为我们有LINQ TO SQL.
2.LINQ TO SQL 的Model对象
传统的Model对象是一种信息模型, Model类一般不包含任何的方法, 只包含属性.要描述一个现实世界Student, 首先需要一个Student的Model类, 比如类名就叫做Student, 用来保存每一个Student特有的信息, 比如名字. 然后将其具有的行为在业务逻辑层描述, 比如在业务逻辑层创建一个StudentBL类, 为其添加一个Walk()方法. 现在比较流行使用业务模型设计系统, 业务模型会将信息和行为抽象成一个Student角色.但是我还很少使用.
使用LINQ TO SQL, 我们能将传统意义上的Model模型的某一个属性, 比如Student类的Name属性与数据库Student表的Name字段建立映射关系, 这就是ORM: 对象Object 关系Relational 映射Mapping 的含义. 有了ORM, 当我们修改Student的Name时,将自动修改数据库中的数据. 我们再也看不到SQL了, 系统中统统都是操作对象.
LINQ TO SQL 提供了可视化设计工具"O/R 设计器"和命令行工具SqlMetal.exe 为数据库中的表自动生成Model类代码. 但是默认生成的代码只能做做简单的Demo例子, 在实际应用中有很多问题和需要修改的地方.下面将我创建一个Model对象的的"最佳实践"拿出来和大家分享.
三. 使用工具建立Model类
在架构上我将Model类横向切割为一层, 纵向将每个业务线切割为一个项目.比如Tag系统的模型层为: Com.Elong.Model.Tag
每个项目首先都要为每个表创建Model类, 这一步我会使用O/R设计器的可视化设计.下面简单介绍一下步骤.
1.使用 O/R 设计器
在项目中添加新项是, 我们选择"LINQ TO SQL类",如下图:
我们会在项目中添加TagDataContext.dbml文件, 展开此文件的"+"会发现其包含两个子文件: TagDataContext.dbml.layout以及TagDataContext.design.cs
其中 TagDataContext.design.cs 中保存我们的DataContext类和所有表的Model类代码. TagDataContext.dbml.layout保存的是可视化设计的一些信息.
双击 TagDataContext.dbml 文件会进入可视化设计阶段. 使用O/R设计器十分简单, 在此不做介绍.想学习的可以参考MSDN或者一些前辈的系列教程,无非就是拖拖拽拽的工作.下面是一张O/R设计器截图:
提供下面几个技巧和经验:
1.如果想自动创建数据库中两个表的关系, 需要将这两个表同时拖出.
2.单击设计器空白处,查看属性面板, 有很多属性可以在这里修改:
最有用的上下文命名空间 和 实体命名空间. 上下文命名空间是DataContext类的命名空间, 我通常将其放置在DataAccess层中.实体命名空间是所有Model类所在的命名空间, 我将其放置在Model层中.
3.任何在可视化设计中的修改, 都会造成 .design.cs 文件重写, 所以现阶段不要手工对此文件做修改. 因为一旦作了修改将不能再使用O/R 设计器.
4.O/R 设计器允许我们创建一个不会重写的类来扩成自动生成的代码,创建方法是在O/R设计器的空白处点右键, 在弹出菜单中选择"查看代码",如图:
会在.dbml文件中添加一个.cs文件:
请一定要创建这个文件,因为以后我会将Model类的方法创建在这个文件中.
四.使用SqlMetal.exe工具
上一章中已经提到过SqlMetal.exe,它是一个命令行工具, 需要从Visual Studio的命令行中使用.也会生成DataContext和Model类的代码.比如:
Sqlmetal /conn:"uid=****;pwd=****;initial catalog=DataBaseName;data source=192.168.0.1;Connect Timeout=900;Connection Reset=FALSE" /code:"d:\DB.cs"
DB.CS中会有包含了是数据库所有表的DataContext以及每一张表的Model类.
一旦我们对可视化设计生成的.Design.cs文件作了修改, 比如添加了注释, 那么请不要再使用 O/R 设计器进行可视化的更改.正确的做法是手动的修改.Design.cs中的代码, 对于新加表等工作量比较大的修改, 可以先使用SqlMetal.exe生成表对应的Model类的初始代码, 将其Copy到.Design.cs中,然后再自行修改.
五.修改 TagDataContext.Design.cs 文件
在Design.cs中, 我们要做下面几项修改:
1.修改Model类名
修改Model类名称,以及Model类Table特性的值,去掉其中的"用户."前缀.添加注释.下面是一个例子.
修改前:
[Table(Name = "dbo.T_Activity")] public partial class T_Activity
修改后:
/// <summary> /// 活动表.保存各种活动信息 /// </summary> [Table(Name = "T_Activity")] public partial class Activity
2. 添加所有属性的默认值和注释.
以ActivityName属性为例.
添加默认值:
private string _ActivityName = string.Empty;
添加注释:
/// <summary> /// 活动名称 /// </summary> [Column(Storage = "_ActivityName", DbType = "NVarChar(200) NOT NULL", CanBeNull = false)] public string ActivityName
3.在主键列的Column特性上添加IsVersion属性,值为true
比如:
/// <summary> /// 自增主键 /// </summary> [Column(Storage = "_pkid", AutoSync = AutoSync.OnInsert, DbType = "Int NOT NULL IDENTITY", IsPrimaryKey = true, IsDbGenerated = true, IsVersion = true)] public int pkid
这一条相当重要.否则使用LINQ会遇到很多问题.当然也可以建立一个额外的TimeStamp列来用做IsVersion列,但是比较麻烦.
对于主键来说, AutoSync属性应该设置为 OnInsert, 实现的功能就是当我们将想要Insert一个对象时, 在插入前并没有对象的主键ID为null或默认值, 但是当调用DataContext完成Insert操作后, 此对象的主键ID属性会被自动被填充.
3.将使用数据库默认值的模型属性上,添加Column(IsDbGenerated=true)
比如:
[Column(Storage = "_CreatedTime", DbType = "DateTime NOT NULL", IsDbGenerated=true)] public System.DateTime CreatedTime {...}
CreatedTime是数据库中使用getDate()在创建时写入的,我们希望使用数据库中的值.而不是在程序中更新.注意使用LINQ更新的是对象,如果使用默认的代码并且没有添加IsDbGenerated, 则数据库中的getDate()将失效.
4.修改DataContext类
如果我们添加了新的表, 除了修改.Design.cs中的Model模型类,还需要修改同样在此文件中的DataContext类.假设我们要添加一个Activity表,则需要在DataContext类中增加下面代码:
partial void InsertActivity(Activity instance); partial void UpdateActivity(Activity instance); partial void DeleteActivity(Activity instance); public System.Data.Linq.Table<Activity> Activity { get { return this.GetTable<Activity>(); } }
这三个分布方法是LINQ提供的机制,可以用来自定义增删改操作.Activity属性则是在LINQ语句中需要使用的数据库表对象.
5.为反向引用添加 XmlIgnore 特性标签
自顶而下的方向是指从1对多中的"1"一方作为顶端, 叶子节点是元子表 或 "多对多"表.
实际代码中, 就是将所有 EntityRef<T> 的字段所对应的属性上添加[XmlIgnore()]特性, 否则提供远程服务需要序列化对象对象时, 会提示存在循环引用.
六.修改 TagDataContext.cs 文件
在上面我们通过O/R 设计器创建的.cs文件中, 为每一个类添加一个Detach方法,下面是一个标准的Detach方法:
[DataContract()] public partial class TagCategory { public void Detach() { this.PropertyChanged = null; this.PropertyChanging = null; this._TagCategoryLanguage = new EntitySet<TagCategoryLanguage>(new Action<TagCategoryLanguage>(this.attach_TagCategoryLanguage), new Action<TagCategoryLanguage>(this.detach_TagCategoryLanguage)); this._TagCategoryRefCity = new EntitySet<TagCategoryRefCity>(new Action<TagCategoryRefCity>(this.attach_TagCategoryRefCity), new Action<TagCategoryRefCity>(this.detach_TagCategoryRefCity)); this._TagItem = new EntitySet<TagItem>(new Action<TagItem>(this.attach_TagItem), new Action<TagItem>(this.detach_TagItem)); this._TagCategory1 = new EntitySet<TagCategory>(new Action<TagCategory>(this.attach_TagCategory1), new Action<TagCategory>(this.detach_TagCategory1)); this._T_TAG_TagCategory1 = default(EntityRef<TagCategory>); } }
一个partial类可以将类代码分布在多个文件中,所以我们可以在.cs文件中扩充Model类.
一个标准的Detach方法需要做如下几个事情:
1.如果有 PropertyChanged 和 PropertyChanging 成员, 则设置为null
其中只要是Model类实现了 INotifyPropertyChanging, INotifyPropertyChanged 这两个接口, 就会存在 PropertyChanged 和 PropertyChanging 这两个事件委托.默认生成的Model类实现了这两个接口.
2.将Model类所有的EntitySet和EntityRef字段设置为默认值.
这部分代码我们可以在自动生成的Model类的构造函数中找到. 需要做的就是Copy到此方法中.
在我的LINQ框架中, 每个Model类都要有Detach方法.
七.Detach的作用
Detach的主要作用是让对象脱离DataContext的跟踪, 从而实现一个方法:Update, 参数是Model对象模型.
一个对象Detach后, 我们即可调用自己的业务逻辑或数据访问层对象, 进行更新操作. 而不是每次都必须依赖DataContext.
如果依赖DataContext, 那么业务逻辑和职责就没办法拆分. 如果已经有了一个信息完整的实体对象, 在不调用Detach方法时会常常遇到无法Attach的问题. 每次更新都要先Select, 那显然是笨重的也是影响效率的.
也许至今还会有人此方法的正确性. 因为不了解LINQ底层的对象跟踪机制, 我也无法知道为何要这么做. 但是我花了好几天去寻找这个Detach方法, 因为中文没有相关资料, 而英文资料也是在试探了好几个解决方案后才在事件中验证了此方法的可行性.
有了Detach, 数据访问层的Update方法就可以这么写了:
#region ===== Update ===== /// <summary> /// 更新对象 /// </summary> /// <param name="item">TagCategory对象</param> /// <returns>变更集</returns> public ChangeSet Update(TagCategory item) { TagDataContext dc = new TagDataContext(m_ConnectionString); item.Detach(); dc.TagCategory.Attach( item, true ); ChangeSet result = dc.GetChangeSet(); dc.SubmitChanges(); return result; } /// <summary> /// 更新集合 /// </summary> /// <param name="item">TagCategory集合</param> /// <returns>变更集</returns> public ChangeSet Update(List<TagCategory> itemList) { TagDataContext dc = new TagDataContext(m_ConnectionString); foreach (TagCategory tempItem in itemList) { tempItem.Detach(); } dc.TagCategory.AttachAll(itemList, true); ChangeSet result = dc.GetChangeSet(); dc.SubmitChanges(); return result; } #endregion
八.总结
也许有人又要批判我的LINQ架构的效率问题. 首先可以肯定我的Update方法会产生正确的操作, 比如肯定可以将一个对象完整的更新到数据库中.使用LINQ TO SQL 的跟踪和更新机制, 并不能提升多少效率, 反而会经常遇到问题, 而且没有一种能够将职责切分的方案.
至少我提供了一种方案.而且我还没找到第二种LINQ的框架设计.
Model层讲完了, 还剩下业务逻辑层和数据访问层对象的讲解.剩下的工作就轻松了, 我只需要提供完整的代码,并且对一些关键点做讲解就好了.Model类的绝对是LINQ TO SQL 的关键点.自认为本篇文章比上一篇要好, 因为花了心思整理和陈述. 欢迎大家继续拍砖!
出处:http://www.cnblogs.com/zhangziqiu/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。