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://gitee.com/yinrunhao/data-access-helper

进一步封装

以上的例子虽然很简陋,但功能是实现了。倘若需要应用到项目里,这种封装程度是远远不够的,还需要对代码进行进一步封装。以下是我本人对以上代码的一个简单封装,希望能帮助到有需要的同学。

只适用于EFCore 3.0以上版本
https://gitee.com/yinrunhao/data-access-helper

知识点总结

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

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智慧校园整体解决方案是响应国家教育信息化政策,结合教育改革和技术创新的产物。该方案以物联网、大数据、人工智能和移动互联技术为基础,旨在打造一个安全、高效、互动且环保的教育环境。方案强调从数字化校园向智慧校园的转变,通过自动数据采集、智能分析和按需服务,实现校园业务的智能化管理。 方案的总体设计原则包括应用至上、分层设计和互联互通,确保系统能够满足不同用户角色的需求,并实现数据和资源的整合与共享。框架设计涵盖了校园安全、管理、教学、环境等多个方面,构建了一个全面的校园应用生态系统。这包括智慧安全系统、校园身份识别、智能排课及选课系统、智慧学习系统、精品录播教室方案等,以支持个性化学习和教学评估。 建设内容突出了智慧安全和智慧管理的重要性。智慧安全管理通过分布式录播系统和紧急预案一键启动功能,增强校园安全预警和事件响应能力。智慧管理系统则利用物联网技术,实现人员和设备的智能管理,提高校园运营效率。 智慧教学部分,方案提供了智慧学习系统和精品录播教室方案,支持专业级学习硬件和智能化网络管理,促进个性化学习和教学资源的高效利用。同时,教学质量评估中心和资源应用平台的建设,旨在提升教学评估的科学性和教育资源的共享性。 智慧环境建设则侧重于基于物联网的设备管理,通过智慧教室管理系统实现教室环境的智能控制和能效管理,打造绿色、节能的校园环境。电子班牌和校园信息发布系统的建设,将作为智慧校园的核心和入口,提供教务、一卡通、图书馆等系统的集成信息。 总体而言,智慧校园整体解决方案通过集成先进技术,不仅提升了校园的信息化水平,而且优化了教学和管理流程,为学生、教师和家长提供了更加便捷、个性化的教育体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值