使用Entity Framework 4进行代码优先开发 转

NET 4随带发布了一个改进版的Entity Framework(EF)— 一个位于System.Data.Entity命名空间的数据访问函数库。

当Entity Framework在.NET 3.5 SP1里第一次发布的时候,很多程序员给我们提供了反馈,指出他们认为在第一个版本中不足的地方。SQL团队积极听取这些意见,并且在.NET 4的版本里吸取了意见。

EF4里一些重大改进包含有:

· 支持简单CLR对象(POCO):现在你不需要基类或者数据持久化属性就可以定义实体了。

· 支持延迟加载:现在你可以即时加载子对象,而不是预先加载它们。

· 支持N-层技术和自跟踪实体:用来处理在层间传输实体或者无状态Web调用的情况。

· 更好的SQL语句生成和存储过程的支持:EF 4执行的是更快的SQL,而且还包括了对更好的存储过程集成支持。

· 支持自动的单复数名词转换:EF4支持对表名自动转换单复数(例如:Categories -> Category)。

· 更佳的可测性:在EF4里,现在可以更容易地根据接口来创建模拟对象。

· 支持更多的LINQ操作符:EF4现在支持LINQ所有的操作符。

Visual Studio 2010同时也提供了更丰富的针对EF的设计器和工具。VS 2010的EF设计器同时支持“数据库优先”的开发模式—即通过已有的数据库在设计界面上创建模型层。和支持“模型优先”的开发模式—即你在设计界面上先定义好模型层,然后再根据它生成数据库的结构。

使用EF进行代码优先开发

除了支持以设计器为基础的开发流程,EF 4还支持一个以代码为中心的开发方案,我们称它为“代码优先开发”。代码优先开发开启了一个美好的开发流程。它允许你:

· 不需要打开设计器或者定义一个XML映射文件就可以开发程序。

· 只需要写一个简单的类就可以定义模型对象,而不要求有任何基类。

· 使用“惯例高于配置”的方式来实现数据库持久化,不需要显式配置任何东西。

· 也可以有选择性的复写基于惯例的持久化,并提供一个更流畅的API来完全定制持久化映射。

要使用EF的“代码优先开发”概念,当前你需要额外下载一个运行在.NET 4内置EF之上的工具。“代码优先”函数库的CTP 4版本已于本周发布,你可以从这里下载。

你可以在VS 2010的任一个.NET 4项目中使用它(包括ASP.NET Web窗体项目和ASP.NET MVC)。

手把手教程:使用代码优先的方式创建NerdDinner

去年我写了一篇ASP.NET MVC 1.0的教程,发表在博客和一本书里。这篇教程捋了一遍创建一个叫做“NerdDinner”的简单程序的过程,这个程序方便人们在线组织、主办和参加餐会。可以在这里参阅我原先的ASP.NET V1 NerdDinner教程。新书ASP.NET MVC 2高级编程也包含了更新过的教程。

这篇NerdDinner教程原先使用的是“数据库优先”的方式,也就是预先定义好数据库结构,然后使用Visual Stduio设计器创建LINQ to SQL或LINQ to Entities等模型对象来做映射。

下面我将演示如何用EF 4的“代码优先”的方式来创建NerdDinner的模型层和数据库结构,然后使用ASP.NET MVC创建一个增删改查程序。

我们将一步一步地创建这个程序,本文的最后有完整示例代码的下载链接。

第一步:新建一个空的ASP.NET MVC 2程序

我们从在Visual Studio 2010新建一个ASP.NET MVC 2项目开始,选择文件 -> 新项目并使用“ASP .NET MVC 2空 Web 应用程序”项目模板就行了。

这样就会新建一个空的ASP.NET MVC 2工程,里面没有什么控制器、模型和视图。

下一步我们将定义NerdDinner“模型”——表示程序中数据的对象,以及包含验证机制和业务规则等的逻辑。模型是基于MVC程序的“心脏”,实际上掌控了程序的行为。我们将使用EF 4新的“代码优先”方式来创建这个模型层。

第二步:创建模型

现在假设我们尚未创建数据库,从头开始创建NerdDinner程序。

我们不需要从数据库开始

当使用代码优先的开发流程时,我们不需要从先创建数据库或者定义数据库结构来开始程序开发。而可以从定义最适合我们程序的模型对象的标准.NET类开始——免除在里面混杂数据持久化逻辑的烦恼。

