在Abp中将数据库由SQL Server切换为MariaDB是相当简单的,但又有一些需要特别注意的问题,如MariaDB的索引字段,字段长度是有限制的,需要修改Abp框架内已定义好的索引字段的长度。本文是笔者在使用MariaDB数据库中的一些经验总结,希望对大家有所帮助。
数据库连接的定义在appsettings.json
文件中,需要修改的地方有两处,一个是Web.Host
项目,一个是Migrator
项目。项目Web.Host
是API服务器的启动项目,项目Migrator
是用于迁移数据库的迁移项目。
打开这两个项目的appsettings.json
文件,然后将ConnectionStrings
的Default
值修改为以下字符串就行了:
{
"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.EntityFrameworkCore
和Devart.Data.MySql.EFCore
只表明支持MySQL,不过感觉也能用。
对笔者来说,首选的当然是开源协议的数据提供者,因而Devart
的这个,基本就不考虑了。余下的就是在Oracle
和Pomelo
之间选择了。Oracle
使用的是GPL2协议,Pomelo
使用的是MIT协议,因而,在选择上,较为宽松的开源协议是首选,在这里MIT协议是比较宽松的开源协议。
至于性能上的区别,笔者没测试过,因为在笔者在使用MariaDB做开发的时候,Oracle还没开发数据提供者呢,当时Pomelo
是唯一的选择,因而,在未来的选择上,笔者也懒得去做测试对比了。使用Pomelo
在反馈和更新方面都会比Oracle
做得好。还有一个更深层的原因,以Oracle
哪个性格,哪天不鸟你也是可能,所以,我们的选择其实只有一个:Pomelo
。当然,对于不差钱的,Devart
可能是更好的选择。
选定了数据提供者,我们就可以在EntityFrameworkCore
项目上单击鼠标右键,打开NuGet管理器,为项目添加Pomelo.EntityFrameworkCore.MySql
程序包,并卸载Microsoft.EntityFrameworkCore.SqlServer
程序包。
打开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.Logging
的ILoggerFactory
类,不能是Castle.Core.Logging
的ILoggerFactory
类。
修改好以上两个方法后,打开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,然后将值传递给AbpDevelopmentLogDbContextConfigurer
的Configure
方法。
在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
修改表是实现不了的,因而,可行的办法是使用迁移程序创建数据库后,再把要分区的表删除,然后再创建。
在当前,我们就不考虑这么多,演示一下怎么对日志表分区就行了。要对日志表进行分区,并且使用每日一个文件的策略,首先要做的是将日志表的主键修改为Id
与ExecutionTime
的复合主键,而这需要使用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
方法将Id
与ExecutionTime
声明为了复合主键,为了保证主键的两个字段的值不会为空,需要使用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