在我们的项目中经常采用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数据库表脚本的更多相关文章

  1. EF Core中,通过实体类向SQL Server数据库表中插入数据后,实体对象是如何得到数据库表中的默认值的

    我们使用EF Core的实体类向SQL Server数据库表中插入数据后,如果数据库表中有自增列或默认值列,那么EF Core的实体对象也会返回插入到数据库表中的默认值. 下面我们通过例子来展示,EF ...

  2. 修改SQL Server数据库表的创建时间最简单最直接有效的方法

    说明:这篇文章是几年前我发布在网易博客当中的原创文章,但由于网易博客现在要停止运营了,所以我就把这篇文章搬了过来,因为这种操作方式是通用的,即使是对现在最新的SQL Server数据库里面的操作也是一 ...

  3. 千万级SQL Server数据库表分区的实现

    千万级SQL Server数据库表分区的实现 2010-09-10 13:37 佚名 数据库 字号:T | T 一般在千万级的数据压力下,分区是一种比较好的提升性能方法.本文将介绍SQL Server ...

  4. SQL SERVER 数据库表同步复制 笔记

    SQL SERVER 数据库表同步复制 笔记 同步复制可运行在不同版本的SQL Server服务之间 环境模拟需要两台数据库192.168.1.1(发布),192.168.1.10(订阅) 1.在发布 ...

  5. SQL Server 数据库表的管理

    上一篇文章简单梳理了一下SQL Server数据库的安装和基本操作,这篇文章主要讲述一下数据库表的管理 一.数据库的创建 有关数据库的创建有两种方式,一种是通过视图创建,第二种就是通过T-SQL语句来 ...

  6. 将文件导入到SQL server数据库表中的字段中

    一.在要执行的sql server数据库a中执行如下脚本,创建存储过程sp_textcopy /* 将二进制文件导入.导出到数据库相应字段列中 */ CREATE PROCEDURE sp_textc ...

  7. 创建SQL Server数据库集群的经历

    自己尝试安装SQL Server集群和配置AlwaysOn可用性组,服务器系统是Windows Server 2012 R2,SQL Server是2014企业版,我的环境是一台服务器,然后用Hype ...

  8. SQL Server 数据库巡检脚本

    --1.查看数据库版本信息 select @@version --2.查看所有数据库名称及大小 exec sp_helpdb --3.查看数据库所在机器的操作系统参数 exec master..xp_ ...

  9. 获取sql server数据库表结构

    if exists (select 1 from sysobjects where name = 'sysproperties'and xtype = 'V')begin    DROP VIEW s ...

随机推荐

  1. 解释下Http协议

    HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统. HTTP协议的主要特点可概括如下: 1.支持客户/服务器模式. 2.简单快速:客户向服务器请求服务时,只 ...

  2. Java 基础:继承中的执行顺序

    1.单独的父类测试 Java中,new一个类的对象,类里面的静态代码块.非静态代码.无参构造方法.有参构造方法.类的一般方法等部分, 它们的执行顺序相对来说比较简单,用程序也很容易验证. 比如新建一个 ...

  3. 线程池(4)-参数-RejectedExecutionHandler

    1.介绍 当线程池线程数大于最大线程数(maximumPoolSize)时,多余的任务,程序应该按照什么拒绝策略处理. 2.拒绝策略4个 AbortPolicy:丢弃任务,并抛出RejectedExe ...

  4. LeetCode之打家劫舍

    1. 问题 在一条直线上,有n个房屋,每个房屋中有数量不等的财宝,有一个盗 贼希望从房屋中盗取财宝,由于房屋中有报警器,如果同时从相邻的两个房屋中盗取财宝就会触发报警器.问在不触发报警器的前提下,最多 ...

  5. OpenFOAM-双柱及群柱绕流

    这次的教程是紧接前几次的教程,设置与前几次教程类似,但是对于设置上稍微有一点点区别,就是在设置值的时候,出现了$internalField,其实这是一个字符串替换,就是在出现$internalFiel ...

  6. redis渐进式 rehash

    转载(http://redisbook.com/preview/dict/incremental_rehashing.html) 上一节说过, 扩展或收缩哈希表需要将 ht[0] 里面的所有键值对 r ...

  7. mysql8数据库连接kettle

    1.将kettle连接mysql的包放入lib文件目录 2.修改data-integration\simple-jndi路径下的jdbc.properties配置文件,加上如下内容(kettle为数据 ...

  8. OpenCV 3.4.2 环境搭建(适用于Ubuntu 一键安装)

    前面的话 最近决定要好好地学习一下OpenCV,Ubuntu系统上简单地搭建了OpenCV环境,(Windows的搭建方法移步到window10的搭建方法),千里之行始于足下,不积跬步无以至千里,在这 ...

  9. arcpy SearchCursor sql_clause

    import arcpy fc = 'c:/data/base.gdb/well' fields = ['WELL_ID', 'WELL_TYPE'] # Use ORDER BY sql claus ...

  10. Navicat Premium连接MySQL 1251错误和Mysql初始化root密码和允许远程访问

    Mysql初始化root密码和允许远程访问 在我们使用mysql数据库时,有时我们的程序与数据库不在同一机器上,这时我们需要远程访问数据库.缺省状态下,mysql的用户是没有远程访问的权限. 下面介绍 ...