Abp开发日志:MariaDB(MySql)

在Abp中将数据库由SQL Server切换为MariaDB是相当简单的,但又有一些需要特别注意的问题,如MariaDB的索引字段,字段长度是有限制的,需要修改Abp框架内已定义好的索引字段的长度。本文是笔者在使用MariaDB数据库中的一些经验总结,希望对大家有所帮助。

修改数据库连接

数据库连接的定义在appsettings.json文件中,需要修改的地方有两处,一个是Web.Host项目,一个是Migrator项目。项目Web.Host是API服务器的启动项目,项目Migrator是用于迁移数据库的迁移项目。

打开这两个项目的appsettings.json文件,然后将ConnectionStringsDefault值修改为以下字符串就行了:

{
  "ConnectionStrings": {
    "Default": "Server=localhost;database=AbpDevelopmentLogDb;uid=root;pwd=abcd-1234;charset=UTF8;default command timeout=3600;"
  },
}

以上连接字符串要注意的最后一个超时时间的设置,由于执行表分区需要时间,经常会发生执行SQL语句超时的错误,因而将它设置大一点有好处。

数据提供者的选择

在微软的实体框架文档《数据库提供程序》中,可以找到实体框架(Core版)目前所能支持的数据库。对于MariaDB,只有Pomelo.EntityFrameworkCore.MySql表明是支持的,其他两个MySql.Data.EntityFrameworkCoreDevart.Data.MySql.EFCore只表明支持MySQL,不过感觉也能用。
对笔者来说,首选的当然是开源协议的数据提供者,因而Devart的这个,基本就不考虑了。余下的就是在OraclePomelo之间选择了。Oracle使用的是GPL2协议,Pomelo使用的是MIT协议,因而,在选择上,较为宽松的开源协议是首选,在这里MIT协议是比较宽松的开源协议。
至于性能上的区别,笔者没测试过,因为在笔者在使用MariaDB做开发的时候,Oracle还没开发数据提供者呢,当时Pomelo是唯一的选择,因而,在未来的选择上,笔者也懒得去做测试对比了。使用Pomelo在反馈和更新方面都会比Oracle做得好。还有一个更深层的原因,以Oracle哪个性格,哪天不鸟你也是可能,所以,我们的选择其实只有一个:Pomelo。当然,对于不差钱的,Devart可能是更好的选择。
选定了数据提供者,我们就可以在EntityFrameworkCore项目上单击鼠标右键,打开NuGet管理器,为项目添加Pomelo.EntityFrameworkCore.MySql程序包,并卸载Microsoft.EntityFrameworkCore.SqlServer程序包。

将DbContext的配置修改为使用MySQL

打开EntityFrameworkCore项目的AbpDevelopmentLogDbContextConfigurer.cs文件,会看到代码默认是使用UseSqlServer方法来初始化数据库的,我们要做的就是将UseSqlServer方法替换为UseMySql方法,替换后的代码如下:

    public static class AbpDevelopmentLogDbContextConfigurer
    {
        public static void Configure(DbContextOptionsBuilder<AbpDevelopmentLogDbContext> builder, string connectionString)
        {
            builder.UseMySql(connectionString);
        }

        public static void Configure(DbContextOptionsBuilder<AbpDevelopmentLogDbContext> builder, DbConnection connection)
        {
            builder.UseMySql(connection);
        }
    }

添加数据库查询日志

在调用实体框架的查询方法时,了解它生成的SQL语句来优化查询是一件必不可少的事。要实现这个,需要调用UseLoggerFactory方法为DbContext添加日志工厂。要为DbContext添加日志工厂,有两种方式。一是在配置类中直接定义日志工厂,该方式的主要问题是不能与Abp框架的日志合在一起,需要输出为独立的日志文件。分成独立的日志文件其实也有好处,不会与其他日志混在一起,阅读起来比较方便。另外一种方式是结合Abp框架的日志系统,而这需要将日志工厂传递给配置类,为了实现这个,需要将以上两个配置方法修改以下代码:

        public static void Configure(DbContextOptionsBuilder<AbpDevelopmentLogDbContext> builder, string connectionString, ILoggerFactory loggerFactory = null)
        {
            if (loggerFactory != null) builder.UseLoggerFactory(loggerFactory);
            builder.UseMySql(connectionString);
        }

        public static void Configure(DbContextOptionsBuilder<AbpDevelopmentLogDbContext> builder, DbConnection connection, ILoggerFactory loggerFactory = null)
        {
            if (loggerFactory != null) builder.UseLoggerFactory(loggerFactory);
            builder.UseMySql(connection);
        }