创建模型类型

NerdDinner是一个小程序,所需要的数据存储非常简单。我们希望能定义和存储代表人们可参加的事件——“Dinners(餐会)”。我们还希望能够定义和存储参加餐会的人员——“RSVP”,用来跟踪有兴趣参加特定餐会的人。

让我们来创建两个类(Dinner和RSVP)来表现这个概念。在我们的ASP.NET MVC项目中添加两个类——“Dinner”和“RSVP”就可以了:

上面的“Dinner”和“RSVP”模型类就是“简单CLR对象”(即POCO)。它们不需要继承什么基类或者实现什么接口,而且里面定义的属性都是标准的.NET数据类型。里面没有添加数据持久化属性和操作数据的代码。

不需要在定义模型类时将他们绑定到特定的数据库、数据库API和数据库结构的功能的确很好,很强大——而且也让我们有了更弹性的数据访问方式。这样我们就能专注于程序和业务需求上,而不用过多考虑持久化的实现方式。另外,它还为我们提供了更换数据库结构或者存储实现方式的弹性空间—不需要重写模型对象或者与它们交互的代码。

创建Context类来处理数据持久化

我们已经定义了两个POCO模型类,现在让我们创建一个类来处理在数据库获取或保存Dinner和RSVP实例的工作。

我们将这个类命名为“NerdDinners”。它继承于DbContext基类,并且定义了两个公开属性——一个用来公开Dinner对象集合,另外一个公开RSVP对象集合:

上面用到的DbContext和DbSet类是EF 4代码优先函数库自带的。你需要添加对System.Data.Entity.CTP程序集的引用,它位于目录\Program Files\Microsoft ADO.NET Entity Framework Feature CTP4\Binaries中。另外在“NerdDinners”类型定义文件中,你需要添加“using System.Data.Entity”命名空间的引用。

这就是所有我们需要编写的代码了

上面三个类就是为我们NerdDinner程序实现一个基本的模型和数据持久层所需要的所有代码了。我们不需要配置额外的数据库结构影射信息,不需要运行任何工具,不需要编写什么XML文件,不需要用什么设计器来使用我们的类在数据库中获取、更新和存储数据。

基于惯例的持久化影射

不需要写什么额外代码,不需要创建什么XML文件,不需要运行什么工具来在数据库中映射我们的模型类。你可能会问,这可能吗?

默认情况下,EF代码优先库支持一个“惯例高于配置”的方式,它使用常见的映射习惯,从而避免显式地做任何配置。如果你想使用自定义的数据库映射规则的话,可以复写这些惯例。但如果你使用惯例的话,你会发现你自己需要写的代码真的很少,在不需要添加额外的代码和配置的前提下,90%的常见情况都可以正常工作。

在上面我们的例子当中,NerdDinner Context类默认会将“Dinners”和“RSVPs”属性映射为数据库的“Dinners”和“RSVPs”数据表。“Dinners”表里每一行都被映射成“Dinner”类型的一个实例。当然啦,“RSVPs”表里每一行都被映射成“RSVP”类型的一个实例。“Dinner”和“RSVP”类型里的属性也就随之被映射成“Dinners”和“RSVPs”表里的列。

EF支持的其它的惯例包括通过常见命名模式自动识别主键和外键(例如:根据Dinner类里的ID或者DinnerID属性推断出主键)。EF还灵巧地支持在两个模型间绑定相联关系的惯例。这里有一篇EF团队的博客讲解了所支持的默认惯例的工作方式。

使用我们模型的代码示例

前面我们创建的三个类包含了所有实现NerdDinner模型和数据持久化的所有代码。现在我们来看一些代码示例,看看如何使用这些类来执行常见的数据库操作:

使用LINQ表达式执行查询

用下面的LINQ查询表达式从数据库中获取数据。下图使用了LINQ表达式来获取所有将要举行的餐会:

我们在写LINQ表达式的时候也可以利用Dinners和RSVPs存在的关系。留意下图“where”子句过滤出参加人数大于0的餐会的方式:

要注意的是上面的查询的“where”子句(即只获取至少有一人参加的餐会)是在服务器端执行的——使执行查询和加载的数据量更为高效。

获取一个实例

我们可以使用LINQ的Single()函数,加上Lamda查询来获取Dinner的一个实例,如下面的代码所示:

