【IT168 技术文档】
简介
本教程演示如何基于NBearV3的ORM模块开发一个Web应用程序的全过程。本教程演示的实体关系包括:继承、1对1关联、1对多关联,多对多关联。同时,本教程还演示如何设计实体属性为nullable类型或符合类型。
注:所谓nullable类型主要是针对之类型而言的,.Net2.0位所有的值类型支持nullable,设为nullable的值类型,允许是null的,这样,就可以映射数据库中的null;而符合类型指这个属性是一个复合类型,但是,保存到数据库的时候,整个复合类型序列化后保存为一个数据表的字段的值。
目标
通过本教程,读者应能够掌握使用NBearV3的ORM模块进行应用程序设计的基本过程,以及开发过程中,NBearV3提供的相关工具的使用方法。
代码
本教程演示创建的所有工程和代码,包含于可以从sf.net下载的NBearV3最新源码zip包中的tutorials\ORM_Tutorial目录中。因此,在使用本教程的过程中如有任何疑问,可以直接参考这些代码。
Step 1 下载NBearV3最新版本
1.1访问http://sf.net/projects/nbear,下载NBearV3的最新版本到本地目录。
1.2 将下载的zip文件解压至C:\,您将看到,加压后的NBearV3目录中包括:dist、doc、cases、src等目录。其中,在本教程中将会使用的是dist目录,该目录下包含所有release编译版本的dll和exe。
Step 2 创建应用程序解决方案
2.1 打开VS2005开发环境,新建一个空的解决方案sln。
2.2 向sln中添加两个新建的C#类库工程,两个类库工程的名称分别为EntityDesigns和Entities,删除IDE自动创建的Class1.cs文件。
2.3 向sln中新建一个名叫website的ASP.NET Web应用程序,为website添加一个Web.config文件。
3.1 在2.2创建的EntityDesigns工程中,新建一个名为ClassDiagram.cd的类图文件。注:如果您的IDE不支持类图设计,或者您更习惯写代码,您也可以参照下面的步骤直接创建代码。
3.2为EntityDesigns工程添加到dist目录下的NBear.Common.Design.dll的引用。因为下面的每一个设计实体接口必须继承自NBear.Common.Design.Entity这个接口。在Entities工程中创建一个名为UserName的struct包含FirstName和LastName两个string类型的Field。同时在Entities工程中创建一个名为UserStatus的枚举类型,包含两个枚举选项:Availale和Deleted。注:这两个类型将用于后面的设计实体的设计。之所以这两个类型定义在Entities工程中而不是EntityDesigns工程中是,最终,所有生成的实体将放在Entities工程,且Entities工程最后对EntityDesigns工程是没有依赖关系的。
3.3 双击ClassDiagram.cd打开设计界面,您现在就可以设计实体了。注意,所有的设计实体必须是接口。为了使用3.2创建的类型,需要让Entit一Designs工程引用Entities工程。
3.4 向类图中添加一个User接口,继承自NBear.Common.Design.Entity。添加属性ID,Name,Status和Birthday。类型分别为Guid、UserName、UserStatus和DateTime?。注意,这里的Name和Status的类型为3.2创建的自定义符合类型UserName和枚举类型UserStatus。而Birthday属性的类型为一个Nullable类型的DateTime?。注意DateTime后面的问号,表示这个类型实际是一个Nullable<DateTime>,也就是说,Birthday类型如果不赋初始值的话,它的值为null。
3.5 向类图中再添加一个LocalUser接口,继承自NBear.Common.Design.Entity。添加属性LoginName和Password。类型都为string。
3.6 从工具栏添加继承线条,让LocalUser继承User。
3.7 向类图中添加一个UserProfile接口,继承自NBear.Common.Design.Entity。添加属性ID,UserID和ProfileContent。类型分别为Guid,Guid和string。注:这里的ProfileContent仅仅象征型的代表profile数据,用于演示1对1关联,实际的项目中可能会有更多属性。
3.8 从工具栏添加关联线条,让User包含一个名叫Profile的UserProfile类型的属性。这样我们就1对1关联了User和UserProfile实体。注:如果操作图形设计界面觉得麻烦,也可以切换到源代码界面,直接编码。
3.9 向类图中添加一个LocalUserPhone接口,继承自NBear.Common.Design.Entity。添加属性ID,UserID,Description,Number。类型分别为Guid,Guid,string和string。
3.10 从工具栏添加关联线条,让LocalUser包含一个名叫Phones的类型为LocalUserPhone[]的数组类型的属性。这样我们就1对多关联了LocalUser和UserPhone。
3.11 向类图添加一个Group接口,继承自NBear.Common.Design.Entity。添加属性ID,Name。类型分别为Guid和string。
3.12 从工具栏添加关联线条,让User包含一个名叫Groups的类型为Group[]的数组类型的属性。注意,这里我们要实现的是多对多关联,所以下面我们还要建一个UserGroup关联实体来连接这个多对多关系。
3.13 向类图添加一个UserGroup接口,继承自NBear.Common.Design.Entity。添加属性UserID和GroupID。类型都是Guid。
设计完的实体关系图,应该类似下面这样:

