Entity Framework 實體框架的形成之旅--Code First模式中使用 Fluent API 配置(6)


在前面的隨筆《Entity Framework 實體框架的形成之旅--Code First的框架設計(5)》里介紹了基於Code First模式的實體框架的經驗,這種方式自動處理出來的模式是通過在實體類(POCO)里面添加相應的特性說明來實現的,但是有時候我們可能需要考慮基於多種數據庫的方式,那這種方式可能就不合適。本篇主要介紹使用 Fluent API 配置實現Code First模式的實體框架構造方式。

使用實體框架 Code First 時,默認行為是使用一組 EF 中內嵌的約定將 POCO 類映射到表。但是,有時您無法或不想遵守這些約定,需要將實體映射到約定指示外的其他對象。特別是這些內嵌的約定可能和數據庫相關的,對不同的數據庫可能有不同的表示方式,或者我們可能不同數據庫的表名、字段名有所不同;還有就是我們希望盡可能保持POCO類的純潔度,不希望弄得太過烏煙瘴氣的,那么我們這時候引入Fluent API 配置就很及時和必要了。

1、Code First模式的代碼回顧

上篇隨筆里面我構造了幾個代表性的表結構,具體關系如下所示。

 

 

這些表包含了幾個經典的關系,一個是自引用關系的Role表,一個是User和Role表的多對多關系,一個是User和UserDetail之間的引用關系。

我們看到,默認使用EF工具自動生成的實體類代碼如下所示。

    [Table("Role")]
    public partial class Role
    {
        public Role()
        {
            Children = new HashSet<Role>();
            Users = new HashSet<User>();
        }

        [StringLength(50)]
        public string ID { get; set; }

        [StringLength(50)]
        public string Name { get; set; }

        [StringLength(50)]
        public string ParentID { get; set; }

        public virtual ICollection<Role> Children { get; set; }

        public virtual Role Parent { get; set; }

        public virtual ICollection<User> Users { get; set; }
    }

而其生成的數據庫操作上下文類的代碼如下所示。

    public partial class DbEntities : DbContext
    {
        public DbEntities() : base("name=Model1")
        {
        }
        public virtual DbSet<Role> Roles { get; set; }
        public virtual DbSet<User> Users { get; set; }
        public virtual DbSet<UserDetail> UserDetails { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Role>()
                .HasMany(e => e.Children)
                .WithOptional(e => e.Parent)
                .HasForeignKey(e => e.ParentID);

            modelBuilder.Entity<Role>()
                .HasMany(e => e.Users)
                .WithMany(e => e.Roles)
                .Map(m => m.ToTable("UserRole"));

            modelBuilder.Entity<User>()
                .HasMany(e => e.UserDetails)
                .WithOptional(e => e.User)
                .HasForeignKey(e => e.User_ID);

            modelBuilder.Entity<UserDetail>()
                .Property(e => e.Height)
                .HasPrecision(18, 0);
        }
    }

 

2、使用Fluent API 配置的Code First模式代碼結構

不管是Code First模式中使用 Fluent API 配置,還是使用了前面的Attribute特性標記的說明,都是為了從代碼層面上構建實體類和表之間的信息,或者多個表之間一些關系,不過如果我們把這些實體類Attribute特性標記去掉的話,那么我們就可以通過Fluent API 配置進行屬性和關系的指定了。

其實前面的OnModelCreating函數里面,已經使用了這種方式來配置表之間的關系了,為了純粹使用Fluent API 配置,我們還需要把實體類進行簡化,最終我們可以獲得真正的實體類信息如下所示。

    public partial class User
    {
        public User()
        {
            UserDetails = new HashSet<UserDetail>();
            Roles = new HashSet<Role>();
        }

        public string ID { get; set; }

        public string Account { get; set; }

        public string Password { get; set; }

        public virtual ICollection<UserDetail> UserDetails { get; set; }

        public virtual ICollection<Role> Roles { get; set; }
    }

這個實體類和我們以往的表現幾乎一樣,沒有多余的信息,唯一多的就是完全是實體對象化了,包括了一些額外的關聯對象信息。

前面說了,Oracle的生成實體類字段全部為大寫字母,不過我們實體類還是需要保持它的Pascal模式書寫格式,那么就可以在Fluent API 配置進行指定它的字段名為大寫(注意,Oracle一定要指定字段名為大寫,因為它是大小寫敏感的)

