版本
1.2 [2006-11-12]
简介
本教程演示如何基于NBearV3的IoC模块开发一个Web应用程序的基本过程。本教程同时演示使用NBear.Tools.DbToEntityDesign.exe工具从现有的数据库生成设计实体的过程。
注:在阅读本文之前,建议读者先阅读《NBearV3 Step by Step教程——ORM篇》以掌握NBearV3中有关ORM的基本知识。
目标
通过本教程,读者应能够掌握使用NBearV3的IoC模块的基本过程,以及使用NBear.Tools.DbToEntityDesign.exe工具,对已有数据库结构的项目,使用NBearV3的ORM组件进行数据持久化的过程。
代码
本教程演示创建的所有工程和代码,包含于可以从sf.net下载的NBearV3最新源码zip包中的tutorials\IoC_Tutorial目录中。因此,在使用本教程的过程中如有任何疑问,可以直接参考这些代码。
时间
<45分钟。
正文
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中再添加两个新建的C#类库工程,两个类库工程的名称分别为ServiceInterfaces和ServiceImpls,删除IDE自动创建的Class1.cs文件。
2.4 向sln中新建一个名叫website的ASP.NET Web应用程序,为website添加一个Web.config文件。
Step 3 设计实体、关系及元数据
3.1 运行dist\ NBear.Tools.DbToEntityDesign.exe,在Connection String文本框中输入下面的连接子串:
Server=(local);Database=Northwind;Uid=sa;Pwd=sa
我们将从SQL Server 2000自带的演示数据库Northwind,为我们需要的某些表和视图生成设计实体。
点击Connect按钮,连接上数据库后,从左边的Tables和Views列表中分别选择下面这些表或视图(因为这里主要是演示,我们只选择和Category和Product相关的几个表和视图):Categories, Products, Products by Category。点击Generate Entities Design按钮,在代码生成文本框内的任意位置右键单击鼠标,并选择Copy to ClipBoard,这将能把所有的代码复制到剪贴板。
3.2 为EntityDesigns工程添加到dist目录下的NBear.Common.Design.dll的引用,因为每一个设计实体接口必须继承自NBear.Common.Design.Entity这个接口。在EntitiyDesigns工程中新建一个代码文件EntityDesigns.cs,添加using System和using NBear.Common.Design设置namespace为EntityDesigns。并将刚才从DbToEntityDesign复制的代码粘贴至该文件中。
此时,EntityDesigns.cs文件中的内容应该象下面这样:
using NBear.Common.Design;
namespace EntityDesigns
{
public interface Categories : Entity
{
[PrimaryKey]
int CategoryID { get; }
[SqlType("nvarchar(15)")]
string CategoryName { get; set; }
[SqlType("ntext")]
string Description { get; set; }
byte[] Picture { get; set; }
}
//…
}
您会注意到,生成的代码中,数据库中的表或视图名称对应设计实体的名称,字段名称对应设计实体的属性名称,如果名称中包含空格,空格会被转换为_nbsp_。同时,是主键的标字段对应的属性,会自动包含PrimaryKey这个Attribute,而所有的文本类型的字段,则会自动包含从数据库中继承的数据类型和长度。另外,所有从视图生成的实体接口会包含ReadOnly这个Attribute,代表,该实体是一个只能用来从数据库取数据,不能用来保存数据到数据库的实体。
3.3 下面,我们可以对这些生成的代码做一下改造,让我们看着更舒服。比如,用_nbsp_代表空格多少影响视觉审美,我们可以给设计实体添加MappingName这个Attribute,来修改实体接口名称,但是,保证实体还是对应数据库中的这个表或视图。例如,对于Products_nbsp_by_nbsp_Category视图,我们可以将它修改为下面的样子:
[MappingName("Products by Category")]
public interface ProductsByCategory : Entity
{
[SqlType("nvarchar(15)")]
string CategoryName { get; }
[SqlType("nvarchar(40)")]
string ProductName { get; }
[SqlType("nvarchar(20)")]
string QuantityPerUnit { get; }
short UnitsInStock { get; }
bool Discontinued { get; }
}
我们可能同样不喜欢实体名称是复数的英文单词,所以,我们也可以象下面这样把Categories实体的名称改为Category:
public interface Category : Entity
{
[PrimaryKey]
int CategoryID { get; }
[SqlType("nvarchar(15)")]
string CategoryName { get; set; }
[SqlType("ntext")]
string Description { get; set; }
byte[] Picture { get; set; }
}
我们可以用同样的方法,修改所有的实体名称,和属性名称。属性名称同样支持MappingName这个Attribute。我们将所有的实体名称的复数改为单数,并去掉_nbsp_。现在,所有的设计实体应该象下面这个样子:
using NBear.Common.Design;
namespace EntityDesigns
{
[MappingName("Categories")]
public interface Category : Entity
{
[PrimaryKey]
int CategoryID { get; }
[SqlType("nvarchar(15)")]
string CategoryName { get; set; }
[SqlType("ntext")]
string Description { get; set; }
byte[] Picture { get; set; }
[Query(Where="{CategoryID} = @CategoryID", OrderBy="{ProductName}", LazyLoad=true)]
[Contained]
Product[] Products
{
get;
set;
}
}
[MappingName("Products")]
public interface Product : Entity
{
[PrimaryKey]
int ProductID { get; }
[SqlType("nvarchar(40)")]
string ProductName { get; set; }
int SupplierID { get; set; }
int CategoryID { get; set; }
[SqlType("nvarchar(20)")]
string QuantityPerUnit { get; set; }
decimal UnitPrice { get; set; }
short UnitsInStock { get; set; }
short UnitsOnOrder { get; set; }
short ReorderLevel { get; set; }
bool Discontinued { get; set; }
[Query(Where="{CategoryID} = @CategoryID", LazyLoad=false)]
Category Category
{
get;
set;
}
}
[ReadOnly]
[MappingName("Products by Category")]
public interface ProductsByCategory : Entity
{
[SqlType("nvarchar(15)")]
string CategoryName { get; }
[SqlType("nvarchar(40)")]
string ProductName { get; }
[SqlType("nvarchar(20)")]
string QuantityPerUnit { get; }
short UnitsInStock { get; }
bool Discontinued { get; }
}
}
3.4 实体和属性名称我们改造完了,下面还可以给设计实体添加一点关联。我们可以注意到,Category和Product是一个明显的1对多关联。因此,我们可以像下面这样,为Category实体添加一个Products属性,1对多关联到Product表。
public interface Category : Entity
{
[PrimaryKey]
int CategoryID { get; }
[SqlType("nvarchar(15)")]
string CategoryName { get; set; }
[SqlType("ntext")]
string Description { get; set; }
byte[] Picture { get; set; }
[FkQuery("Category", OrderBy="{ProductName}", Contained=true, LazyLoad=true)]
Product[] Products
{
get;
set;
}
}
如果您看过之前的ORM教程,您应该能理解加粗的代码的意思。它表示Category实体的CategoryID属性和Product实体的CategoryID关联,Products属性的Product按ProductName正序排序,同时,该属性延迟载入(即到第一次访问该属性才载入)。Contained同时代表Products属性在Category保存或删除时,会自动级联保存或删除。
3.5 我们同时也可以给Product添加到Category的引用,因为,在查看一个Product信息时,查看相关的Category是非常常见的。注意,此时我们可以删掉Product中原来的CategoryID属性,将它合并到Category属性中:
public interface Product : Entity
{
[PrimaryKey]
int ProductID { get; }
[SqlType("nvarchar(40)")]
string ProductName { get; set; }
int SupplierID { get; set; }
[SqlType("nvarchar(20)")]
string QuantityPerUnit { get; set; }
decimal UnitPrice { get; set; }
short UnitsInStock { get; set; }
short UnitsOnOrder { get; set; }
short ReorderLevel { get; set; }
bool Discontinued { get; set; }
[FkReverseQuery(LazyLoad = true)]
[MappingName("CategoryID")]
Category Category
{
get;
set;
}
}
注意,我们没有添加ContainedAttribute,因为,我们并不希望一个Category跟随他的一个Product的更新级联更新。同时,我们将Category属性设置为非延迟载入(即实例化Product的同时就载入Category属性的数据)。
这里要特别引起注意的是,当两个实体互相关联,或者,多个实体循环关联时,千万注意不要同时将互相关联的属性全都设为LazyLoad=fasle,否则将可能导致循环载入,造成程序死循环。在NBearV3的后续有版本将会引入缓存机制来解决这个问题,但是在这之前,请大家自己注意小心避免。
Step 4 从实体设计代码生成实体代码、实体配置文件
4.1 至此,实体的设计就完毕了。编译EntityDesigns工程。下面我们将从设计实体生成实际的实体代码和配置文件。注意,这里和之前的ORM教程不同的是,我们不生成数据库创建脚本,而直接使用一个已经存在的数据库Northwind。
4.2 运行dist目录中的NBear.Tools.EntityDesignToEntity.exe工具,载入EntityDesigns工程编译生成的EntityDesigns.dll。
4.3 点击Generate Entities按钮,将生成的代码保存到Entities工程中的一个名叫Entities.cs的新代码文件。并为Entities工程添加到dist\NBear.Common.Common.dll的引用。
4.4 点击Generate Configuration按钮,将生成的代码保存到website工程下的名为EntityConfig.xml的新文件中。
Step 5 使用实体及NBear.Data.Gateway访问数据库
5.1 现在我们就可以使用前面生成的实体了。我们先要让website工程引用Entities工程,以及dist/NBear.Data.dll。
5.2 我们还需要设置website的Web.config文件,添加一个entityConfig section以包含EntityConfig.xml这个实体配置文件,并设置数据库连接字串。下面是设置完的Web.config,注意,粗体的部分都是我们添加的代码(注意修改数据库登录密码。):
<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=" Northwind" connectionString="Server=(local);Database=Northwind;Uid=sa;Pwd=sa" providerName="NBear.Data.SqlServer.SqlDbProvider"/>
</connectionStrings>
<system.web>
<compilation debug="false" />
<authentication mode="Windows" />
</system.web>
</configuration>
5.3 好了,到目前为止,实体设置和配置完毕了。下面我们将开始讨论IoC模块的使用。
Step 6 定义Service接口和Service实现
6.1 下面我们开始定义一个基于NBear.IoC的Service。我们先要为ServiceInterfaces工程添加到dist\NBear.Common.dll和dist\NBear.IoC.dll的引用。一个Service由一个接口定义。我们这个Service的功能很简单,就是我们想获得一些需要的Category和Product。所以,我们还需要为ServiceInterfaces工程添加到Entities工程的引用。在ServiceInterfaces工程中定义接口ICategoryService和IProductService如下:
using NBear.IoC.Service;
using Entities;
namespace ServiceInterfaces
{
public interface ICategoryService : IService
{
Category[] GetAllCategories();
Category GetCategoryByID(int categoryID);
}
}
using NBear.IoC.Service;
using Entities;
namespace ServiceInterfaces
{
public interface IProductService
{
Product[] GetAllProducts();
Product GetProductByID(int productID);
}
}
注意,基于NBear.IoC的Service,必须从NBear.IoC.Service.IServiceInterface这个接口继承。
6.2 定义完Service接口,我们还需要实现它。在ServiceImpls工程中,添加到Entities,ServiceInterfaces和到dist\NBear.Common.dll,dist\NBear.Data.dll和dist\NBear.IoC.dll的引用,分别实现这两个接口如下:
using NBear.Common;
using NBear.Data;
using Entities;
using ServiceInterfaces;
namespace ServiceImpls
{
public class CategoryService : ICategoryService
{
ICategoryService Members
}
}
using NBear.Common;
using NBear.Data;
using Entities;
using ServiceInterfaces;
namespace ServiceImpls
{
public class ProductService : IProductService
{
IProductService Members
}
}
Step 7 配置Service,使用ServiceFactory,调用Service
7.1 编译ServiceImpls。我们就可以准备在website中使用Service了。为website添加到Entities、ServiceInterfacs、dist\NBear.Common.dll、dist\NBear.IoC.dll的引用。
注意,这里无需为website添加到ServiceImpls的引用。想想为什么?如果website依赖ServiceImpls的实现代码,那么就不叫IoC了。IoC的核心是基于容器的依赖注入。换句话说,我们只需要在配置文件中指定一个Service的接口和对应的实现类的位置,从而使得Service的调用者与Service的实现者松散耦合。但是,为了能让我们的website能根据配置文件找到Service的实现类,我们还是需要复制ServiceImpls编译输出的ServiceImpls.dll到website的Bin目录中。
7.2 接着,我们需要在Web.config中配置IoC容器。NBearV3的IoC组件使用castle作为IoC容器,因此,可以使用标准的castle配置与法进行配置。不过一般,只需要使用下面这样最简单的语法就行了:
<configuration>
<configSections>
<section name="entityConfig" type="NBear.Common.EntityConfigurationSection, NBear.Common"/>
<section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor"/>
</configSections>
<entityConfig>
<includes>
<add key="Sample Entity Config" value="~/EntityConfig.xml"/>
</includes>
</entityConfig>
<castle>
<components>
<!--You can use standard castle component decleration schema to define service interface impls here-->
<component id="category service" service="ServiceInterfaces.ICategoryService, ServiceInterfaces" type="ServiceImpls.CategoryService, ServiceImpls"/>
<component id="product service" service="ServiceInterfaces.IProductService, ServiceInterfaces" type="ServiceImpls.ProductService, ServiceImpls"/>
</components>
</castle>
<appSettings/>
<connectionStrings>
<add name="Northwind" connectionString="Server=(local);Database=Northwind;Uid=sa;Pwd=sa" providerName="NBear.Data.SqlServer.SqlDbProvider"/>
</connectionStrings>
<system.web>
<compilation debug="true">
<assemblies>
<add assembly="System.Transactions, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Runtime.Remoting, Version=2.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/></assemblies></compilation>
<authentication mode="Windows"/>
</system.web>
</configuration>
注意加粗的部分,这里我们添加了一个castle的配置块,并且配置了两个我们定义的Service和他们对应的实现。
7.3 接着,在Default.aspx.cs文件中的PageLoad中,添加下面的代码,访问Service。
注意,所有的Service只需简单地从ServiceFactory.GetService<ServiceType>()得到。然后就能使用了。
{
ServiceFactory factory = ServiceFactory.Create();
IProductService ps = factory.GetService<IProductService>();
Product[] products = ps.GetAllProducts();
WriteLine(string.Format("Got all products, {0} in total.", products.Length));
ICategoryService cs = factory.GetService<ICategoryService>();
Category[] categories = cs.GetAllCategories();
WriteLine(string.Format("Got all categories, {0} in total.", categories.Length));
WriteLine("In each category:");
foreach (Category item in categories)
{
WriteLine(string.Format("ID={0}, Name={1}, Products in category: {2}.", item.CategoryID, item.CategoryName, item.Products.Length));
}
}
private void WriteLine(string str)
{
Response.Write(Server.HtmlEncode(str) + "<br /><br />");
}
正文结束。
附录
1 关于ServiceFactory.Create()
在website中,您一定注意到,我们使用NBear.IoC.Service.ServiceFactory.Create()方法获得了ServiceFactory实例,并通过他获得Service接口的实现类实例。
在实际的项目中,您也无需在一个统一的地方定义全局的ServiceFactory实例引用,可以在每一个需要ServiceFactory实例的地方直接调用ServiceFactory.Create(),因为ServiceFactory.Create()内部实际上使用了Singleton模式,它总是返回的唯一的一个ServiceFactory实例。
同时,除了在website中通过ServiceFactory访问service之外,在某一个Service的实现代码中,也可以访问ServiceFactory.Create(),从而访问另一个同样在Web.config的castle块中配置的service。这样,当不同的Service实现程序集之间互相调用Service时,只需要互相引用Service Interfaces,Service的实现代码互相就能避免任何依赖,从而将模块间的耦合度降至最低。
//本文结束