EFCore 通过实体Model生成创建SQL Server数据库表脚本

 更新时间:2021年3月3日 17:49  点击:1424

  在我们的项目中经常采用Model First这种方式先来设计数据库Model,然后通过Migration来生成数据库表结构,有些时候我们需要动态通过实体Model来创建数据库的表结构,特别是在创建像临时表这一类型的时候,我们直接通过代码来进行创建就可以了不用通过创建实体然后迁移这种方式来进行,其实原理也很简单就是通过遍历当前Model然后获取每一个属性并以此来生成部分创建脚本,然后将这些创建的脚本拼接成一个完整的脚本到数据库中去执行就可以了,只不过这里有一些需要注意的地方,下面我们来通过代码来一步步分析怎么进行这些代码规范编写以及需要注意些什么问题。

  一  代码分析

/// <summary>
    /// Model 生成数据库表脚本
    /// </summary>
    public class TableGenerator : ITableGenerator {
        private static Dictionary<Type, string> DataMapper {
            get {
                var dataMapper = new Dictionary<Type, string> {
                    {typeof(int), "NUMBER(10) NOT NULL"},
                    {typeof(int?), "NUMBER(10)"},
                    {typeof(string), "VARCHAR2({0} CHAR)"},
                    {typeof(bool), "NUMBER(1)"},
                    {typeof(DateTime), "DATE"},
                    {typeof(DateTime?), "DATE"},
                    {typeof(float), "FLOAT"},
                    {typeof(float?), "FLOAT"},
                    {typeof(decimal), "DECIMAL(16,4)"},
                    {typeof(decimal?), "DECIMAL(16,4)"},
                    {typeof(Guid), "CHAR(36)"},
                    {typeof(Guid?), "CHAR(36)"}
                };
 
                return dataMapper;
            }
        }
 
        private readonly List<KeyValuePair<string, PropertyInfo>> _fields = new List<KeyValuePair<string, PropertyInfo>>();
 
        /// <summary>
        ///
        /// </summary>
        private string _tableName;
 
        /// <summary>
        ///
        /// </summary>
        /// <returns></returns>
        private string GetTableName(MemberInfo entityType) {
            if (_tableName != null)
                return _tableName;
            var prefix = entityType.GetCustomAttribute<TempTableAttribute>() != null ? "#" : string.Empty;
            return _tableName = $"{prefix}{entityType.GetCustomAttribute<TableAttribute>()?.Name ?? entityType.Name}";
        }
 
        /// <summary>
        /// 生成创建表的脚本
        /// </summary>
        /// <returns></returns>
        public string GenerateTableScript(Type entityType) {
            if (entityType == null)
                throw new ArgumentNullException(nameof(entityType));
 
            GenerateFields(entityType);
 
            const int DefaultColumnLength = 500;
            var script = new StringBuilder();
 
            script.AppendLine($"CREATE TABLE {GetTableName(entityType)} (");
            foreach (var (propName, propertyInfo) in _fields) {
                if (!DataMapper.ContainsKey(propertyInfo.PropertyType))
                    throw new NotSupportedException($"尚不支持 {propertyInfo.PropertyType}, 请联系开发人员.");
                if (propertyInfo.PropertyType == typeof(string)) {
                    var maxLengthAttribute = propertyInfo.GetCustomAttribute<MaxLengthAttribute>();
                    script.Append($"\t {propName} {string.Format(DataMapper[propertyInfo.PropertyType], maxLengthAttribute?.Length ?? DefaultColumnLength)}");
                    if (propertyInfo.GetCustomAttribute<RequiredAttribute>() != null)
                        script.Append(" NOT NULL");
                    script.AppendLine(",");
                } else {
                    script.AppendLine($"\t {propName} {DataMapper[propertyInfo.PropertyType]},");
                }
            }
 
            script.Remove(script.Length - 1, 1);
 
            script.AppendLine(")");
 
            return script.ToString();
        }
 
        private void GenerateFields(Type entityType) {
            foreach (var p in entityType.GetProperties()) {
                if (p.GetCustomAttribute<NotMappedAttribute>() != null)
                    continue;
                var columnName = p.GetCustomAttribute<ColumnAttribute>()?.Name ?? p.Name;
                var field = new KeyValuePair<string, PropertyInfo>(columnName, p);
                _fields.Add(field);
            }
        }
    }

  这里的TableGenerator继承自接口ITableGenerator,在这个接口内部只定义了一个 string GenerateTableScript(Type entityType) 方法。

/// <summary>
    /// Model 生成数据库表脚本
    /// </summary>
    public interface ITableGenerator {
        /// <summary>
        /// 生成创建表的脚本
        /// </summary>
        /// <returns></returns>
        string GenerateTableScript(Type entityType);
    }

  这里我们来一步步分析这些部分的含义,这个里面DataMapper主要是用来定义一些C#基础数据类型和数据库生成脚本之间的映射关系。

     1  GetTableName

  接下来我们看看GetTableName这个函数,这里首先来当前Model是否定义了TempTableAttribute,这个看名字就清楚了就是用来定义当前Model是否是用来生成一张临时表的。