最終我們定義了Oracle數據庫USERS表對應映射關系如下所示。

    /// <summary>
    /// 用戶表USERS的映射信息(Fluent API 配置)
    /// </summary>
    public class UserMap : EntityTypeConfiguration<User>  
    {
        public UserMap()
        {
            HasMany(e => e.UserDetails).WithOptional(e => e.User).HasForeignKey(e => e.User_ID);
            
            Property(t => t.ID).HasColumnName("ID");
            Property(t => t.Account).HasColumnName("ACCOUNT");
            Property(t => t.Password).HasColumnName("PASSWORD");

            ToTable("WHC.USERS");
        }
    }

我們為每一個字段進行了字段名稱的映射,而且Oracle要大寫,我們通過 ToTable("WHC.USERS") 把它映射到了WHC.USERS表里面了。

如果對於有多對多中間表關系的Role來說,我們看看它的關系代碼如下所示。

    /// <summary>
    /// 用戶表 ROLE 的映射信息(Fluent API 配置)
    /// </summary>
    public class RoleMap : EntityTypeConfiguration<Role>  
    {
        public RoleMap()
        {
            Property(t => t.ID).HasColumnName("ID");
            Property(t => t.Name).HasColumnName("NAME");
            Property(t => t.ParentID).HasColumnName("PARENTID");
            ToTable("WHC.ROLE");
            
            HasMany(e => e.Children).WithOptional(e => e.Parent).HasForeignKey(e => e.ParentID);
            HasMany(e => e.Users).WithMany(e => e.Roles).Map(m=>
                {
                    m.MapLeftKey("ROLE_ID");
                    m.MapRightKey("USER_ID");
                    m.ToTable("USERROLE", "WHC");
                });
        }
    }

這里注意的是MapLeftKey和MapRightKey一定的對應好了,否則會有錯誤的問題,一般情況下,開始可能很難理解那個是Left,那個是Right,不過經過測試,可以發現Left的肯定是指向當前的這個映射實體的鍵(如上面的為ROLE_ID這個是Left一樣,因為當前的實體映射是Role對象)。

通過這些映射代碼的建立,我們為每個表都建立了一一的對應關系,剩下來的就是把這映射關系加載到數據庫上下文對象里面了,還記得剛才說到的OnModelCreating嗎,就是那里,一般我們加載的方式如下所示。

            //手工加載
            modelBuilder.Configurations.Add(new UserMap());
            modelBuilder.Configurations.Add(new RoleMap());
            modelBuilder.Configurations.Add(new UserDetailMap()); 

這種做法代替了原來的臃腫代碼方式。

            modelBuilder.Entity<Role>()
                .HasMany(e => e.Children)
                .WithOptional(e => e.Parent)
                .HasForeignKey(e => e.ParentID);

            modelBuilder.Entity<Role>()
                .HasMany(e => e.Users)
                .WithMany(e => e.Roles)
                .Map(m => m.ToTable("UserRole"));

            modelBuilder.Entity<User>()
                .HasMany(e => e.UserDetails)
                .WithOptional(e => e.User)
                .HasForeignKey(e => e.User_ID);

            modelBuilder.Entity<UserDetail>()
                .Property(e => e.Height)
                .HasPrecision(18, 0);

一般情況下,到這里我認為基本上把整個思路已經介紹完畢了,不過精益求精一貫是個好事,對於上面的代碼我還是覺得不夠好,因為我每次在加載 Fluent API 配置的時候,都需要指定具體的映射類,非常不好,如果能夠把它們動態加載進去,豈不妙哉。

對類似下面的關系硬編碼可不是一件好事。

modelBuilder.Configurations.Add(new UserMap());
modelBuilder.Configurations.Add(new RoleMap());
modelBuilder.Configurations.Add(new UserDetailMap()); 

我們可以通過反射方式,把它們進行動態的加載即可。這樣OnModelCreating函數處理的時候,就是很靈活的了,而且OnModelCreating函數只是在程序啟動的時候映射一次而已,即使重復構建數據庫操作上下文對象DbEntities的時候,也是不會重復觸發這個OnModelCreating函數的,因此我們利用反射不會有后顧之憂,性能只是第一次慢一點而已,后面都不會重復觸發了。