或者,我们也可以利用EF“代码优先”函数库提供的Find()函数,根据ID来加载一个实例:

举办新餐会

下面的代码演示了如何创建一个新的Dinner实例并把它添加到数据库中。所有我们需要做的工作就是“new”一个Dinner对象,设置它的属性,最后把它加到NerdDinner Context对象的Dinners属性中。NerdDinner Context类支持 “工作单元”的模式,即你可以在Context中添加多个模型,然后调用“SaveChanges()”以事务的方式保存所有的数据库变动。

更新餐会信息

下面的代码演示了获取一个Dinner实例,更新其中一个属性,然后将改动保存到数据库的过程:

第三步:创建使用我们的模型的ASP.NET MVC控制器

现在来看看使用模型的更完整的场景,使用一个控制器类型来公布即将到来的餐会列表,并允许用户添加新的餐会:

右键单击“Controllers”文件夹,然后选择“添加->控制器”菜单项,我们把新的控制器命名为“HomeController”。

接着添加三个“动作函数”用来处理前面使用EF“代码优先”函数库创建的NerdDinners模型:

.

“Index”动作函数加载并显示即将到来的餐会列表。

而“Create”动作函数允许用户发布新的餐会。第一个“Create”函数用来处理用户访问/HOME/Create页面时,处理“HTTP GET” 的场景,以及发一个“New Dinner”表单让用户填写。而第二个“Create”函数用来处理跟那个表单关联起来的“HTTP POST”场景—将新餐会信息保存到数据库中。如果有任何验证错误,它会重新显示表单并加上相应的错误消息。

给我们的控制器加上视图

下一步将是给项目里加两个“视图模板”——一个给“Index”,另外一个给“Create”。

把鼠标放在控制器里的Index函数里,就可以在项目中为它添加一个视图了,然后单击右键在菜单中选择“添加视图”,弹出“添加视图”对话框。我们将创建一个强类型视图,然后给它传入一个包含“Dinner”模型对象的IEnumerable列表:

单击“添加”后,Visual Studio会创建一个/Views/Home/Index.aspx文件。再添加以下这些代码——为餐会信息生成一个<ul>列表,然后显示创建餐会的链接:

把光标放在控制器的Create函数来在项目中添加“Create”视图,单击右键菜单里的“添加视图”命令。在“添加视图”对话框中,我们还是指定创建一个强类型视图,然后传给它一个Dinner对象。另外还说明了我们希望使用“Create”模板搭个框架:

单击“添加”后,Visual Studio会创建一个/Views/Home/Create.aspx文件,里面有一个包含了为“Dinner”对象生成的HTML <form>表单的草案。我们做一点点的调整,并删掉了为DinnerID属性生成的input元素。最终的视图模板如下所示:

迄今为止,我们已经写好了网站里实现列出餐会清单和创建餐会信息的控制器和视图的所有代码。

第四步: 数据库

代码已经写好了。现在让我们来运行程序。

但数据库呢?

我们现在还没有数据库呢——其实到目前为止也没有必要,因为我们“代码优先”的开发流程并不要求数据库来定义和使用我们的模型类。

但当我们实际运行我们的应用程序时,我们要存储Dinner和 RSVP对象的。我们可以用下面的两种方式之一去创建数据库:

1. 使用数据库工具(如SQL Management Studio或 Visual Studio)去手动创建和定义数据库结构;

2. 使用EF代码优先类库根据我们定义好的模型自动创建和生成数据库结构。

第二种方式非常酷,而且我们要把它用在我们的NerdDinner程序中。

配置数据库连接字符串

在开始前,我们要设置好指向将要创建的数据库的连接字符串。我们把数据库连接字符串“NerdDinners”添加到应用程序的web.config文件中,如下所示:

在默认情况下,当你用EF的代码优先类库创建一个DbContext类的时候,它会自动查找和Context类名匹配的连接字符串。因为我们将Context类命名为“NerdDinners”,所以,当ASP.NET应用程序初始化它的时候,默认会找到前面添加好的“NerdDinners”数据库连接字符串。

使用SQL CE 4

EF 代码优先类库可以支持多个不同的数据库——包括 SQL Server、 SQL Express和 MySQL。