/// <summary>
    /// 是否临时表, 仅限 Dapper 生成 数据库表结构时使用
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public class TempTableAttribute : Attribute {
 
    }

  具体我们来看看怎样在实体Model中定义TempTableAttribute这个自定义属性。

[TempTable]
      class StringTable {
          public string DefaultString { get; set; }
          [MaxLength(30)]
          public string LengthString { get; set; }
          [Required]
          public string NotNullString { get; set; }
      }

  就像这样定义的话,我们就知道当前Model会生成一张SQL Server的临时表。

  当然如果是生成临时表,则会在生成的表名称前面加一个‘#'标志,在这段代码中我们还会去判断当前实体是否定义了TableAttribute,如果定义过就去取这个TableAttribute的名称,否则就去当前Model的名称,这里也举一个实例。

[Table("Test")]
       class IntTable {
           public int IntProperty { get; set; }
           public int? NullableIntProperty { get; set; }
       }

  这样我们通过代码创建的数据库名称就是Test啦。

     2  GenerateFields

  这个主要是用来一个个读取Model中的属性,并将每一个实体属性整理成一个KeyValuePair<string, PropertyInfo>的对象从而方便最后一步来生成整个表完整的脚本,这里也有些内容需要注意,如果当前属性定义了NotMappedAttribute标签,那么我们可以直接跳过当前属性,另外还需要注意的地方就是当前属性的名称首先看当前属性是否定义了ColumnAttribute的如果定义了,那么数据库中字段名称就取自ColumnAttribute定义的名称,否则才是取自当前属性的名称,通过这样一步操作我们就能够将所有的属性读取到一个自定义的数据结构List<KeyValuePair<string, PropertyInfo>>里面去了。

     3  GenerateTableScript

  有了前面的两步准备工作,后面就是进入到生成整个创建表脚本的部分了,其实这里也比较简单,就是通过循环来一个个生成每一个属性对应的脚本,然后通过StringBuilder来拼接到一起形成一个完整的整体。这里面有一点需要我们注意的地方就是当前字段是否可为空还取决于当前属性是否定义过RequiredAttribute标签,如果定义过那么就需要在创建的脚本后面添加Not Null,最后一个重点就是对于string类型的属性我们需要读取其定义的MaxLength属性从而确定数据库中的字段长度,如果没有定义则取默认长度500。

  当然一个完整的代码怎么能少得了单元测试呢?下面我们来看看单元测试。

  二  单元测试

public class SqlServerTableGenerator_Tests {
       [Table("Test")]
       class IntTable {
           public int IntProperty { get; set; }
           public int? NullableIntProperty { get; set; }
       }
 
       [Fact]
       public void GenerateTableScript_Int_Number10() {
           // Act
           var sql = new TableGenerator().GenerateTableScript(typeof(IntTable));
           // Assert
           sql.ShouldContain("IntProperty NUMBER(10) NOT NULL");
           sql.ShouldContain("NullableIntProperty NUMBER(10)");
       }
 
       [Fact]
       public void GenerateTableScript_TestTableName_Test() {
           // Act
           var sql = new TableGenerator().GenerateTableScript(typeof(IntTable));
           // Assert
           sql.ShouldContain("CREATE TABLE Test");
       }
 
       [TempTable]
       class StringTable {
           public string DefaultString { get; set; }
           [MaxLength(30)]
           public string LengthString { get; set; }
           [Required]
           public string NotNullString { get; set; }
       }
 
       [Fact]
       public void GenerateTableScript_TempTable_TableNameWithSharp() {
           // Act
           var sql = new TableGenerator().GenerateTableScript(typeof(StringTable));
           // Assert
           sql.ShouldContain("Create Table #StringTable");
       }
 
       [Fact]
       public void GenerateTableScript_String_Varchar() {
           // Act
           var sql = new TableGenerator().GenerateTableScript(typeof(StringTable));
           // Assert
           sql.ShouldContain("DefaultString VARCHAR2(500 CHAR)");
           sql.ShouldContain("LengthString VARCHAR2(30 CHAR)");
           sql.ShouldContain("NotNullString VARCHAR2(500 CHAR) NOT NULL");
       }
 
       class ColumnTable {
           [Column("Test")]
           public int IntProperty { get; set; }
           [NotMapped]
           public int Ingored {get; set; }
       }
 
       [Fact]
       public void GenerateTableScript_ColumnName_NewName() {
           // Act
           var sql = new TableGenerator().GenerateTableScript(typeof(ColumnTable));
           // Assert
           sql.ShouldContain("Test NUMBER(10) NOT NULL");
       }
 
       [Fact]
       public void GenerateTableScript_NotMapped_Ignore() {
           // Act
           var sql = new TableGenerator().GenerateTableScript(typeof(ColumnTable));
           // Assert
           sql.ShouldNotContain("Ingored NUMBER(10) NOT NULL");
       }
 
       class NotSupportedTable {
           public dynamic Ingored {get; set; }
       }
 
