EF6 CodeFirst+Repository+Ninject+MVC4+EasyUI实践(四)
前言
- 这一篇,我们终于到了讲解Entity Framework CodeFirst 的时刻了,首先创建实体对象模型,然后会通过配置Fluent API的方式来对实体对象模型进行完整的数据库映射操作。
- 此篇幅中会涉及到一些Entity Frame的相关概念,会给出初步的解释。如果需要详细了解,可以查阅相关的帮助文档。
EF实体对象模型的创建
- EF的实体对象模型大都采用POCO类的方式创建。POCO的全称为Plain_Old_CLR_Object(简单传统CLR对象),是指那些没有从任何类继承,也没有实现任何接口的简单对象。EF CodeFirst可以利用POCO类创建的实体对象来对数据库进行映射,即我们可以通过编写POCO类中的属性和关联POCO类的关系的方式完成对数据库的映射操作。
- 我们知道数据库的规范通过范式来进行约束,那我们如何通过POCO类的方式来完成对数据库映射以及约束的操作呢?首先我们得把实体对象间的关系创建清楚,实体对象间的关系有三种:一对一、一对多、多对多。接下来我们通过完成示例来演示如何创建这些示例。
- 打开解决方案的Entities工程,我们把POCO类都建立在此工程下。没有关注过此系列文章的朋友可以在第二篇的末尾下载到解决方案工程文件。创建用户类S_User,与此类关联的对象有S_Role和S_Log,因为一个用户只属于某一个对象,一个用户包含多条操作日志。因此S_User的代码如下:
public class S_User { public S_User(){this.S_Logs = new List<S_Log>();} public long ID { get; set; } public long RoleID { get; set; } public string UserName { get; set; } public string UserPwd { get; set; } public string IsUse{ get; set; } public string Phone{ get; set; } public string Email{ get; set; } public string Remark { get; set; } public virtual S_Role S_Role { get; set; } public virtual ICollection<S_Log> S_Logs { get; set; } }
2. 接下来是日志类S_Log,与此关联的对象有S_User,即一条日志只属于某一个用户。因此S_Log的代码如下:
public class S_Log { public long ID { get; set; } public long UserID { get; set; } public DateTime OperationDate { get; set; } public string OperationMenu{ get; set; } public string OperationType{ get; set; } public string Detail{ get; set; } public virtual S_User S_User { get; set; } }
3. 接下来是角色类S_Role,与此关联的对象有S_User和S_Menu,即一个角色可以包含多个用户,一个角色用户可以操作多个菜单页面。
因此S_Role代码如下:
public class S_Role { public S_Role(){ this.S_Users = new List<S_User>();} public long ID { get; set; } public string RoleName { get; set; } public string Remark { get; set; } public virtual ICollection<S_User> S_Users { get; set; } public virtual ICollection<S_Menu> S_Menus { get; set; } }
4. 接下来是菜单类S_Menu,与此类关联的对象有S_Role,即一个菜单页面可以被多个角色用户操作。但是S_Menu采用的是树级结构的方式,
一个父级菜单包含多个子菜单,一个子菜单只属于某一个父级菜单。即子菜单的PID是父级菜单的ID,因此S_Menu的PID因该是S_Menu中ID
的外键,由于顶级的父级菜单是没有父级菜单的,所以我们可以设置PID为可空类型,S_Menu的代码如下:
public class S_Menu { public long ID { get; set; } public string MenuName { get; set; } public string Icon { get; set; } public string Link { get; set; } public string IsUse { get; set; } public int Level { get; set; } public int SerialNO { get; set; } public Nullable<long> PID { get; set; } public string Remark { get; set; } public virtual ICollection<S_Role> S_Roles { get; set; } public virtual S_Menu Parent { get; set; } public virtual ICollection<S_Menu> Children { get; set; } }
5. 接下来是字典类S_TypeInfo,由于没有与之关联的对象,因此,我们只需简单定义属性就可以呢。S_TypeInfo的代码如下:
public class S_TypeInfo { public long ID { get; set; } public string Type { get; set; } public string Name { get; set; } public string Value { get; set; } public string Remark { get; set; } }
6. 注意:一对多关系中,我们需要对外键的实体进行构造函数进行重载,比如角色中包含多个用户,public S_Role(){this.S_Users = new
List<S_User>();}。多对多关系就不用进行此操作呢。还有一点就是外键必须显示的设置,比如RoleID。因为后面在Fluent API映射数据时
,配置文件中需要用到。
EF实体上下文的创建
- 从Nuget上获取EntityFramework,工程选择Concrete,在“程序包管理器控制台”中输入Install-Package EntityFramework命令就可以安装了。
- 在Concrete工程中添加EFDbContext类来构建领域实体上下文模型。设置我们的数据库连接名称为EFDbContext,修改Web项目下的web.config设置连接名称为EFDbContext,配置好数据库连接字符串后,我们才可以连接数据库。在数据库映射的创建方法OnModelCreating中,我们把映射的配置文件放到Mapper工程下,把数据库的初始化操作放到Initializer工程下。EFDbContext代码如下:
public class EFDbContext : DbContext { public EFDbContext() : base("EFDbContext") { } public EFDbContext(string nameOrConnectionString) : base(nameOrConnectionString) { } public DbSet<S_Log> S_Logs { get; set; } public DbSet<S_Menu> S_Menus { get; set; } public DbSet<S_Role> S_Roles { get; set; } public DbSet<S_TypeInfo> S_TypeInfos { get; set; } public DbSet<S_User> S_Users { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new S_LogMap()); modelBuilder.Configurations.Add(new S_MenuMap()); modelBuilder.Configurations.Add(new S_RoleMap()); modelBuilder.Configurations.Add(new S_TypeInfoMap()); modelBuilder.Configurations.Add(new S_UserMap()); modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); //表中都统一设置禁用一对多级联删除 modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>(); //表中都统一设置禁用多对多级联删除 base.OnModelCreating(modelBuilder); } }
Fluent API配置数据库映射
- 其实如果不使用数据库配置方式,EntityFramework也可以将实体库映射到数据库文件中,只是可能达不到我们预期的数据库设计的目的,因为EntityFramework会采用默认的数据库映射方式来生成数据库。采用Fluent API可以对数据库进行详细的配置,主要包括:
- 主键的设置
- 属性的设置
- 表和字段的设置
- 关系的设置
- 在采用Fluent API方式配置实体时,实体都继承一个类型为EntityTypeConfiguration的泛型类,只有继承此类构建的实体才可以在数据库中映射出对应的约束条件。
- 首先我们来建立一下S_User的映射文件S_UserMap,代码如下:
public class S_UserMap : EntityTypeConfiguration<S_User> { public S_UserMap() { // Primary Key this.HasKey(t => t.ID); // Properties this.Property(t => t.UserName).IsRequired().HasMaxLength(20); this.Property(t => t.UserPwd).IsRequired().HasMaxLength(25); this.Property(t => t.IsUse).IsRequired().HasMaxLength(2); this.Property(t => t.Phone).IsOptional().HasMaxLength(11); this.Property(t => t.Email).IsOptional().HasMaxLength(25); this.Property(t => t.Remark).IsOptional().HasMaxLength(20); // Table & Column Mappings this.ToTable("S_User"); this.Property(t => t.ID).HasColumnName("ID").HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); this.Property(t => t.UserName).HasColumnName("UserName"); this.Property(t => t.UserPwd).HasColumnName("UserPwd"); this.Property(t => t.IsUse).HasColumnName("IsUse"); this.Property(t => t.Phone).HasColumnName("Phone"); this.Property(t => t.Email).HasColumnName("Email"); this.Property(t => t.Remark).HasColumnName("Remark"); this.Property(t => t.RoleID).HasColumnName("RoleID"); // Relationships this.HasRequired(t => t.S_Role).WithMany(t => t.S_Users).HasForeignKey(d => d.RoleID); } }
- S_UserMap继承了EntityTypeConfiguration<S_User>的泛型类
- HasKey用来指定那个属性为主键
- 在Properties设置中,IsRequired用来设定属性为必须字段,不可为空。HasMaxLength用来设置字段的长度。IsOptional用来设定属性为可选字段,可以为空。
- ToTable可以用来设置表的名称,HasColumnName用来设定字段的名称。如果你想数据库字段名和实体类中的属性名不一样,可以在此进行设置。HasDatabaseGeneratedOption用来表示字段列是否为自增长,本示例中,我们的主键采用的long类型的日期流水码,不需要字段编号。所以设置为none。
- 在Relationships中,由于一个用户只属于一个角色,所以RoleID就为S_User对象的外键,配置外键的方式如代码所示。
- 接下来建立S_Role的映射文件S_RoleMap,代码如下:
public class S_RoleMap : EntityTypeConfiguration<S_Role> { public S_RoleMap() { // Primary Key this.HasKey(t => t.ID); // Properties this.Property(t => t.RoleName).IsRequired().HasMaxLength(20); this.Property(t => t.Remark).IsRequired().HasMaxLength(200); // Table & Column Mappings this.ToTable("S_Role"); this.Property(t => t.ID).HasColumnName("ID").HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); this.Property(t => t.RoleName).HasColumnName("RoleName"); this.Property(t => t.Remark).HasColumnName("Remark"); // Relationships this.HasMany(t => t.S_Menus) .WithMany(t => t.S_Roles) .Map(m => { m.ToTable("S_RoleMenu"); m.MapLeftKey("RoleID"); m.MapRightKey("MenuID"); }); } }
- 其他的设置在上面有说明呢,主要是Relationships,因为这里的S_Role和S_Menu的关系为多对多的关系,所以会产生一张关系表S_RoleMenu,并且是由RoleID和MenuID联合产生的主键,并且RoleID为S_Menu对象的外键,MenuID为S_Role的外键。
- 接下来建立S_Menu的映射文件S_MenuMap,代码如下:
public class S_MenuMap : EntityTypeConfiguration<S_Menu> { public S_MenuMap() { this.HasKey(t => t.ID); // Properties this.Property(t => t.MenuName).IsRequired().HasMaxLength(20); this.Property(t => t.Icon).IsRequired().HasMaxLength(20); this.Property(t => t.Link).IsRequired().HasMaxLength(20); this.Property(t => t.IsUse).IsOptional().HasMaxLength(2); this.Property(t => t.Remark).IsOptional().HasMaxLength(200); // Table & Column Mappings this.ToTable("S_Menu"); this.Property(t => t.ID).HasColumnName("ID").HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); this.Property(t => t.MenuName).HasColumnName("MenuName"); this.Property(t => t.Icon).HasColumnName("Icon"); this.Property(t => t.Link).HasColumnName("Link"); this.Property(t => t.IsUse).HasColumnName("IsUse"); this.Property(t => t.Level).HasColumnName("Level"); this.Property(t => t.SerialNO).HasColumnName("SerialNO"); this.Property(t => t.PID).HasColumnName("PID"); this.Property(t => t.Remark).HasColumnName("Remark"); // Relationships this.HasOptional(t => t.Parent) .WithMany(t => t.Children) .HasForeignKey(d => d.PID); } }
- 在此关系中,PID为主键ID的外键,并且PID是为可空类型,因此外键的设定方式和RoleID的设定方式一样,只是把HasRequired变成了HasOptional,因为PID可以为空。
- S_LogMap和S_TypeInfoMap就按照以上的方式创建就行呢。
- 此示例中没有一对一的关系,特此我也把一对一的关系设定方式以一个示例写出来。
初始化数据库
- 因为数据库是通过映射自动形成的,所以在数据库初始化的时候,我们给以为生成的表添加一些默认数据,比如默认的角色用户admin
- 在工程Initializer下添加InitializerUserData类和DatabaseInitializer类,InitializerUserData类用来添加默认的角色用户。而DatabaseInitializer类负责对数据库的初始化,利用DbContext的Initialize来进行初始化。
- InitializerUserData的代码如下:
public class InitializerUserData : CreateDatabaseIfNotExists<EFDbContext> { protected override void Seed(EFDbContext context) { //添加默认角色 S_Role role = new S_Role(); role.ID = NewID.NewComb(); role.RoleName = "管理员"; role.Remark = "管理系统所有操作"; //添加默认用户 long RoleID = role.ID; S_User user = new S_User(); user.ID = NewID.NewComb(); user.RoleID = RoleID; user.UserName ="admin"; user.UserPwd=DESEncrypt.Encrypt("123"); user.IsUse="是"; user.Phone="12345678901"; user.Email="demo@hotmail.com"; user.Remark = "系统管理员账户"; user.S_Role = role; context.S_Roles.Add(role); context.S_Users.Add(user); context.SaveChanges(); } }
2. DatabaseInitializer的代码如下:
public static class DatabaseInitializer { public static void Initialize() { Database.SetInitializer(new InitializerUserData()); using (var db = new EFDbContext()) { db.Database.Initialize(false); } } }
3. 注意:数据的初始化有三种方式,本示例选择CreateDatabaseIfNotExists,也就是如果数据库不存在我们就进行创建,如果数据库存在,
我们就只能通过数据迁移来进行对数据库的修改操作。
4. 在web工程的asp.net mvc 项目中的Global.asax添加对Initializer工程的引用,添加对数据库初始化操作的注册。如下显示:
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); DatabaseInitializer.Initialize(); } }
- 运行WEB工程,我们就可以把实体映射生成数据库,打开数据库管理器查看如下:
备注
- 到此,我们完成了POCO类的建立,以及通过Fluent API配置数据库,设置数据库初始化的值,完成了数据库的映射操作。如果要对实体进行修改重新映射到数据库,那么就要使用数据迁移,这个我就不多说了。
- 完成的示例代码,我会放到网盘,不过目前的代码就是博文提到的,可以点击下载。如果是自己搭建的,可能会遇到EntityFramework程序集加载不正确的错误。原因是因为本地创建的MVC项目采用的是EntityFramework的5.0版本,而我们通过Nuget获取的是6.0版本,将工程集的EntityFramework5.0版本移除,重新加载6.0的就行呢。