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 类型的多个模型之间切换

发布了15 篇原创文章 · 获赞 2 · 访问量 4856

猜你喜欢

转载自blog.csdn.net/weixin_38138153/article/details/101997138
今日推荐