65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (4投票s)

2017年4月24日

CPOL

4分钟阅读

viewsIcon

6171

实现一个“插入”存储库,可动态(重新)创建数据库表(如果不存在)。可以指定用于此的迁移,否则将使用默认创建逻辑。使用示例 - 动态日志记录和存档,或出于某些目的手动创建表。

问题

有时,我们有各种原因来(重新)创建特定的数据库表。让我们考虑一个非常常见的日志记录场景。很可能,所有项目都会编写某种日志,其中许多都存储在数据库中。在某个时刻,这些日志,即表,将增长到惊人的巨大尺寸。这是一个问题,因为数据库的大小也会相应增加,如果我们想,例如,查找某些特定记录,即使使用 WHERE 过滤器和实现的索引,查找过程也可能需要很长时间,并且此时数据库将承受重负载,这在使用 LIKE 语句进行查询时尤其如此。

在大多数情况下,我们不需要保留所有时间的所有日志。我们可以定期删除之前的行,此外,我们可以进行某种存档,这意味着选择旧行,例如,存档到 .zip 或 .rar 文件,然后从数据库中删除它们。这样,我们总是会有一个大小合理的日志表,并且所有先前的数据将存储在单独的地方,我们可以随时从中恢复它们。

但在这种情况下,有一些问题:SELECTDELETE 操作,它们将影响大量行,而这些行总是处于繁忙的日志表中(因为它是实际的日志表,记录以很高的频率不断插入),将导致性能问题、数据库加载,并且它们会非常非常慢

解决方案

您仍然可以实现存档方法,但另一种方式。与其从表中读取、存档和删除行,不如定期重命名该表,例如,每天午夜将表重命名为 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,它是一种模板,用于将AddAddRange 方法作为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();

限制和注意事项

  1. 通过属性从迁移创建表始终比从“默认”Scripts 更好,因为在最后一种情况下,不能保证所有索引都将被创建,而且迁移可能包含一些用于触发器、视图等的自定义代码;“默认”方法找不到它们。
  2. 目标表不能是任何其他表的主表,否则您应该自己解决这种情况。
  3. 方法:AddAddRange 在表创建的上下文中是线程安全的。
  4. 该解决方案已在 MS SQL 场景下使用 EF 6.1.3 进行了测试。

结论

在本文中,我向您展示了如何从迁移代码或通过“默认”方法动态(重新)创建表。此解决方案可能很有用,例如,在日志记录和存档问题中,当日志的总大小非常大,并且您想快速操作它们并将它们分成一些逻辑部分,以便以后处理这些隔离的部分。您也可以简单地创建表,如果需要的话。

© . All rights reserved.