两个星期前,我写了一篇在ASP.NET中使用嵌入式SQL CE 4 数据库引擎博客。SQL CE 4 是一个轻量级的基于文件的数据库,安装简便,且可以嵌入在ASP.NET应用程序中。它支持低成本的宿主环境,又能够轻松地迁移到SQL Server 数据库当中。

在定义(和重新定义) 你的数据模型层的前期阶段,SQL CE可以是一个很不错的选择——另外,只要你想,还可以快速地创建和重新创建数据库。在开发NerdDinner应用程序时,我们将以SQL CE 4 作为数据库。后面,我们可以选择更改连接字符串去将产品部署到SQL Express或者SQL Server上——无需修改任何一行代码。

我前面设置的数据库连接字符串,指定了NerdDinners.sdf 数据库文件,并使用SQL CE 4 数据库驱动程序。要使它正常工作,你还需要安装SQL CE 4——安装它可以通过标准的SQL CE安装程序WebMatrix(已内置SQL CE)。SQL CE 4 很小,只要几秒钟就可以安装好。

重要事项上面的连接字符串中,我们指定将NerdDinners.sdf文件创建在\DataDirectory\文件夹下——在ASP.NET应用程序中对应的就是其根目录下的\App_Data\文件夹。默认情况下,新建"空ASP.NET MVC Web应用程序"项目模板不会创建此目录。所以你需要显式在项目中创建此文件夹(右键单击该项目并选择"添加ASP.NET文件夹-> Add_Data->"菜单项)。

自动创建数据库结构(Schema)

EF 代码优先类库支持自动地从数据模型类生成数据库结构和数据库的能力——免除用户必须手动去执行这些步骤。

如果您的连接字符串指定的SQL CE 或 SQL Express 数据库文件在磁盘中不存在默认就会发生。你根本不需要执行任何手动步骤。

要看到效果,我们可以按 F5 键运行我们的NerdDinner应用程序。它会启动浏览器,并访问我们应用程序的根目录"/"。你应该可以看到下面所示的画面:

访问应用程序的根目录会调用到HomeController.Index()这个函数——它会实例化并查询我们的NerdDinners context类,来从数据库获取即将举办的餐会列表。因为我们在连接字符串中指定的NerdDinners.sdf数据库文件并不存在,所以EF 代码优先类库将自动地为我们创建该数据库。它会根据NerdDinners context类,推断出数据库结构,最后生成数据库。

你可以通过在Visual Studio解决方案管理器上点击“显示所有文件”图标,然后点击“刷新”按钮,展开App_Data文件夹查看那个自动生成的SQL CE数据库文件。

将来我们会发布一个VS 2010的升级包,你将能在“服务器资源管理器”上打开、编辑SQL CE 4数据库(就跟你现在编辑SQL 数据库一样)。这样,你就可以很容易地看到(或许做点调整)数据库结构和数据内容了。到了那时,你也可以在WebMatrix中使用数据库工具去查看SQL CE 4数据库文件的内容。

我们还没有给NerdDinners数据类指定任何自定义的数据持久化映射规则——所以EF 代码优先类库将会根据默认的命名规范作为映射规则自动生成数据库。但是如果我们指定了任何自定义的映射规的话,EF 代码优先类库将会遵守这些规则,生成与它们匹配的数据库。

让我们回顾一下,这里是我们先前定义的两个POCO数据模型类和NerdDinners Context类:

下面是我们运行应用程序后,根据上述模型类自动创建的数据库中的两个表。

“Dinners”表的定义如下图所示。其中,列名和它的数据类型和Dinner类中定义的属性是完全对应的。DinnerID列被自动的设置成了该表的主键和唯一标识列。

“RSVPs”表的定义如下图所示。该表的列名和它的数据类型和我们在RSVP类中定义的属性也是一致的。其中,RsvpID列被设置成了该表的主键和唯一标识列。

在Dinners表和RSVPs表之间,一个一对多的主键/外键关系也同时被创建起来了。EF 代码优先类库自动建立了这种关联,因为Dinner类中有一个类型为ICollection<RSVP>的RSVPs属性,同时,RSVP类中也有一个Dinner的属性。

为数据库填充餐会信息

现在,让我们来新建一些餐会信息并将它们写入数据库。我们可以通过点击我们首页中的“Create New Dinner”链接来跳转到“新建”页面。

当我们点击“Create”按钮时,新餐会将被保存到数据库。我们可以重复数次来新建多个不同的餐会。每条我们新建的餐会都将会保存在数据库中,并会被显示在首页的“Upcoming Dinners”(即将举办的餐会)列表中。

第五步:更改数据模型

随着我们应用程序开发的推进,我们将继续改进和重构我们的模型。EF 代码优先类库包括一些很好的开发功能,这使我们在开发数据库时更容易协调某些改进。

给Dinner(餐会)模型添加一个新的属性

让我们对我们的Dinner类做一个简单的修改,更具体的说,我们将给Dinner类添加一个新的“Country”属性。

做完改动,让我们在Virtual Studio中按下F5按钮,重新编译并运行应用程序。将会看到下面的这些错误信息:

这些错误信息之所以出现,是因为我们修改了Dinner类的结构定义,我们的模型对象现在已经不再和数据库中自动创建的Dinners表一致了。

当EF自动地为你创建数据库时,默认地会增加一个“EdmMetadata”表,这个表用来记录自动创建数据库时使用的模型对象的形状。

当EF发现你修改过模型对象,并且和之前自动创建的数据库不再同步时,就会出现上面的错误消息。

重新同步数据模型类到数据库

我们有很多同步模型对像和数据库的方式:

  • 我们可以手动地更新数据库中的结构(Schema)让它们保持一致。
  • 我们也可以先删除数据库文件,然后重新运行应用程序,让EF自动重新创建数据库。
  • 我们也可以开启EF 代码优先功能,当数据模型发生任何改变时能够自动更新数据库 。

下面,让我们在NerdDinner应用程序中看看如何使用最后一种的自动更新的方式。

当模型对象发生变化时重新创建数据库(RecreateDatabaseIfModelChanges)功能

在EF 代码优先类库的CTP 4版本中包括了一个非常有用的开发时development-time)功能,它允许你在任意时刻修改数据模型类,自动重建数据库。当你开启这项功能的时候,EF能够识别用来创建数据库的类模型在何时被改动,何时可以重建你的数据库以匹配新的模型类——你不需要做任何手工操作。

这项功能在你刚开发一个应用程序时特别实用,因为它为你快速地重构你的模型代码带来了很大的自由度和灵活性——你根本不用去手动地保持数据库结构的同步。它特别适合SQL CE,因为SQL CE是一个基于文件的数据库而且可以随时在运行时删除和创建。这使得开发流程变得不可思议的流畅。

启用这项功能最简单的方法就是在Global.asax类中的Application_Start()事件处理函数中加上Database.SetInitializer()方法的调用。

这个调用告诉EF当数据模型发生任何变化时,重建NerdDinners.sdf数据库以匹配NerdDinners类。现在当我们重新运行应用程序的时候,再也不会出现提示说类模型和数据库不匹配的错误信息了。反而,EF会自动重建数据库以匹配新的数据模型类,我们的应用程序会正常运行:

为自动创建的数据库产生初始化的数据

在上面的截图上,你可能已经注意到我们之前添加的餐会数据在重建数据库之后就丢失了。这是因为根据模型变化自动重建数据库的功能并不适用于实际场合,此时你想将已经存在的数据从一个数据库移植到另外一个数据库中。相反,它是被设计用来适用于开发场合,我们需要数据库结构能快速地自动更新——而不是通过手动地或指定移植规则来实现。

注意:我们另外正在做的工作,用来提供更好的数据移植服务,支持这种适用于产品数据和数据库结构的版本管理。但我们想这和前面讲的前期开发时(development-time)功能是不同的场景。这种数据移植的功能现在还不会在这个星期的CTP上发布。

其实,EF已经为我们提供了可选的 “种子” 功能,用以在创建或者重建数据库时产生默认或测试数据。我觉得这个功能真的很实用,因为这样一来,在我重构一个数据模型后,马上就可运行应用程序去测试—不用重新手动地输入一堆的测试数据。

我们可以通过实现一个如下所示的“NerdDinnersIntializer”类,将这些“种子”数据到加入到数据库当中。我这里用它创建两个“示例餐会”并把它们加入到数据库中。

然后,我们可以在Global.asax加入Database.Initializer()方法以在启动时使用这个“NerdDinnersInitializer”类。

现在任何时候我们更新NerdDinner模型类,数据库文件都会被删除和重建以匹配我们的模型类,同时,两条默认餐会数据也会被添加到数据库中用以测试。

方便重构

上述的功能使我们非常容易地在开发时改进和重构代码——不需要用到任何工具和脚本去手动地保持数据库结构和代码的同步。

由于我们的模型类、LINQ表达式和“种子”测试数据都是强类型,所以我们也可以很快速地用Visual Studio的重构工具自动在代码文件中应用所有更改。

第六步: 加入验证规则

我们已经创建了一个简单但很好的数据服务应用程序。

但是,这里还是有一个问题,那就是我们还没有任何地方做数据验证以确保我们填入的数据库中的数据都是正确的。让我们来搞定它。

用DataAnnotations实现数据验证

在基于ASP.NET MVC的应用程序中,通常加入验证规则的地方最好是数据模型。这样,可以只在一个地方进行维护,同时也使强制任何与它交互的Controller和View的遵循它。在ASP.NET MVC中,你可以通过很多种机制去实现数据验证,它可以很灵活地支持你想要使用的任何验证机制。

ASP.NET MVC 2内置支持使用.NET类库中System.ComponentModel.DataAnnotations的验证规则库—你可以显式地在模型类中通过验证特性(Attribute)来使用验证规则。你可以在我之前发表的博客中了解这项功能的更多信息。我们将把这种方式的优势应用到我们的NerdDinner应用程序中的输入数据验证上面来。

让我们回过头,将一些数据验证的特性加入到我们之前定义好的Dinner类的属性当中去。(注意:我们同时要加入“using System.ComponentModel.DataAnnotations”命名空间。)

[Required] 验证指明这个属性的数据是必须指定的(非空的)。[StringLength] 指定了该属性允许输入的最大字符串长度。[RegularExpression] 验证指定了用以验证输入匹配的正则表达式(这里我们用以验证邮件地址)。

每一个验证特性都支持“ErrorMessage”属性,这使得我们可以指定验证失败时显示的错误消息。你既可以通过硬编码的方式(像上面一样)指定,也可以通过让它从一个资源文件里取得—可以很容易地实现本地化。

引入一些CSS和JavaScript文件

最后一步,我们将回到Create.aspx视图模板中,加入<link>引用Site.css文件到项目中,同时也加入两个<script>元素引用两个JavaScript文件。我们还在<form>元素呈现之前加入一行代码来调用Html.EnableClientValidation()方法。

这些改动将会确保那些验证错误信息以正确的样式显示出来(让它们更惹眼),而且这些数据模型的验证规则会同时应用在客户端和服务器端。

运行应用程序

让我们重新运行应用程序并尝试注册一个新的餐会。让我们先按一下还没有填写数据时的"创建"按钮。现在我们会发现模型中的验证错误消息在浏览器中已经显示出来:

由于我们在ASP.NET MVC中启用了客户端验证(即我们上面所写的代码中的一行),我们的错误消息将会实时更新:

注意上面,当我们给”Title”输入的字符串长度大于20的时候,验证错误信息改变的方式。这是因为我们为Dinner.Title设置的验证[StringLength]特性指明了最大允许的长度是20。而当我们开始在“HostedBy”文本框中输入字符串时,错误信息从“[Requred]”(要求你输入电子邮件地址)变成了“[RegularExpression]”(它告诉我们没有输入一个有效的电子邮件地址)。

这些验证规则在浏览器中(通过 JavaScript)和服务器上(即使有人试图绕过JavaScript 验证,也能保护数据的输入有效性)都有效——不用我们对Controller类作任何更改。这种将规则在我们的模型类内指定一次就可以被应用到处任何地方的能力是极其强大的——将使我们能够以一种非常清楚的方式继续改进我们的程序。

你可以通过这里了解更详细的有关这些 ASP.NET MVC 2 模型验证功能和它们的工作方式。

下载

点击这里下载和运行我们在这篇博客里实现的NerdDinnerReloaded示例。你需要安装了VS 2010 (或者免费的Visual Web Developer 2010 Express) 。

重要提示:你必须在你的机子上下载和安装SQL CE 4后才能运行上面的示例程序。你可以从这里下载EF 代码优先类库。这两个下载都不会对你的机器有太大的影响

posted @ 2012-09-15 23:26  Buu  阅读(499)  评论(0编辑  收藏  举报