1. 程式人生 > >手把手教你:讓EF動態支援新增表、動態支援多資料庫

手把手教你:讓EF動態支援新增表、動態支援多資料庫

名詞解釋:此動態非執行時動態,讓EF動態支援新增表、動態切換資料庫意在不改變專案核心框架,

通過新增或者替換元件的方式達到標題目地。

一、先來點簡單的,動態支援多資料庫

AppDbContext實現:

public class AppDbContext:DbContext
    {
        public AppDbContext(string configKey)
            : base(configKey)
        {
        }
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }
    }

在AppDbContext建構函式中新增configKey引數,通過configKey引數指定配置檔案中的連線字串的配置項;

建立IDbContextProvider介面,如下:

public interface IDbContextProvider
    {
        AppDbContext Get();
    }

意圖很明顯了,通過IDbContextProvider來提供AppDbContext,這樣我們首先將AppDbContext與業務層解耦;

繼續建立2個專案:MsSqlProvider、MySqlProvider,分別實現IDbContextProvider介面:

MsSql:

public class MsSqlProvider:IDbContextProvider
    {
        AppDbContext m_AppDbContext = null;
        public AppDbContext Get()
        {
            return m_AppDbContext ?? new AppDbContext("MsSql");
        }
    }

MySql:

  public class MySqlProvider:IDbContextProvider
    {
        AppDbContext m_AppDbContext = null;
        public AppDbContext Get()
        {
            return m_AppDbContext ?? new AppDbContext("MySql");
        }
    }

下面繼續解釋動態支援/切換DbContextProvider,沒錯…聰明的你一開始就應該想到了..依賴注入,這個時候我們就需要使用依賴注入來完成使命了;

我已MEF為例來演示下如何動態獲取2種DbContextProvider:

首先為我們的IDbContextProvider新增 [InheritedExport] 標記,然後分別為兩種Provider新增 [Export]標記;

"MEF的使用還請大家自己去熟悉,我也僅僅是會使用而已,並不精通"

接著在Demo中新增App.Config和測試程式碼;

App.Config:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="MsSql" connectionString="Data Source=LIANG-HU-PC;Initial Catalog=appbase;Integrated Security=True;Pooling=False" providerName="System.Data.SqlClient" />
    <add name="MySql" connectionString="server=localhost;User Id=root;password=mysql;Persist Security Info=True;database=appbase" providerName="MySql.Data.MySqlClient" />
  </connectionStrings>
</configuration>

這裡要提醒下哦:要使MySql能夠支援EF使用的話,需要到MySql官方下載最新的驅動;

測試程式碼如下:

 class Program
    {
        [ImportMany]
        static IEnumerable<IDbContextProvider> m_Providers = null;
        static void Main(string[] args)
        {
            //使用目錄方式查詢MEF部件
            var catalog = new DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory);
            //建立Container
            var container = new CompositionContainer(catalog);
            //獲取Export項,這裡直接載入不採用Lazy
            m_Providers = container.GetExportedValues<IDbContextProvider>();
            if (m_Providers != null)
            {
                foreach (var provider in m_Providers)
                {
                    Console.WriteLine(provider.Get().Database.Connection.ConnectionString);
                }
            }
            Console.ReadKey(false);
        }

OK,我們來編譯測試下,當應用程式目錄下沒有任何Provider的時候是沒有獲取到任何是不會獲取到任何Provider的,如果只放置MySqlProvider再執行的話結果如下:

image

放置兩項Provider元件的時候自然就會是兩個都被獲取到,我就不演示了;

到這裡可能很多人就要噓聲一片了,也許你會提出一些問題:

比如:

1)為什麼不做一個Provider的實現,在Get()方法或者建構函式中依賴注入引數呢?

其實這樣做的目地是我們在使用UnitOfWork和Repository模式時能夠簡單方便的獲取DbContext;

可參見示例:

    /// <summary>
    /// Entity Framework Repository
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class EFRepository<T>:IRepository<T>
        where T:class
    {
        readonly IDataBaseFactory m_DataBaseFactory = null;
        public EFRepository(IDataBaseFactory dataBaseFactory)
        {
            if(dataBaseFactory==null)
            {
                throw new ArgumentNullException("DataBaseFactory");
            }
            m_DataBaseFactory=dataBaseFactory;
        }
        DbContext m_DbContext = null;
        protected DbContext DbContext
        {
            get
            {
                return m_DbContext ?? m_DataBaseFactory.Get();
            }
        }
對UnitOfWork模式的使用與此類似;

2)我只需要一個DbContext,但有時候需要切換資料庫,那怎麼辦呢?

