在 ef-code-first 的情况下,插入事件时自动创建(重新创建)表






4.20/5 (4投票s)
实现一个“插入”存储库,可动态(重新)创建数据库表(如果不存在)。可以指定用于此的迁移,否则将使用默认创建逻辑。使用示例 - 动态日志记录和存档,或出于某些目的手动创建表。
问题
有时,我们有各种原因来(重新)创建特定的数据库表。让我们考虑一个非常常见的日志记录场景。很可能,所有项目都会编写某种日志,其中许多都存储在数据库中。在某个时刻,这些日志,即表,将增长到惊人的巨大尺寸。这是一个问题,因为数据库的大小也会相应增加,如果我们想,例如,查找某些特定记录,即使使用 WHERE
过滤器和实现的索引,查找过程也可能需要很长时间,并且此时数据库将承受重负载,这在使用 LIKE
语句进行查询时尤其如此。
在大多数情况下,我们不需要保留所有时间的所有日志。我们可以定期删除之前的行,此外,我们可以进行某种存档,这意味着选择旧行,例如,存档到 .zip 或 .rar 文件,然后从数据库中删除它们。这样,我们总是会有一个大小合理的日志表,并且所有先前的数据将存储在单独的地方,我们可以随时从中恢复它们。
但在这种情况下,有一些问题:SELECT
和 DELETE
操作,它们将影响大量行,而这些行总是处于繁忙的日志表中(因为它是实际的日志表,记录以很高的频率不断插入),将导致性能问题、数据库加载,并且它们会非常非常慢。
解决方案
您仍然可以实现存档方法,但另一种方式。与其从表中读取、存档和删除行,不如定期重命名该表,例如,每天午夜将表重命名为 MyTableName_ddMMyyyy 格式。 这样,每天您都会有一个新的、独立的存档表,它完全不受外部压力,并且是隔离的,然后您可以对它做任何您想做的事情:进一步存档或不受任何问题地保留它。
但是当我们重命名当前表时,EF 在尝试将下一条记录插入表中时会做什么?是的,会抛出错误,因为对象(表)不存在。本文致力于解释和展示如何通过 EF 环境动态(重新)创建不存在的表。
该解决方案以实现众所周知的存储库模式的形式呈现,您可以从中继承您自己的存储库类。
实现
该解决方案以 Nuget 包的形式呈现,您可以在此处找到:https://nuget.net.cn/packages/FenixRepo/
Install-Package FenixRepo
源代码在这里:https://github.com/SlavaUtesinov/FenixRepo。
我们有来自 `FenixRepo.Core`
命名空间中三个最重要的类,每个类都继承自前一个。它们是:FenixRepositoryScriptExtracto、FenixRepositoryCreateTable<T>
和 FenixRepository<T>。
让我们一个一个地看。
FenixRepositoryScriptExtractor
包含静态“全局”Initialize
方法,带有两个参数:contextFactory
- 用于创建上下文的操作,以及contextConfiguration
- 您的配置类的实例,该类通常位于 /Migrations 文件夹中。此方法应首先调用,在 ASP.NET MVC 的情况下,最合适的位置是 Global.asax。它为所有表(tableTypes
)和索引准备 SQL 脚本。
public static void Initialize<TDbContext>(Func<TDbContext> contextFactory, DbMigrationsConfiguration<TDbContext> contextConfiguration) where TDbContext : DbContext { Factory = contextFactory; Configuration = contextConfiguration; //Tables are DbSet<T> properties of Context class: var tableTypes = typeof(TDbContext).GetProperties().Where(x => x.PropertyType.FullName.Contains("System.Data.Entity.DbSet")).Select(x => x.PropertyType.GenericTypeArguments.First()).ToList(); var migrator = new DbMigrator(Configuration); var scriptor = new MigratorScriptingDecorator(migrator); var migrationScript = scriptor.ScriptUpdate("0", null); using (var context = contextFactory()) { var baseScripts = ((IObjectContextAdapter)context).ObjectContext.CreateDatabaseScript(); Scripts = tableTypes.ToDictionary(x => x, x => { var tableName = GetFullTableNameAndSchema(x, context); return new FenixScript { TableScript = ExtractTableScript(tableName, baseScripts), FkScripts = ExtractFkScripts(tableName, baseScripts), IndexScripts = ExtractIndexScripts(tableName, migrationScript) }; }); } }
FenixRepositoryScriptExtractor
使用 IObjectContextAdapter.ObjectContext.CreateDatabaseScript
方法获取所有表的 SQL 脚本。它使用方法:ExtractTableScript、ExtractFkScripts
来提取特定表的相应 SQL 部分。由于 CreateDatabaseScript
不提供索引的 SQL,我们将使用 MigratorScriptingDecorator.ScriptUpdate
方法(它返回所有迁移的 SQL 脚本,其中当然包括索引)和 ExtractIndexScripts。
然后该方法将这些对:TableType 和 SQL 脚本保存到 Scripts
字典中。
有一个重要的注意事项:数据库不能包含具有相同名称的约束(PK、FK)。因此,我们不能直接执行获取的脚本,而是必须始终使用 Guid.NewGuid()
为所有脚本分配新的唯一名称。
public string getBaseScript() { //new name for PK constraint var answer = Regex.Replace(TableScript, "(?i)primary key", m => $"constraint [{Guid.NewGuid()}] {m.Value}"); //new names for FKs if (FkScripts.Count > 0) answer += FkScripts.Select(x => Regex.Replace(x, @"(?i)(?<=add constraint\s+\[).+?\]", m => $"{Guid.NewGuid()}]")).Aggregate((a, b) => $"{a}\r\n{b}"); return answer; }
GetFullTableNameAndSchema
方法用于通过类型获取实际的表和架构名称(解决方案在此处找到:https://romiller.com/2014/04/08/ef6-1-mapping-between-types-tables/)。
FenixRepositoryCreateTable<T>
使用我刚刚解释的“默认”Scripts
或从迁移中创建选定的表和索引。
private void CreateTableInner(DbContext context) { var migrationScript = GetScriptFromMigrations(); //if class has FenixAttribute, we will use it if (migrationScript != null) context.Database.ExecuteSqlCommand(migrationScript); else { //if not, we will get scripts from "default" Scripts var info = Scripts[typeof(T)]; var toExclude = new List<string>(); context.Database.ExecuteSqlCommand(info.getBaseScript()); foreach (var index in info.IndexScripts) { try { context.Database.ExecuteSqlCommand(index); } catch (SqlException e) when ((index.ToLower().Contains("create") && e.Number == 3701) || (index.ToLower().Contains("drop") && e.Number == 1911)) { toExclude.Add(index); } } toExclude.ForEach(x => info.IndexScripts.Remove(x)); } }
如果您的表脚本位于多个迁移中,并且其中除了与该特定表相关的其他内容之外没有其他内容,您可以使用 FenixAttribute
以这种方式指定这些迁移。
[Fenix(nameof(FirstMigrationName), nameof(SecondMigrationName))] public class Order { //some stuff... }
在这种情况下,将使用这些迁移来创建表。它们的 SQL 脚本将这样获取:
private string GetScriptFromMigrations() { var type = typeof(T); var fenixAttr = type.CustomAttributes.Where(x => x.AttributeType == typeof(FenixAttribute)).FirstOrDefault(); if (fenixAttr != null) { var migrations = (fenixAttr.ConstructorArguments.First().Value as ReadOnlyCollection<CustomAttributeTypedArgument>).Select(x => x.Value.ToString()).ToArray(); var migrator = new DbMigrator(Configuration); var allMigrations = migrator.GetLocalMigrations().ToList(); var scriptor = new MigratorScriptingDecorator(migrator); string allMigrationScripts = null; foreach (var migration in migrations) { var target = allMigrations.Where(x => x.Contains(migration)).First(); var targetIndex = allMigrations.IndexOf(target); //source is previous migration var source = targetIndex == 0 ? "0" : Regex.Match(allMigrations.Where(x => allMigrations.IndexOf(x) == (targetIndex - 1)).First(), @"(?<=\d+_).+").Value; string script = scriptor.ScriptUpdate(source, target); allMigrationScripts += $"{ExtractScriptFromMigration(script, source)}{"\r\n"}"; } return allMigrationScripts.Trim(); } return null; }
并使用 FenixRepositoryScriptExtractor.ExtractScriptFromMigration
方法提取,然后将它们连接并执行。
FenixRepository<T>
主方法是BaseWrapper
,它是一种模板,用于将Add
和 AddRange
方法作为function
参数传递。
//where T is table type public T Add(T item) { return BaseWrapper(table => table.Add(item)); } public IEnumerable<T> AddRange(List<T> items) { return BaseWrapper(table => table.AddRange(items)); } TResult BaseWrapper<TResult>(Func<DbSet<T>, TResult> function) where TResult : class { using (var context = Factory()) { var table = context.Set<T>(); TResult answer = null; try { answer = function(context.Set<T>()); context.SaveChanges(); return answer; } catch (Exception e) when (tableNotExistsException(e)) { lock (typeof(T)) { try { context.SaveChanges(); return answer; } catch (Exception e2) when (tableNotExistsException(e2)) { CreateTable(context); } } return BaseWrapper(function); } } }
如果打算插入记录的尝试以“数据库中不存在表对象”之类的错误结束,它将获取一个锁,再尝试一次,如果结果相同,则调用CreateTable(context)
。
用法
首先,您应该调用Initialize
方法。
FenixRepositoryScriptExtractor.Initialize(() => new Context(), new Configuration());
就是这样,然后您就可以这样使用它。
var repository = new FenixRepository<Person>(); //Person table will be created, if it is needed repository.Add(person); //or repository.AddRange(people);
您也可以简单地创建表。
repository.CreateTable();
限制和注意事项
- 通过属性从迁移创建表始终比从“默认”
Scripts
更好,因为在最后一种情况下,不能保证所有索引都将被创建,而且迁移可能包含一些用于触发器、视图等的自定义代码;“默认”方法找不到它们。 - 目标表不能是任何其他表的主表,否则您应该自己解决这种情况。
- 方法:
Add
和AddRange
在表创建的上下文中是线程安全的。 - 该解决方案已在 MS SQL 场景下使用 EF 6.1.3 进行了测试。
结论
在本文中,我向您展示了如何从迁移代码或通过“默认”方法动态(重新)创建表。此解决方案可能很有用,例如,在日志记录和存档问题中,当日志的总大小非常大,并且您想快速操作它们并将它们分成一些逻辑部分,以便以后处理这些隔离的部分。您也可以简单地创建表,如果需要的话。