最終我們看看一步步下來的代碼如下所示(注釋的代碼是不再使用的代碼)。

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            #region MyRegion
            //modelBuilder.Entity<Role>()
            //    .HasMany(e => e.Children)
            //    .WithOptional(e => e.Parent)
            //    .HasForeignKey(e => e.ParentID);

            //modelBuilder.Entity<Role>()
            //    .HasMany(e => e.Users)
            //    .WithMany(e => e.Roles)
            //    .Map(m => m.ToTable("UserRole"));

            //modelBuilder.Entity<User>()
            //    .HasMany(e => e.UserDetails)
            //    .WithOptional(e => e.User)
            //    .HasForeignKey(e => e.User_ID);

            //modelBuilder.Entity<UserDetail>()
            //    .Property(e => e.Height)
            //    .HasPrecision(18, 0);

            //手工加載
            //modelBuilder.Configurations.Add(new UserMap());
            //modelBuilder.Configurations.Add(new RoleMap());
            //modelBuilder.Configurations.Add(new UserDetailMap()); 
            #endregion

            //使用數據庫后綴命名,確保加載指定的數據庫映射內容
            //string mapSuffix = ".Oracle";//.SqlServer/.Oracle/.MySql/.SQLite
            string mapSuffix = ConvertProviderNameToSuffix(defaultConnectStr.ProviderName);
            
            var typesToRegister = Assembly.GetExecutingAssembly().GetTypes()
                .Where(type => type.Namespace.EndsWith(mapSuffix, StringComparison.OrdinalIgnoreCase))
                .Where(type => !String.IsNullOrEmpty(type.Namespace))
                .Where(type => type.BaseType != null && type.BaseType.IsGenericType
                    && type.BaseType.GetGenericTypeDefinition() == typeof(EntityTypeConfiguration<>));

            foreach (var type in typesToRegister)
            {
                dynamic configurationInstance = Activator.CreateInstance(type);
                modelBuilder.Configurations.Add(configurationInstance);
            }
            base.OnModelCreating(modelBuilder); 
        }

這樣我們運行程序運行正常,不在受約束於實體類的字段必須是大寫的憂慮了。而且動態加載,對於我們使用其他數據庫,依舊是個好事,因為其他數據庫也只需要修改一下映射就可以了,真正遠離了復雜的XML和實體類臃腫的Attribute書寫內容,實現了非常彈性化的映射處理了。

最后我貼出一下測試的代碼例子,和前面的隨筆使用沒有太大的差異。

        private void button1_Click(object sender, EventArgs e)
        {
            DbEntities db = new DbEntities();

            User user = new User();
            user.Account = "TestName" + DateTime.Now.ToShortTimeString();
            user.ID = Guid.NewGuid().ToString();
            user.Password = "Test";

            UserDetail detail = new UserDetail() { ID = Guid.NewGuid().ToString(), Name = "userName33", Sex = 1, Note = "測試內容33", Height = 175 };
            user.UserDetails.Add(detail);
            db.Users.Add(user);

            Role role = new Role();
            role.ID = Guid.NewGuid().ToString();
            role.Name = "TestRole";
            //role.Users.Add(user);

            user.Roles.Add(role);
            db.Users.Add(user);
            //db.Roles.Add(role);
            db.SaveChanges();

            Role roleInfo = db.Roles.FirstOrDefault();
            if (roleInfo != null)
            {
                Console.WriteLine(roleInfo.Name);
                if (roleInfo.Users.Count > 0)
                {
                    Console.WriteLine(roleInfo.Users.ToList()[0].Account);
                }
                MessageBox.Show("OK");
            }
        }

測試Oracle數據庫,我們可以發現數據添加到數據庫里面了。

而且上面例子也創建了總結表的對應關系,具體數據如下所示。

 

如果是SQLServer,我們還可以看到數據庫里面添加了一個額外的表,如下所示。

 如果表的相關信息變化了,記得把這個表里面的記錄清理一下,否則會出現一些錯誤提示,如果去找代碼,可能會發現浪費很多時間都沒有很好定位到具體的問題的。

這個表信息,在其它數據庫里面沒有發現,如Oracle、Mysql、Sqlite里面都沒有,SQLServer這個表的具體數據如下所示。

整個項目的結構優化為標准的框架結構后,結構層次如下所示。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM