手把手教你:讓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再執行的話結果如下:
放置兩項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類庫,存放一些與實體相關的介面和基類,結構如圖所示:
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的演示:
其具體實現為:
/// <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去解耦,所以沒有很好的演示效果,但是實際上是沒有問題的,這就跟我們具體的應用息息相關了。
到這裡,我們已經完整的解釋了整個過程,為此我也是邊建立專案邊寫部落格,在最後會附上完整專案原始碼,有興趣的可以自行下載學習;如果這篇文章對你有所啟發,或者讓你學到了一些東西,那是我非常樂見的,同時也希望各位高手不要鄙視。