水平分表
當我們數據庫中某個表的數據量大到足以影響性能的時候,一般可以使用兩種方案解決。
1、添加索引。
2、採用分表、分庫策略。
分表策略有兩種,水平分表和垂直分表。
垂直分表的思路是把表中使用頻繁的字段分離到另一個表裏存放,提高查詢效率。
水平分表的思路是把表數據按一定的規則,分到其他表結構相同的數據表,以降低單個表的負荷。
在ASP.net Core的開發中,對於數據庫的操作幾乎離不開EFCore,那麼如果要用EFCore的情況下實現水平分表該怎麼實現呢?衆所周知,EFCore中一個類就映射一個數據表,現在要想一個類映射多個數據表,從實現的角度可以有兩種方案。
1、在數據庫裏通過存儲過程等操作實現。
2、在代碼里根據規則動態映射到不同的數據表。
由於本人不是專業的數據庫開發人員,所以這裏我以第二種方案實現。
代碼運行環境
EFCore版本:2.2.6
測試控制檯程序:.net core 2.2
數據庫提供程序:MySql.Data.EntityFrameworkCore 8.0.17
經檢驗,EFCore 3.0以上已經不能使用此方法了,EFCore 3.0以上的實現代碼請直接移步到下文中的 EFCore 3.x 節。
實現方法
這裏Post類對應兩個數據表:post_odd 和 post_even
代碼
Program.cs
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
namespace TableMappingTest
{
class Program
{
static void Main(string[] args)
{
string table1 = "post_odd";
string table2 = "post_even";
BloggingContext context = new BloggingContext();
// step1:改變實體模型緩存工廠的返回值,使EFCore認爲Model已經發生改變,下次使用實體前將更新模型映射
DynamicModelCacheKeyFactory.ChangeTableMapping();
// step2:獲取實體模型Post的映射(這裏使用了實體模型,所以會更新模型映射)
if (context.Model.FindEntityType(typeof(Post))?.Relational() is RelationalEntityTypeAnnotations relational)
{
// step3:修改Post實體映射的數據表
relational.TableName = table1;
}
// 此時該context內Post實體的映射表已經是 post_odd, 就算重複以上3步也不會改變,除非重新new一個
List<Post> list1 = context.Set<Post>().Where(s => true).ToList();
Console.WriteLine(table1);
PrintList(list1);
// 改另一個表測試
BloggingContext context_1 = new BloggingContext();
DynamicModelCacheKeyFactory.ChangeTableMapping();
if (context_1.Model.FindEntityType(typeof(Post))?.Relational() is RelationalEntityTypeAnnotations r)
{
r.TableName = table2;
}
List<Post> list2 = context_1.Set<Post>().Where(s => true).ToList();
Console.WriteLine(table2);
PrintList(list2);
Console.ReadKey();
}
static void PrintList(List<Post> list)
{
foreach(Post item in list)
{
Console.WriteLine(item);
}
Console.WriteLine();
}
}
}
Models.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Threading;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
namespace TableMappingTest
{
/// <summary>
/// 用於替換的模型緩存工廠
/// </summary>
public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory
{
private static int m_Marker = 0;
/// <summary>
/// 改變模型映射,只要Create返回的值跟上次緩存的值不一樣,EFCore就認爲模型已經更新,需要重新加載
/// </summary>
public static void ChangeTableMapping()
{
Interlocked.Increment(ref m_Marker);
}
/// <summary>
/// 重寫方法
/// </summary>
/// <param name="context">context模型</param>
/// <returns></returns>
public object Create(DbContext context)
{
return (context.GetType(), m_Marker);
}
}
// Context模型
public class BloggingContext : DbContext
{
public virtual DbSet<Blog> Blogs { get; set; }
public virtual DbSet<Post> Posts { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// step0: 調用ReplaceService替換掉默認的模型緩存工廠
optionsBuilder.UseMySQL("連接字符串")
.ReplaceService<IModelCacheKeyFactory, DynamicModelCacheKeyFactory>()
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>(entity =>
{
entity.HasKey(e => e.BlogId);
entity.ToTable("blog");
entity.HasIndex(s => s.UserId)
.HasName("blog_user_FK_index");
entity.Property(e => e.BlogId)
.HasColumnName("blogid")
.HasColumnType("int(11)")
.ValueGeneratedOnAdd();
entity.Property(e => e.Rating)
.HasColumnName("rating")
.HasColumnType("int(11)");
entity.Property(e => e.UserId)
.HasColumnType("int(11)")
.HasColumnName("userId");
});
modelBuilder.Entity<Post>(entity =>
{
entity.HasKey(e => e.PostId);
entity.ToTable("post");
entity.HasIndex(e => e.BlogId)
.HasName("post_blog_FK_idx");
entity.Property(e => e.PostId)
.HasColumnName("postid")
.HasColumnType("int(11)")
.ValueGeneratedOnAdd();
entity.Property(e => e.Title)
.HasColumnName("title")
.HasMaxLength(64);
entity.Property(e => e.Content)
.HasColumnName("content")
.HasMaxLength(1024);
entity.Property(e => e.BlogId)
.HasColumnName("blogId")
.HasColumnType("int(11)");
entity.HasOne(e => e.Blog)
.WithMany(s => s.Posts);
});
}
}
public class Blog
{
public Blog()
{
Posts = new HashSet<Post>();
}
public int BlogId { get; set; }
public string Url { get; set; }
public int Rating { get; set; }
public int UserId { get; set; }
[DefaultValue(null)]
public ICollection<Post> Posts { get; set; }
// 爲了方便測試就重寫了ToString方法
public override string ToString()
{
return $"Id: {BlogId} Url: {Url}";
}
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
[DefaultValue(null)]
public Blog Blog { get; set; }
// 爲了方便測試就重寫了ToString方法
public override string ToString()
{
return $"Id: {PostId} Title: {Title}";
}
}
}
運行結果
可以看到,同一條查詢表達式,讀了不同的數據表。
實現步驟
正如代碼中一樣,步驟如下:
1.我們要自己定義一個ModelCacheKeyFactory類,實現IModelCacheKeyFactory接口,該接口就一個Create方法,返回的是一個對象
作用:EFCore會利用這個對象,調用這個對象的Equals方法判斷映射模型是否改變,如果改變了,就不會使用舊的緩存,重新調用OnModelCreating加載新的映射模型。
2.將我們定義的ModelCacheKeyFactory類通過配置的形式替換掉EFCore默認的工廠類。在Context的OnConfiguring方法中調用ReplaceService。
3.當我們需要更換映射表的時候,想辦法使我們自己定義的ModelCacheKeyFactory類的Create方法返回不同的值。這裏我採用了靜態變量自增的辦法。
4.用context.Model.FindEntityType({Type}).Relational() 方法獲取實體模型的映射。
5.設置該映射的數據表。
注意,因爲這裏動態修改DbContext的映射關係會影響到所有使用該DbContext的線程,所以它不是線程安全的。事實上所有基於EFCore動態修改表映射關係的方案都幾乎不可能做到線程安全,所以如果你想在EFCore中動態修改表映射關係就一定要注意避免多線程共用DbContext。
EFCore 3.x
經檢驗,在EFCore 3.0之後,已經不能對IEntityType使用Relational()方法獲取RelationalEntityTypeAnnotations,猜測可能是因爲這樣做很不安全,在DbContext創建出來之後再動態修改映射表可能會影響到其他線程,舉個例子,有可能A線程和B線程同時使用一個DbContext訪問數據庫,但是A線程中途把映射表修改了,B線程剛好需要撤銷某些操作,因爲A線程吧映射關係改了,所以B線程的操作被影響到。
所以如果要實現動態修改映射表,只能在DbContext對象被創建出來的時候動態指定,並保證DbContext的生命週期內表的映射關係不能被改變,如果要改只能重新創建一個DbContext對象。這就需要在DbContext的OnModelCreating()方法中做做文章了,我們需要在調用這個方法的時候就明確指定那個類型映射哪個表,這就意味着,如果我們要實現動態切換映射表,就必須加一層封裝,切換的時候根據新的映射關係重新創建DbContext。
具體的實現請參考
https://github.com/YinRunhao/DataAccessHelper/tree/EFCore31
進一步封裝
以上的例子雖然很簡陋,但功能是實現了。倘若需要應用到項目裏,這種封裝程度是遠遠不夠的,還需要對代碼進行進一步封裝。以下是我本人對以上代碼的一個簡單封裝,希望能幫助到有需要的同學。
只適用於EFCore 3.0前版本
https://github.com/YinRunhao/DataAccessHelper/tree/master
適用於EFCore 2.x 和3.x
https://github.com/YinRunhao/DataAccessHelper/tree/EFCore31
知識點總結
1.EFCore 默認是一個類映射一個數據表,並通過調用OnModelCreating方法(或其他方法)實現類、屬性和數據表、字段等的映射。
2.可以在OnConfiguring方法中通過調用ReplaceService方法來注入自己的實現IModelCacheKeyFactory的工廠類。
3.EFCore 的映射關係有緩存機制,一般情況下只會在context第一次用到實體時調用一次OnModelCreating建立映射關係,然後將映射關係緩存下來。
4.可以通過自行實現IModelCacheKeyFactory的辦法改變EFCore 的緩存行爲(可改成永不緩存或者有改變後再緩存等)。
5.EFCore 的映射關係緩存行爲由IModelCacheKeyFactory派生類的Create方法所決定,若Create方法返回的值和上次緩存的值一樣就不會調用OnModelCreating方法來更新映射關係。
6.要使IModelCacheKeyFactory派生類的Create方法返回的值與上次不一樣,不一定要重寫Equals方法和GetHashCode的方法;可以通過返回一個元組,且元組中的某個值類型不一樣即可(微軟文檔裏的騷操作)。
如果這篇文章有幸能幫助到你,請不要吝嗇你的贊。
參考文章
使用EntityFrameworkCore實現Repository, UnitOfWork,支持MySQL分庫分表
使用EntityFrameworkCore實現Repository, UnitOfWork,支持MySQL分庫分表
EFCore文檔:具有相同 DbContext 類型的多個模型之間切換