以上代码为原因方法添加了一个名为loggerFactory的参数,当该参数为null时,说明不需要记录实体日志,则不调用UseLoggerFactory方法,当该参数不为null时,则要调用UseLoggerFactory方法来设置日志工厂。
以上代码中要注意的是UseLoggerFactory方法传入的是Microsoft.Extensions.LoggingILoggerFactory类,不能是Castle.Core.LoggingILoggerFactory类。

修改好以上两个方法后,打开AbpDevelopmentLogEntityFrameworkModule.cs文件,并先为AbpDevelopmentLogEntityFrameworkModule类添加一个私有属性_useEfLog,该值为true时开启实体日志,为false是关闭实体日志,代码如下:

private readonly bool _useEfLog = true;

添加完_useEfLog属性后,将PreInitialize方法的代码修改为以下代码:

        public override void PreInitialize()
        {
            if (!SkipDbContextRegistration)
            {
                Configuration.Modules.AbpEfCore().AddDbContext<AbpDevelopmentLogDbContext>(options =>
                {
                    var loggerFactory = _useEfLog ? IocManager.Resolve<ILoggerFactory>() : null;
                    if (options.ExistingConnection != null)
                    {
                        AbpDevelopmentLogDbContextConfigurer.Configure(options.DbContextOptions, options.ExistingConnection, loggerFactory);
                    }
                    else
                    {
                        AbpDevelopmentLogDbContextConfigurer.Configure(options.DbContextOptions, options.ConnectionString, loggerFactory);
                    }
                });
            }
        }

以上代码主要修改的地方是添加了变量loggerFactory,该变量会根据_useEfLog的设置来获取框架的日志工厂或设置为null,然后将值传递给AbpDevelopmentLogDbContextConfigurerConfigure方法。

修改索引字段的长度

Pomelo.EntityFrameworkCore.MySql的Github首页上,建议数据库的字符集使用utf8mb4,我们跟着建议走就是了。

使用utf8mb4作为字符集,索引字段的长度就不能超过193,在这里,我们统一为190就行了。
打开EntityFrameworkCore项目AbpDevelopmentLogDbContext.cs的文件,在AbpDevelopmentLogDbContext类中,我们需要通过重写OnModelCreating方法使用Fluent API来重写字段长度,具体代码如下:

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.Entity<ApplicationLanguageText>(b => { b.Property(m => m.Key).HasMaxLength(190); });
            modelBuilder.Entity<EntityChange>(b => { b.Property(m => m.EntityTypeFullName).HasMaxLength(190); });
            modelBuilder.Entity<EntityChangeSet>(b => { b.Property(m => m.Reason).HasMaxLength(190); });
            modelBuilder.Entity<NotificationSubscriptionInfo>(b =>
            {
                b.Property(m => m.EntityTypeName).HasMaxLength(190);
            });
            modelBuilder.Entity<RoleClaim>(b => { b.Property(m => m.ClaimType).HasMaxLength(190); });
            modelBuilder.Entity<Setting>(b =>
            {
                b.Property(m => m.Name).HasMaxLength(190);
            });

            modelBuilder.Entity<UserClaim>(b =>
            {
                b.Property(m => m.ClaimType).HasMaxLength(190);
            });

            modelBuilder.Entity<UserLoginAttempt>(b =>
            {
                b.Property(m => m.UserNameOrEmailAddress).HasMaxLength(190);
            });

            modelBuilder.Entity<UserLogin>(b =>
            {
                b.Property(m => m.ProviderKey).HasMaxLength(190);
            });

            modelBuilder.Entity<UserLoginAttempt>(b =>
            {
                b.Property(m => m.UserNameOrEmailAddress).HasMaxLength(190);
            });

            modelBuilder.Entity<User>(b =>
            {
                b.Property(m => m.UserName).HasMaxLength(190);
                b.Property(m => m.EmailAddress).HasMaxLength(190);
                b.Property(m => m.NormalizedUserName).HasMaxLength(190);
                b.Property(m => m.NormalizedEmailAddress).HasMaxLength(190);
            });

            modelBuilder.Entity<UserAccount>(b =>
            {
                b.Property(m => m.EmailAddress).HasMaxLength(190);
                b.Property(m => m.UserName).HasMaxLength(190);
            });

        }

从以上代码可以看到,要修改的地方还是比较多的,不过这是一劳永逸的做法,在未来的项目,直接复制粘贴就行了。

不过,随着Abp框架的不断更新,可能会增加或修改表结构,有些地方可能还没加进去,这时候,不要担心,只要运行一次Migrator项目,就会看到索引创建错误,根据错误去修改字段长度就行了。

中文排序

在使用UTF来存储中文字符时,排序的依据并不是我们习惯的拼音排序,因而在需要使用拼音作为排序依据的字段上,我们需要将排序的字符集修改为gbk_chinese_ci,而这个,也需要使用Fluent API来实现,具体的代码如下:

            modelBuilder.Entity<OrganizationUnit>(b =>
                b.Property(m => m.DisplayName).HasColumnType("VARCHAR(128) COLLATE gbk_chinese_ci"));

            modelBuilder.Entity<User>(b =>
            {
                b.Property(m => m.UserName).HasMaxLength(190);
                b.Property(m => m.EmailAddress).HasMaxLength(190);
                b.Property(m => m.NormalizedUserName).HasMaxLength(190);
                b.Property(m => m.NormalizedEmailAddress).HasMaxLength(190);
                b.Property(m => m.Name).HasColumnType("VARCHAR(64) COLLATE gbk_chinese_ci");
                b.Property(m => m.Surname).HasColumnType("VARCHAR(64) COLLATE gbk_chinese_ci");
            });

以上代码,我们修改了3个地方,一个是组织的显示名称(DisplayName),其余两个是用户的名字(Name)和姓(Surname)。主要修改的地方就是为字段添加COLLATE gbk_chinese_ci声明。在排序声明前面的字段类型和长度定义是必须的,不然会出错,这个要注意。

表分区

对于一个交易量比较可观的系统,如一天的记录量达到10万级以上的表,对表进行分区,将数据分散于不同的文件中,以提高查询速度是必不可少的。对于Abp框架来说,它的日志记录表,在业务运作起来的时候,它的数据量也是很可观的,因而,对该表进行分区,也是很有必要的。
在进行表分区之前,需要考虑的是分区策略。对于分区策略,主要的考量点是数据量和分区文件数(MariaDB是使用文件来进行分区的,这个要注意)。假设,我们的日志表,每日的日志量是百万级的,那么,比较好的策略就是按日分区,也就是一日一个文件,让单个文件的数据量保持在50万到100万这个级别是比较合适的。按日分区需要考虑的问题是日益增长的文件数,1年会生成365文件,5年就是1800多个文件,如果要分区的表比较多,那么数据库目录就会有很多文件,这时候就会严重影响文件的读取速度。在这里,我们就要在做一个平衡,好好分析需要分区的表的数量以及每个表单日的数据量,然后做好规划,如应该在什么时候需要删除或合并旧数据以减少分区文件等等。当然,最好的解决方案就是将需要分区的表,放在不同的数据目录中,但这在目前架构中,实现比较麻烦,因为要将分区放在不同的文件夹,需要在创建表时指定路径,使用alert修改表是实现不了的,因而,可行的办法是使用迁移程序创建数据库后,再把要分区的表删除,然后再创建。
在当前,我们就不考虑这么多,演示一下怎么对日志表分区就行了。要对日志表进行分区,并且使用每日一个文件的策略,首先要做的是将日志表的主键修改为IdExecutionTime的复合主键,而这需要使用Fluent API来实现,具体代码如下:

            modelBuilder.Entity<AuditLog>(b =>
            {
                b.HasKey(m => new { m.Id, m.ExecutionTime });
                b.Property(m => m.Id).UseMySqlIdentityColumn();
                b.Property(m => m.ExecutionTime).HasDefaultValueSql("CURRENT_TIMESTAMP");

            });

代码中,HasKey方法将IdExecutionTime声明为了复合主键,为了保证主键的两个字段的值不会为空,需要使用UseMySqlIdentityColumn方法将Id字段声明为自增字段,使用HasDefaultValueSql方法将ExecutionTime的默认值设置为系统当前时间。

以上代码只是将日志表修改为可分区的表,并未实现分区,下面要做的是实现自动化分区,而不需要自己去编写SQL代码来分区。在EntityFrameworkCore项目的EntityFrameworkCore\Seed\Host文件夹下,新建一个名为TablePartitionBuilder的类,并加入以下代码:

    public class TablePartitionBuilder
    {
        private readonly AbpDevelopmentLogDbContext _context;

        public static string PartitionByDay;

        public TablePartitionBuilder(AbpDevelopmentLogDbContext context)
        {
            _context = context;
        }


        public void Create()
        {
            PartitionByDay = GetDayPartitionByDaySql();
            AlterAuditLog();
        }

        private void AlterAuditLog()
        {
            var tableName = GetTableName<AuditLog>();
            var sql = string.Format(PartitionByDay, tableName, "ExecutionTime");
            Console.WriteLine(sql);
            _context.Database.ExecuteSqlCommand(sql);
        }


        private static string GetDayPartitionByDaySql()
        {
            var stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("ALTER TABLE `{0}`");
            stringBuilder.AppendLine("PARTITION BY RANGE(to_days(`{1}`))( ");
            var currentYear = DateTime.Now.Year;
            for (var i = 0; i < 2; i++)
            {
                var year = currentYear + i;
                for (var j = 1; j <= 12; j++)
                {
                    var days = DateTime.DaysInMonth(year, j);
                    for (var k = 1; k <= days; k++)
                    {
                        stringBuilder.AppendLine(
                            $"PARTITION p{year}{j:00}{k:00} VALUES LESS THAN (to_days('{year}-{j:00}-{k:00}')),");
                    }
                }
            }
            stringBuilder.AppendLine("PARTITION pmax VALUES LESS THAN (MAXVALUE)");
            stringBuilder.AppendLine(");");
            return stringBuilder.ToString();
        }

        private string GetTableName<T>()
        {
            var mapping = _context.Model.FindEntityType(typeof(T)).Relational();
            return mapping.TableName;
        }

    }

以上代码中的GetTableName方法用于返回表在数据库中的名字,方法GetDayPartitionByDaySql用于生成分区用的SQL代码。
GetDayPartitionByDaySql方法中,最外层循环是用来控制年份的,当前代码从本年度开始,对表进行了2年的分区,如果需要更大年份跨度的分区,可修改i循环的最大值。当记录的日期超过最大设定的日期后,所有记录将保存在最后一个文件中,也就是pmax这个分区文件中。

方法AlterAuditLog主要用来执行分区的SQL语句。

完成TablePartitionBuilder类后,打开InitialHostDbBuilder.cs文件,并在Create方法中,调用SaveChanges方法前,添加以下代码执行TablePartitionBuilder类的Create方法:

new TablePartitionBuilder(_context).Create();

执行一次迁移

在VS中,打开程序包管理控制台(视图>其他窗口>程序包管理控制台)并将默认项目修改为EntityFrameworkCore项目,然后输入以下命令将移除之前的迁移代码:

Remove-Migration

在这里犯了一个错误,忘记要生成一次项目才能执行移除迁移的操作,因而会发生生成错误。比较麻烦的操作是用替换的方式,将产生错误的语句替换掉,图省事的方式就是直接删除全部迁移文件(Migrations文件夹下的文件)。

旧的迁移文件异常后,输入以下命令添加新的迁移:

Add-Migration Initial_Migrations

迁移文件创建后,在数据库中创建一个名为abpdevelopmentlogdb的数据库,记得将字符集设置为utf8mb4_general_ci。其实,执行迁移会自动创建数据库,不过怎么控制创建的数据库的字符集,这个还没研究过,所以这里选择了手动创建。
数据库创建后,重新生成一次Migrator项目,然后打开命令提示符窗口,切换到AbpDevelopmentLog.Migrator\bin\Debug\netcoreapp2.2文件夹执行以下命令执行迁移:

dotnet AbpDevelopmentLog.Migrator.dll

在出现以下提示的时候输入Y执行迁移:

2019-10-06 17:49:42 | Host database: server=localhost;database=AbpDevelopmentLogDb;uid=*****;pwd=*                               ult command timeout=3600
2019-10-06 17:49:42 | Continue to migration for this host database and all tenants..? (Y/N):

迁移执行完成后,就可在数据库目录下看到类似abpauditlogs#p#p20201227.ibd这样的分区文件了,说明表分区已经成功实现了。

为表名添加前缀

如果希望修改表名的前缀,可以在OnModelCreating方法中使用以下代码来实现:

            var prefix = "abcd";
            foreach (var entity in modelBuilder.Model.GetEntityTypes())
            {
                entity.Relational().TableName =
                    $"{prefix}_{entity.ClrType.Name}s";
            }

本文源代码:https://gitee.com/tianxiaode/AbpDevelopmentLog

你可能感兴趣的:(ASP/ASP.NET)