這個問題是ico與依賴注入方面的基礎內容,需要您自己去學習哦;

至此,簡單的“動態”支援多資料庫示例就完成了~~~ 我們的關鍵還是動態支援新建表,下面我們就來一步一步實踐吧;

二、“動態”支援新建表,計劃先行

首先我們建立ModelBase類庫,存放一些與實體相關的介面和基類,結構如圖所示:

image根據專案結構,我需要給大家解釋每個檔案的存在意義;

IEntity介面與AbstractEntityBase類,顧名思義,大家應該猜得到它們是實體基類,為什麼要如此定義呢,主要是方便我們寫實體的時候直接繼承Id屬性,(因為我們的所有表主鍵都是Guid且名為Id)

public interface IEntity
    {
        Guid Id { get; }
    }
public abstract class AbstractEntityBase : IEntity
    {
        public AbstractEntityBase()
        {
            this.Id = Guid.NewGuid();
        }
        [Key]
        [Required]
        public Guid Id
        {
            get;
            protected set;
        }
    }

還有一個好處就是我們直接在基類中描述 主鍵關係,在寫實體的時候直接繼承後,可以省去很多重複操作哦^_^

再來說IMapping和Mapping,為什麼要有這2個基類介面呢,出於以下方面考慮:

1)將實體與資料庫的對映關係產生Mapping類與DbContext類解耦(這個會在下面具體出現時再說)

2)通過MappingBase基類實現一些公共操作,避免每個實體類的重複操作,具體看程式碼你就會明白;

 [InheritedExport]
    public interface IMapping
    {
        void RegistTo(ConfigurationRegistrar configurationRegistrar);
    }
public class MappingBase<TEntity> : EntityTypeConfiguration<TEntity>, IMapping
        where TEntity : class,IEntity
    {
        public MappingBase()
        {
            this.Map(m => m.ToTable(typeof(TEntity).Name));
        }
        public void RegistTo(ConfigurationRegistrar configurationRegistrar)
        {
            configurationRegistrar.Add(this);
        }
    }

呵呵,有了“動態”支援多資料庫,這裡很多人應該就能猜到我們如何“動態”支援新增表咯;注意這裡的IMapping介面的精妙所在哦,您發現了嗎???;

三、萬事俱備,只欠東風

我們先在ModelA類庫中建立一個User實體和Role實體,同時建立UserMapping和RoleMapping,(為什麼要建立Mapping類,後面我會講)

USer 、UserMapping:

 /*
     * 為什麼沒有通過[Table]來指明表明呢,
     * 並不是因為我們需要EF自己支援的表明方式
     * 而是我們繼承自AbstractEntityBase,在其基類已經實現了將類名對映為表名
     */
    public class User : AbstractEntityBase
    {
        [Required]
        public string Username { get; set; }
        [Required]
        public string Password { get; set; }
        /*
         * 注意這裡,我為什麼不通過DataAnnotations方式新增外來鍵關聯呢
         * 個人認為User實體與Role實體關聯,已經擁有Role屬性了,
         * 如果在新增一個RoleId來表示外來鍵關係,會讓我覺得User類不夠清爽
         * 所以我的做法是新增UserMapping類來指定它與Role實體的關係
         * 
         * 但是有一點要注意,如果不指定外來鍵的話,預設資料庫外來鍵是為 表名_主鍵(Role_Id)型別
         */
        public virtual Role Role{get;set;}
    }
[Export("UserMapping")]
    public class UserMapping:MappingBase<User>
    {
        public UserMapping()
        {
            this.HasRequired(m => m.Role)
                .WithMany(m => m.Users);
            /*注意這裡沒有指定HasForeignKey哦*/
        }
    }

Role類和RoleMapping的實現也是同理,結合上面程式碼中的註釋內容,我想大家也能夠理解我的良苦用心了吧;如果還不能理解,我們再看下DbContext是如何實現的:

/*
     * 很清爽的DbContext,完全不包含任何DbSet
     * 通過Mapping來載入表結構
     */
    public class AppDbContext:DbContext
    {
        public AppDbContext(string configKey)
            : base(configKey)
        {
            //可以設定通過反向方式建立表哦,但是我們演示的目地不在於此
            //Database.SetInitializer(new DropCreateDatabaseIfModelChanges<AppDbContext>());
            //載入目錄下所有IMapping實現
            var catalog = new DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory);
            var container = new CompositionContainer(catalog);
            m_Mappings = container.GetExportedValues<IMapping>();
        }
        [ImportMany]
        IEnumerable<IMapping> m_Mappings = null;
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            if (m_Mappings != null)
            {
                //這裡是關鍵
                foreach (var mapping in m_Mappings)
                {
                    mapping.RegistTo(modelBuilder.Configurations);
                }
            }
            base.OnModelCreating(modelBuilder);
        }
    }

沒錯,我們的目地就是讓DbContext完全依靠IMapping去載入解釋表結構關係,這樣即保證DbContext不包含大量的DbSet,又能夠非常好的將DbContext與實體解耦,

更重要的是,我們通過 DataAnnotations 和 Fluent API結合使用,讓我們的實體也非常清爽;

到這裡,其實我已經把核心的內容都展現出來了,對於新增表的動態使用也就類似與最前面講的“動態”支援多資料庫,我們只需要依賴注入所有的IMapping的實現,就可以讓DbContext自動去解釋所有表結構了(所以DbContext的OnModelCreating方法是關鍵所在)。

好,接著我們在新增一個ModelB 作為新增表NewModel實體的載體,來演示是我們的示例是否能夠如題所描述的那樣,不改變核心框架的前提下動態支援新增的表和實體。

 public class NewModel : AbstractEntityBase
    {
        [Required]
        public string Name { get; set; }
    }

我們按照之前做CmdDemo的方式新增一個AppDemo,並新增App.Config檔案,同時建立一個DataViewControl的自定義控制元件用來顯示資料;

我們來看下AppDemo的演示:

image

其具體實現為:

    /// <summary>
    /// MainWindow.xaml 的互動邏輯
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            IDbContextProvider provider = new MsSqlProvider.MsSqlProvider();
            AppDbContext dbContext = provider.Get();
            var users = dbContext.Set<User>();
            if (users != null)
            {
                users.Add(new User()
                {
                    Username = "admin",
                    Password = "admin",
                    Role = new Role() { Name = "administrators" }
                });
                dbContext.SaveChanges();
                DataViewControl usersViewControl=new DataViewControl();
                usersViewControl.Binding(users.ToList());
                TabItem item = new TabItem();
                item.Header = "User表展示";
                item.Content = usersViewControl;
                this.myTabControl.Items.Add(item);
            }
            var roles = dbContext.Set<Role>();
            if (roles != null)
            {
                DataViewControl rolesViewControl = new DataViewControl();
                rolesViewControl.Binding(roles.ToList());
                TabItem item = new TabItem();
                item.Header = "Role表展示";
                item.Content = rolesViewControl;
                this.myTabControl.Items.Add(item);
            }
            /*
             * 請注意此處,我們的NewModel還是和應用耦合在一起了,
             * 並沒有像我們標題說的動態載入;
             * 這裡主要是為了演示方便,我就不在做實體與業務層的解耦了,
             * 一般我們的應用可能是單獨的UI模組和它對應的實體耦合,而不是UI框架耦合
             * 僅在需要的時候載入不同模組的UI元件
             *
             */
            var newModels = dbContext.Set<NewModel>();
            if (newModels != null)
            {
                DataViewControl newModelsViewControl = new DataViewControl();
                newModelsViewControl.Binding(newModels.ToList());
                TabItem item = new TabItem();
                item.Header = "NewModel表展示";
                item.Content = newModelsViewControl;
                this.myTabControl.Items.Add(item);
            }
        }
    }

需要解釋的是AppDemo中沒有很好的演示怎麼動態支援新建表,其實我前面解釋過ModelB中NewModel就是新增的表,主要是為了給大家展示實現思路,我並沒有去把NewModel和AppDemo去解耦,所以沒有很好的演示效果,但是實際上是沒有問題的,這就跟我們具體的應用息息相關了。

到這裡,我們已經完整的解釋了整個過程,為此我也是邊建立專案邊寫部落格,在最後會附上完整專案原始碼,有興趣的可以自行下載學習;如果這篇文章對你有所啟發,或者讓你學到了一些東西,那是我非常樂見的,同時也希望各位高手不要鄙視。