AppBox升级进行时 - 拥抱Entity Framework的Code First开发模式
从Subsonic到Entity FrameworkAppBox 是基于 FineUI 的通用权限管理框架,包括用户管理、职称管理、部门管理、角色管理、角色权限管理等模块。
Subsonic最早发布于2008年,当时他的无代码生成模式吸引了很多人的眼球,ActiveRecord模式的支持也是Subsonic迅速流行的原因之一。Subsonic也曾经一度被认为是NHibernate的有力竞争对手。可惜在2009年左右Subsonic的作者Rob Conery被微软挖去做Asp.net MVC之后,Subsonic实际上已经死去,虽然后来Subsonic 3.0的CodingHorror也试图东山再起,但还是由于性能原因以及各个竞争对手的冲击而逐渐没落。
不过高手的确是高手,Rob Conery在2011年发表的一篇文章《Massive: 400 Lines of Data Access Happiness》出其不意地掀起了一阵Micro-ORM的热潮,随后出现了更多的微型ORM框架,比较著名的有PetaPoco,Dapper,ServiceStack.OrmLite,Simple.Data。我也曾经试用过ServiceStack.OrmLite,对他的易用性赞不绝口,特别是对其通过代码完全控制数据库的创建和操作的方式印象深刻,如下所示。
2. 使用NuGet安装EntityFramework
在VS的工具 -> 库程序包管理器 -> 管理解决方案的NuGet程序包,搜索Entity Framework并安装,如下图所示。
编写Code First所需的模型类(Model)
这里就以用户角色为例,首先定义角色的模型类。
public class Role{[Key]public int ID { get; set; }[Required, StringLength(50)]public string Name { get; set; }[StringLength(500)]public string Remark { get; set; }public virtual ICollection<User> Users { get; set; }}
然后是用户的模型类:
public class User{[Key]public int ID { get; set; }[Required, StringLength(50)]public string Name { get; set; }[Required, StringLength(100)]public string Email { get; set; }[Required, StringLength(50)]public string Password { get; set; }[Required]public bool Enabled { get; set; }[StringLength(10)]public string Gender { get; set; }[StringLength(100)]public string ChineseName { get; set; }[StringLength(100)]public string EnglishName { get; set; }[StringLength(200)]public string Photo { get; set; }[StringLength(50)]public string QQ { get; set; }[StringLength(100)]public string CompanyEmail { get; set; }[StringLength(50)]public string OfficePhone { get; set; }[StringLength(50)]public string OfficePhoneExt { get; set; }[StringLength(50)]public string HomePhone { get; set; }[StringLength(50)]public string CellPhone { get; set; }[StringLength(500)]public string Address { get; set; }[StringLength(500)]public string Remark { get; set; }[StringLength(50)]public string IdentityCard { get; set; }public DateTime? Birthday { get; set; }public DateTime? TakeOfficeTime { get; set; }public DateTime? LastLoginTime { get; set; }public DateTime? CreateTime { get; set; }public virtual ICollection<Role> Roles { get; set; }}
注意,我们在此定义了两个导航属性(Navigation Property),分别是 Role.Users 和 User.Roles,并且声明为 virtual ,其实这就启用了Entity Framework的延迟加载特性。在后面的代码中,你会看到我们都是使用 Include 来即时加载数据(内部SQL实现是表关联),从而避免了延迟加载造成的多次数据库连接。
在上面定义中,我们使用了一些Data Annotations来声明属性,比如Key用来跟踪每一个模型类的实例(也就是实体 - Entity,这也许就是Entity Framework名字的由来),对应到数据库表中的主键。StringLength则用来定义属性的长度,对应到数据库表中字段的长度。更多的Data Annotations请参考:http://msdn.microsoft.com/en-us/data/jj591583
使用Fluent API来配置模型类的关系
虽然使用Data Annotation也能设定模型类的关系,但是不够灵活。Entity Framework还提供了另一种方式Fluent API来设置关系,详细的介绍可以参考博客园 dudu 老大的这篇文章:http://www.cnblogs.com/dudu/archive/2011/07/11/ef_one-to-one_one-to-many_many-to-many.html
定义用户和角色之间多对多的关系:
public class AppBoxContext : DbContext{public DbSet<User> Users { get; set; }public DbSet<Role> Roles { get; set; }protected override void OnModelCreating(DbModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.Entity<Role>().HasMany(r => r.Users).WithMany(u => u.Roles).Map(x => x.ToTable("RoleUsers").MapLeftKey("RoleID").MapRightKey("UserID"));}}
用更加通俗的话来解释上面的代码:
1. 一个角色(Role)有很多(HasMany)用户(Users);
2. 每个用户(Users)又有很多(WithMany)角色(Roles);
3. 把这种多对多的关系映射到一张表(RoleUsers),外键分别是RoleID和UserID。
需要注意的是,在Entity Framework不能直接对关联表进行操作,需要通过Role或者User实体来修改添加删除关系。
编写数据库初始化代码
1. 首先在Global.asax中设置数据库初始化类:
protected void Application_Start(object sender, EventArgs e){ Database.SetInitializer(new AppBoxDatabaseInitializer());}
2. 定义数据库初始化类:
public class AppBoxDatabaseInitializer : DropCreateDatabaseIfModelChanges<AppBoxContext> // DropCreateDatabaseAlways<AppBoxContext>{protected override void Seed(AppBoxContext context){GetUsers().ForEach(u => context.Users.Add(u));GetRoles().ForEach(r => context.Roles.Add(r));}private static List<Role> GetRoles(){var roles = new List<Role>(){new Role(){Name = "系统管理员",Remark = ""},new Role(){Name = "部门管理员",Remark = ""},new Role(){Name = "项目经理",Remark = ""},new Role(){Name = "开发经理",Remark = ""},new Role(){Name = "开发人员",Remark = ""},new Role(){Name = "后勤人员",Remark = ""},new Role(){Name = "外包人员",Remark = ""}};return roles;}private static List<User> GetUsers(){string[] USER_NAMES = { "男", "童光喜", "男", "方原柏", "女", "祝春亚", "男", "涂辉", "男", "舒兆国" };string[] EMAIL_NAMES = { "qq.com", "gmail.com", "163.com", "126.com", "outlook.com", "foxmail.com" };var users = new List<User>();var rdm = new Random();for (int i = 0, count = USER_NAMES.Length; i < count; i += 2){string gender = USER_NAMES[i];string chineseName = USER_NAMES[i + 1];string userName = "user" + i.ToString();users.Add(new User{Name = userName,Gender = gender,Password = PasswordUtil.CreateDbPassword(userName),ChineseName = chineseName,Email = userName + "@" + EMAIL_NAMES[rdm.Next(0, EMAIL_NAMES.Length)],Enabled = true,CreateTime = DateTime.Now});}// 添加超级管理员users.Add(new User{Name = "admin",Gender = "男",Password = PasswordUtil.CreateDbPassword("admin"),ChineseName = "超级管理员",Email = "admin@examples.com",Enabled = true,CreateTime = DateTime.Now});return users;}}
开始查询数据库
使用如下代码查询单个用户:
using(var db = new AppBoxContext()) {int id = Convert.ToInt32(Request.QueryString["id"]);User current = db.Users.Include(u => u.Roles).Where(u => u.UserID == id).FirstOrDefault();if (current != null){labName.Text = current.Name;labRealName.Text = current.ChineseName;labGender.Text = current.Gender; // 用户所属角色labRole.Text = String.Join(",", current.Roles.Select(r => r.Name).ToArray());}}
但是每次都写using 会觉得很烦,能不能就将AppBoxContext实例存储在一个变量中呢,下面这篇文章给出了最佳实践:
http://stackoverflow.com/questions/6334592/one-dbcontext-per-request-in-asp-net-mvc-without-ioc-container
One DbContext per Request
我们的实现,在Global.asax的后台代码中:
protected void Application_BeginRequest(object sender, EventArgs e){}protected virtual void Application_EndRequest(){var context = HttpContext.Current.Items["__AppBoxContext"] as AppBoxContext;if (context != null){context.Dispose();}}
然后在PageBase基类中:
public static AppBoxContext DB{get{// http://stackoverflow.com/questions/6334592/one-dbcontext-per-request-in-asp-net-mvc-without-ioc-containerif (!HttpContext.Current.Items.Contains("__AppBoxContext")){HttpContext.Current.Items["__AppBoxContext"] = new AppBoxContext();}return HttpContext.Current.Items["__AppBoxContext"] as AppBoxContext;}}
下载或捐赠AppBox
1. AppBox v2.1 是免费软件,免费提供下载:http://fineui.com/bbs/forum.php?mod=viewthread&tid=3788
2. AppBox v3.0 是捐赠软件,你可以通过捐赠作者来获取AppBox v3.0的全部源代码(http://fineui.com/donate/)。
返回《AppBox升级进行时》目录