Step 4 设置设计实体元数据
4.1 切换到源代码视图。首先,我们要为除了关联实体UserGroup之外(对于关联实体,凡是标记为RelationKey的属性,会被自动认为是复合主键)的所有设计实体的主键设置PrimaryKey这个Attribute,可以为多主键实体的每个主键添加该属性。如果不正确设置主键,代码生成工具将不能正确生成数据库创建脚本。例如,对于User实体的ID属性,设置后的代码象下面这样:
[PrimaryKey] Guid ID { get; set; }
注:大家可能有疑问,为什么这里主键ID能不能是int,并且是自增长的只读属性呢?答案是完全可以的,完全可以设置某个ID属性为下面这样,无需额外设置,它将映射到一个自增长只读的int类型的数据库字段:
[PrimaryKey] int ID { get; }
4.2 在后面的步骤生成实体对应的数据库创建脚本时,对于数值类型,nullable类型和枚举类型,NBear能够自动将他们对应到数据库的对应类型,但是,对string类型,一般需要指定其映射到数据库时的具体类型和长度。当然,也可以不指定,如果不指定,则string类型默认被映射为nvarchar(127)。例如,对于UserProfile的ProfileContent属性,我们添加下面的Attribute,设置其映射到数据库的类型为ntext:
[SqlType("ntext")] string ProfileContent { get; set; }
又如,对于LocalUserPhone的Number属性,我们添加下面的Attribute,设置其映射到数据库的类型为nvarchar(20):
[SqlType("nvarchar(20)")] string Number { get; set; }
4.2 对于User的Name这个UserName类型的复合类型,我们也需要设置其SqlType,一般设为ntext,因为,默认情况下复合类型被序列化为XML,并保存于对应的数据库字段。另外,复合类型还必须使用CompoundUnit这个Attribute标记,所以User的Name属性需要被设置成下面这样:
[CompoundUnit] [SqlType("ntext")] UserName Name { get; set; }
注:继承关系不需要特别设置,NBear可以识别接口的自然继承关系,但是,注意,不要让一个设计实体接口继承超过一个基类接口,否则,NBear将不能识别这种继承关系。换句话说,NBear不支持多根继承。之所以有这个限制是因为,后面,所有这些设计实体接口会被自动生成为class形式的实体代码,而class是不支持多根继承的。
4.3 对于User和UserProfile的1对1关联,我们需要为User接口的Profile属性设置下面的Attributes(这些Attribues都包含于NBear.Common.Design中,因此,需要注意在代码中using NBear.Common.Design):
[FkQuery("UserID", Contained = true, LazyLoad = false)] UserProfile Profile { get; set; }
其中,FkQuery代表这个属性是一个1对1外键关联,参数UserID表示,在UserProfile实体中,UserID为对应的外键。LazyLoad=false容易理解,表示这个属性不是知道访问才载入数据的,而是,在实例化User对象的时候,就自动载入Profile属性的数据。当然,如果需要,也可以将LazyLoad设为true。另外,可以像下面这样设置UserProfile的UserID属性为外键,则生成的数据库脚本将包含外键引用完整性检测:
[FriendKey(typeof(User))] Guid UserID { get; set; }
4.4 对于LocalUser和LocalUserPhone的1对多关联,我们需要为LocalUser接口的Phones属性设置下面对的Attributes:
[FkQuery("UserID", Contained=true, LazyLoad=true)] LocalUserPhone[] Phones { get; set; }
这里LocalUser和LocalUserPhones是1对多外键关联。Contained=true表示Phones跟随LocalUser级联更新。
4.5 对于User和Group的多对多关联,我们为User.Groups属性设置下面的Attributes:
[ManyToManyQuery(typeof(UserGroup), OrderBy="{Name} DESC", LazyLoad=true)] Group[] Groups { get; set; }
我们可以看到,和前面的1对1和1对多关联相比,多对多关联的主要区别是必须设置ManyToManyQuery的构造函数参数,指定关联实体为UserGroup。这里的OrderBy并不是必须的,如果不指定,则载入的Group按默认规则排序。
4.6 另外,还需要设置UserGroup这个关联实体的属性如何与User和Group的属性进行关联。我们需要对UserGroup这个实体关联接口及它的属性设定下面的Attributes:
[Relation] public interface UserGroup : NBear.Common.Design.Entity { [RelationKey(typeof(User))] Guid UserID { get; set; } [RelationKey(typeof(Group))] Guid GroupID { get; set; } }
注意,首先,关联实体必须使用Relation这个Attribute修饰。其次,每一个关联属性的用于关联的属性,必须使用RelationKey这个Attribute修饰。RelationKey的唯一参数指定这个属性关联到哪一个实体。例如,这里,UserGroup的UserID属性关联到User实体;而GroupID属性则关联到Group实体。
4.7 对于LocalUser的Password,我们可以添加NotNull和SerializationIgnore这两个Attribute,显式地设置其对应字段为非空,并且,保证其不会被包含在默认的XML序列化中。设置到设计实体的SerializationIgnore,会在最终生成的实体中用XmlIngore标识。
[SqlType("nvarchar(50)")] [NotNull] [SerializationIgnore] string Password { get; set; }
Step 5 从实体设计代码生成实体代码、实体配置文件和数据库生成脚本
5.1 至此,所有的实体的设计就完毕了。编译EntityDesigns工程。
5.2 运行dist目录中的NBear.Tools.EntityDesignToEntity.exe工具,载入EntityDesigns工程编译生成的EntityDesigns.dll。
5.3 点击Generate Entities按钮,将生成的代码保存到Entities工程中的一个名叫Entities.cs的新代码文件。并为Entities工程添加到dist\NBear.Common.dll的引用。
5.4 点击Generate Configuration按钮,将生成的代码保存到website工程下的名为EntityConfig.xml的新文件中。
5.5 点击Generate DB Script按钮,将生成的代码保存到website工程下的名为db.sql的新文件,可以在某个新建的SQL Server数据库中执行这些脚本,创建对应于所有实体的数据库脚本。
Step 6 使用实体及NBear.Data.Gateway访问数据库
6.1 现在我们就可以使用前面生成的实体了。我们先要让website工程引用Entities工程,以及dist/NBear.Data.dll。
6.2 我们还需要设置website的Web.config文件,添加一个entityConfig section以包含EntityConfig.xml这个实体配置文件,并设置数据库连接字串。下面是设置完的Web.config,注意,粗体的部分都是我们添加的代码(注意,这里的connectionstring连接到SQL Server数据库的tempdb数据库,我们需要对tempdb数据库执行5.5生成的数据库创建脚本,另外也注意修改数据库登录密码。):
<?xml version="1.0"?> <configuration> <configSections> <section name="entityConfig" type="NBear.Common.EntityConfigurationSection, NBear.Common" /> </configSections> <entityConfig> <includes> <add key="Sample Entity Config" value="~/EntityConfig.xml" /> </includes> </entityConfig> <appSettings/> <connectionStrings> <add name="DbName" connectionString="Server=(local);Database=tempdb;Uid=sa;Pwd=sa" providerName="NBear.Data.SqlServer.SqlDbProvider"/> </connectionStrings> <system.web> <compilation debug="false" /> <authentication mode="Windows" / </system.web> </configuration>
6.3 好了,现在,我们就可以随心所欲的访问数据库了。将下面的代码添加至website工程的Default.aspx.cs文件(您也可以直接打开tutorials\ ORM_Tutorial\website目录下的Default.aspx.cs,从那里复制代码)。这些代码演示了一个非常典型的创建和级联更新有复杂关系的实体的过程,并伴有详细解说。关于Gateway支持的更多方法的介绍,可以参考doc目录下的SDK类库文档。
using System; using System.Data; using System.Configuration; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; using Entities; using NBear.Common; using NBear.Data; public partial class _Default : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { //init a Gateway, the param "tempdb" is the connectionstring name set in Web.config Gateway gateway = new Gateway("tempdb"); //youcan also use gateway = Gateway.Default, which maps to the last connectionstring in Web.config create & save a LocalUser#region create & save a LocalUser WriteLine("Create a new local user and set property values."); LocalUser newLocalUser = new LocalUser(); newLocalUser.ID = Guid.NewGuid(); newLocalUser.Password = "12345"; //by default, newUser.Birthday equals null, because it is DateTime? type, which means when saved in database, its value is dbnull. //newUser.Birthday = null //if you want to set a compoundunit property like User.Name, you must create the compoundunit type first, and then assign it to the property //you should not assign value directly to the compoundunit property's property like "newUser.Name.FirstName = XXX", //or, a compile-time warning will be thrown WriteLine("Create & set the user name."); UserName newUserName = new UserName(); newUserName.FirstName = "teddy"; newUserName.LastName = "ma"; newLocalUser.Name = newUserName; //must create the compoundunit type first, and then assign it to the property newLocalUser.Status = UserStatus.Available; //create and assign the 1 to 1 related Profile property UserProfile newUserProfile = new UserProfile(); newUserProfile.ID = Guid.NewGuid(); //create it first newUserProfile.UserID = newLocalUser.ID; newUserProfile.ProfileContent = "some sample content"; newLocalUser.Profile = newUserProfile; //assign it to the property //create two phones and assign the 1 to many related Phones property WriteLine("Create & set the local user phones"); LocalUserPhone[] newPhones = new LocalUserPhone[2]; newPhones[0] = new LocalUserPhone(); //create first phone newPhones[0].Description = "home"; newPhones[0].ID = Guid.NewGuid(); newPhones[0].UserID = newLocalUser.ID; newPhones[0].Number = "111"; newPhones[1] = new LocalUserPhone(); //create second phone newPhones[1].Description = "work"; newPhones[1].ID = Guid.NewGuid(); newPhones[1].UserID = newLocalUser.ID; newPhones[1].Number = "222"; LocalUserPhoneArrayList newPhoneList = new LocalUserPhoneArrayList(); newPhoneList.AddRange(newPhones); newLocalUser.Phones = newPhoneList; //assign it to the property //create a group and assign it to the Groups property WriteLine("Create & set the user groups."); Group newGroup = new Group(); newGroup.ID = Guid.NewGuid(); newGroup.Name = "new group"; GroupArrayList groupList = new GroupArrayList(); groupList.Add(newGroup); newLocalUser.Groups = groupList; //another way to add a item to array property //save newLocalUser WriteLine("Save the new local user."); gateway.Save<LocalUser>(newLocalUser); //do you know what is happening when saving the new local user? #endregion Check saving result#region Check saving result WriteLine(""); WriteLine("After we saved the local user."); //let find the saved local user by id first LocalUser theSavedLocalUser = gateway.Find<LocalUser>(newLocalUser.ID); if (theSavedLocalUser != null) WriteLine("We found the saved local user itself."); //a local user is also a user, right? then could we find the saved local user as a user? User theSavedLocalUserAsUser = gateway.Find<User>(newLocalUser.ID); if (theSavedLocalUser != null) WriteLine("We found the saved local user itself as a user."); //was the 1 to 1 related user profile saved on the new local user's saving? if (theSavedLocalUser.Profile != null && theSavedLocalUser.Profile.ID == newLocalUser.Profile.ID) WriteLine("We found the 1 to 1 related user profile of the saved local user was also saved."); //were the 1 to many related local user phones saved on the new local user's saving? if (theSavedLocalUser.Phones != null && theSavedLocalUser.Phones[0].ID == newLocalUser.Phones[0].ID && theSavedLocalUser.Phones[1].ID == newLocalUser.Phones[1].ID) WriteLine("We found the 1 to many related local user phones of the saved local user were also saved."); //were the many to many related user group and the usergroup relation entity saved on the new local user's saving? if (theSavedLocalUser.Groups != null && theSavedLocalUser.Groups.Count > 0 && theSavedLocalUser.Groups[0].ID == newLocalUser.Groups[0].ID) WriteLine("We found the many to many related local user group of the saved local user was also saved."); //is the line really executed?? it should not. else WriteLine("Oh! many to many related local user group of the saved local user was NOT saved!! Do you know why? - It is NOT because it is many to many related while profile and phones are 1 to 1 or 1 to many. It is not because you are not Teddy, either. :) It IS because in the entity design of User, the Groups property is NOT marked with the [Contained] attribute."); //save an uncontained property's value WriteLine("To save an uncontained property value, such as user's Groups, you have to manually do this."); WriteLine("Firstly, you should save the group it self."); gateway.Save<Group>(newGroup); WriteLine("Furthermore, you have to create & save a usergroup relation entity manually. Let's do it."); UserGroup newUserGroup = new UserGroup(); //create the new usergroup relation entity instance newUserGroup.UserID = theSavedLocalUser.ID; newUserGroup.GroupID = newGroup.ID; gateway.Save<UserGroup>(newUserGroup); //do the saving WriteLine("Let's find the saved local user again. Was the group saved this time?"); theSavedLocalUser = gateway.Find<LocalUser>(newLocalUser.ID); if (theSavedLocalUser.Groups != null && theSavedLocalUser.Groups.Count > 0 && theSavedLocalUser.Groups[0].ID == newLocalUser.Groups[0].ID) WriteLine("Yes, conguratulation! This time, we found the many to many related local user group of the saved local user was finally saved."); //to see the saved user name details WriteLine("Do you want to know the saved user name's details, which is a compoundunit property? Ok, show you what you want, in fact, it is serialized as xml by the NBear.Common.SerializationManager class, looks like:"); WriteLine(SerializationManager.Serialize(theSavedLocalUser.Name)); WriteLine("Ok, I heard you considering whether you can save it into some other format because you do not want it to be XML? You do have chance to control this!!"); WriteLine("What youshould do is easily register a custom serialize/deserialize delegate method pair."); SerializationManager.RegisterSerializeHandler(typeof(UserName), new SerializationManager.TypeSerializeHandler(CustomSerializeUserName), new SerializationManager.TypeDeserializeHandler(CustomDeserializeUserName)); WriteLine("Let's save the user name again."); theSavedLocalUser.Name = newUserName; gateway.Save<LocalUser>(theSavedLocalUser); WriteLine("What does the details of the user name now become? It becomes:"); WriteLine(SerializationManager.Serialize(theSavedLocalUser.Name)); WriteLine("Cool!~~ Right? But remember, in real project, you must register the custom serialize/deserialize delegate method pair at application started up. For example, in Application_Start()."); WriteLine("Thank you so much for having completed this tutorial. You can look up the appendixes, for more information about the usage of the Gateway."); WriteLine("See you later!"); WriteLine("Warm regards,"); WriteLine("Teddy " + DateTime.Now.ToShortDateString()); SerializationManager.UnregisterSerializeHandler(typeof(UserName)); #endregion } private void WriteLine(string str) { Response.Write(Server.HtmlEncode(str) + "<br /><br />"); } private string CustomSerializeUserName(object name) { UserName userName = (UserName)name; return userName.FirstName + "," + userName.LastName; } private object CustomDeserializeUserName(string data) { string[] splittedData = data.Split(','); UserName userName = new UserName(); userName.FirstName = splittedData[0]; userName.LastName = splittedData[1]; return userName; } }