EFCore實現數據庫水平分表的方法

水平分表

當我們數據庫中某個表的數據量大到足以影響性能的時候,一般可以使用兩種方案解決。
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 類型的多個模型之間切換

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章