       [Fact]
       public void GenerateTableScript_NotSupported_ThrowException() {
           // Act
           Assert.Throws<NotSupportedException>(() => {
               new TableGenerator().GenerateTableScript(typeof(NotSupportedTable));
           });
       }
   }

  最后我们来看看最终生成的创建表的脚本。

          1  定义过TableAttribute的脚本。

CREATE TABLE Test (
     IntProperty NUMBER(10) NOT NULL,
     NullableIntProperty NUMBER(10),
)

          2  生成的临时表的脚本。

CREATE TABLE #StringTable (
     DefaultString VARCHAR2(500 CHAR),
     LengthString VARCHAR2(30 CHAR),
     NotNullString VARCHAR2(500 CHAR) NOT NULL,
)

  通过这种方式我们就能够在代码中去动态生成数据库表结构了。

以上就是EFCore 通过实体Model生成创建SQL Server数据库表脚本的详细内容,更多关于EFCore 创建SQL Server数据库表脚本的资料请关注猪先飞其它相关文章!

[!--infotagslink--]

相关文章

  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • MySQL性能监控软件Nagios的安装及配置教程

    这篇文章主要介绍了MySQL性能监控软件Nagios的安装及配置教程,这里以CentOS操作系统为环境进行演示,需要的朋友可以参考下...2015-12-14
  • PostgreSQL判断字符串是否包含目标字符串的多种方法

    这篇文章主要介绍了PostgreSQL判断字符串是否包含目标字符串的多种方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-02-23
  • PostgreSQL TIMESTAMP类型 时间戳操作

    这篇文章主要介绍了PostgreSQL TIMESTAMP类型 时间戳操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-12-26
  • 详解Mysql中的JSON系列操作函数

    新版 Mysql 中加入了对 JSON Document 的支持,可以创建 JSON 类型的字段,并有一套函数支持对JSON的查询、修改等操作,下面就实际体验一下...2016-08-23
  • Powershell实现编写和运行脚本

    本文为那些对学习 Windows PowerShell 命令行和脚本编写环境感兴趣的系统管理员提供了资源。也请告诉我们本网站如何才能对您更有用处。...2020-06-30
  • postgresql 实现多表关联删除

    这篇文章主要介绍了postgresql 实现多表关联删除操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-02
  • Postgresql 如何选择正确的关闭模式

    这篇文章主要介绍了Postgresl 如何选择正确的关闭模式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-18
  • 深入研究mysql中的varchar和limit(容易被忽略的知识)

    为什么标题要起这个名字呢?commen sence指的是那些大家都应该知道的事情,但往往大家又会会略这些东西,或者对这些东西一知半解,今天我总结下自己在mysql中遇到的一些commen sense类型的问题。 ...2015-03-15
  • postgresql数据添加两个字段联合唯一的操作

    这篇文章主要介绍了postgresql数据添加两个字段联合唯一的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-02-04
  • MySQL 字符串拆分操作(含分隔符的字符串截取)

    这篇文章主要介绍了MySQL 字符串拆分操作(含分隔符的字符串截取),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-02-22
  • Shell脚本中让进程休眠的方法(sleep用法)

    这篇文章主要介绍了Shell脚本中让进程休眠的方法,本文讲解的就是sleep的用法,可以实现睡觉若干秒、若干分钟、若干小时,需要的朋友可以参考下...2020-07-11
  • mysql的3种分表方案

    一、先说一下为什么要分表:当一张的数据达到几百万时,你查询一次所花的时间会变多,如果有联合查询的话,有可能会死在那儿了。分表的目的就在于此,减小数据库的负担,缩短查询时间。根据个人经验,mysql执行一个sql的过程如下:1...2014-05-31
  • Vscode上使用SQL的方法

    这篇文章主要介绍了Vscode上使用SQL的方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-01-26
  • Windows服务器MySQL中文乱码的解决方法

    我们自己鼓捣mysql时,总免不了会遇到这个问题:插入中文字符出现乱码,虽然这是运维先给配好的环境,但是在自己机子上玩的时候咧,总得知道个一二吧,不然以后如何优雅的吹牛B。...2015-03-15
  • SQL Server中row_number函数的常见用法示例详解

    这篇文章主要给大家介绍了关于SQL Server中row_number函数的常见用法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-12-08
  • PostgreSQL 字符串处理与日期处理操作

    这篇文章主要介绍了PostgreSQL 字符串处理与日期处理操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-02-01
  • postgresql重置序列起始值的操作

    这篇文章主要介绍了postgresql重置序列起始值,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-04
  • Centos5.5中安装Mysql5.5过程分享

    这几天在centos下装mysql,这里记录一下安装的过程,方便以后查阅Mysql5.5.37安装需要cmake,5.6版本开始都需要cmake来编译,5.5以后的版本应该也要装这个。安装cmake复制代码 代码如下: [root@local ~]# wget http://www.cm...2015-03-15
  • SQL Server中执行动态SQL

    本文详细讲解了SQLServer中执行动态SQL的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2022-05-19