.NET Core-可用于依赖注入的、数据库表和实体类互相转换的接口实现

一、前言

作为一名程序猿,日常开发中,我们在接到需求以后,一般都会先构思一个模型,然后根据模型写实体类,写完实体类后在数据库里建表,接着进行增删改查, 也有第二种情况,就是有些人喜欢先在数据库里建表,然后再添加实体类。.

前者是code First,后者是db First,如果数据库表和C#实体类可以互相转换的话,那么无疑将大大加快我们的开发速度,很幸运的是,当前依靠一些第三方建模软件或者是EF Core就可以实现,我们以EF Core举例,

1.1、ef core根据实体类生成数据库表

 //先定义一个我们自己的dbcontext
public class SqlServerDb : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var connectionString = MyConfiguration.GetConfiguration("sqlServerDbConnectionString");
        if (string.IsNullOrWhiteSpace(connectionString))
        {
            throw new ArgumentNullException("sqlServer connectionString must not be null");
        }

        optionsBuilder.UseSqlServer(connectionString);
    }

    public DbSet<Customer> Customer { get; set; }
}
//然后在代码里这样调用
using (var database = new SqlServerDb()) 
{
    database.Database.EnsureCreated();
}

那么efcore就会自动替我们生成一个数据库,库里面有一张表叫做Customer。整个过程无须我们手动干预。

接着如果实体类有变更的话,也可以通过add-migration指令进行迁移。

1.2、EF Core根据数据库表生成实体类

通过nuget安装了Microsoft.EntityFrameworkCore,Microsoft.EntityFrameworkCore.SqlServer,Microsoft.EntityFrameworkCore.Tools,Microsoft.VisualStudio.Web.CodeGeneration.Design这几个包之后,我们就可以在"程序包管理器控制台"中执行以下语句生成实体类:

Scaffold-DbContext "Data Source=192.168.12.34;Initial Catalog=数据库名称;User ID=登录名;Password=密码" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -Force

1.3、EF Core互相转换存在的一些缺陷

在我使用这种方式的过程中,发现了ef core的几个缺点。

1、当整个解决方案下,子项目比较多时,我要在程序包管理器控制台以及各个项目的切换上花费很多时间才能顺利执行命令,这就很麻烦。

2、我无法快速的获取某个实体类的建表语句(比如我希望只是生成sql,但不执行迁移),以及快速的从数据库表生成某个实体类(ef core可以自定义的根据表的集合生成指定的实体类,但是在项目中已存在该实体类的情况,要么直接生成失败,要么根据force条件直接覆盖项目中原有的实体类,这太粗暴了,有时候会将我手动修改过的实体类覆盖掉,我希望能自己控制这个生成的过程)。

3、多数据库的ef core驱动不支持实体类注释迁移到数据库表的注释,这么重要的功能不知道为啥不实现。

4、整个ef core自成一派,没有提供接口给到我们,这样就无法将数据库表和c#实体类互转的功能集成到我们自己的系统里。

5、要获取迁移生成的sql以供备份,还要执行命令

Script-Migration -From 20220610.cs -To :"202206101.cs" 

,执行这命令还要找到2个变更点,这还是很麻烦。

二、IDbGenerator横空出世

基于以上痛点,在找不到合适组件的情况下,我在SummerBoot框架中定义了IDbGenerator接口,实现了四种数据库(sqlserver,mysql,oracle(仅支持12),sqlite)表和C#实体类的互相转换。

具体数据库表字段类型和C#类型之间的映射关系,我则是参考了各个ef core驱动里的实现,确保和EF Core生成的表或者实体类相一致。

接下来介绍整个使用过程。

2.1、通过nuget包添加SummerBoot引用

PM> Install-Package SummerBoot

2.2、在startup.cs类中注册服务

services.AddSummerBoot();

services.AddSummerBootRepository(it =>
{
 //-----------以下为必填参数---------
 //注册数据库类型,比如SqliteConnection,MySqlConnection,OracleConnection,SqlConnection
 it.DbConnectionType = typeof(MySqlConnection);
 //添加数据库连接字符串
 it.ConnectionString = "";
});

 

2.3、根据实体类自动生成/修改数据库表

 

2.3.1、定义一个数据库实体类

实体类注解大部分来自于系统命名空间System.ComponentModel.DataAnnotations 和 System.ComponentModel.DataAnnotations.Schema,比如表名Table,列名Column,主键Key,主键自增DatabaseGenerated(DatabaseGeneratedOption.Identity),不映射该字段NotMapped,注释Description等,接下来以Customer为例

public class Customer
 {
    [Key,DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { set; get; }
    /// <summary>
    /// 姓名
    /// </summary>
    [Description("姓名")]
    public string Name { set; get; }
    /// <summary>
    /// 年龄
    /// </summary>
    [Description("年龄")]
    public int Age { set; get; } 

    /// <summary>
    /// 会员号
    /// </summary>
    [Description("会员号")]
    public string CustomerNo { set; get; }

    /// <summary>
    /// 总消费金额
    /// </summary>
    [Description("总消费金额")]
    public decimal TotalConsumptionAmount { set; get; }
}

2.3.2、注入IDbGenerator接口,调用GenerateSql方法生成建表或者修改表结构的sql

public class TestController : Controller
{
 private readonly IDbGenerator dbGenerator;

 public TestController(IDbGenerator dbGenerator)
 {
  this.dbGenerator = dbGenerator;
 }

 [HttpGet("GenerateSql")]
 public async Task<IActionResult> GenerateSql()
 {
  var generateSqls = dbGenerator.GenerateSql(new List<Type>() { typeof(Customer) });
  return Content("ok");
 }
}

2.3.3.1、如果数据库中不存在该表名的表

这里以mysql为例,生成的sql如下:

CREATE TABLE test.`Customer` (
    `Id` int NOT NULL AUTO_INCREMENT,
    `Name` text NULL ,
    `Age` int NOT NULL ,
    `CustomerNo` text NULL ,
    `TotalConsumptionAmount` decimal(18,2) NOT NULL ,
    PRIMARY KEY (`Id`)
)

ALTER TABLE test.`Customer` MODIFY `Name` text NULL  COMMENT '姓名'
ALTER TABLE test.`Customer` MODIFY `Age` int NOT NULL  COMMENT '年龄'
ALTER TABLE test.`Customer` MODIFY `CustomerNo` text NULL  COMMENT '会员号'
ALTER TABLE test.`Customer` MODIFY `TotalConsumptionAmount` decimal(18,2) NOT NULL  COMMENT '总消费金额'

虽然分成了2部分,没有生成的非常完美,但是不影响使用。

2.3.3.2 如果数据库中已存在该表名的表

那么生成的sql为,新增/更新字段的sql或者新增/更新注释的sql,为了避免数据丢失,不会有删除字段的sql,这里以Customer表举例,如果原本数据库里已经有了customer表,接着我们更新实体类,添加了一个地址的属性

public class Customer
{
    [Key,DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { set; get; }
    /// <summary>
    /// 姓名
    /// </summary>
    [Description("姓名")]
    public string Name { set; get; }
    /// <summary>
    /// 年龄
    /// </summary>
    [Description("年龄")]
    public int Age { set; get; } 

    /// <summary>
    /// 会员号
    /// </summary>
    [Description("会员号")]
    public string CustomerNo { set; get; }

    /// <summary>
    /// 总消费金额
    /// </summary>
    [Description("总消费金额")]
    public decimal TotalConsumptionAmount { set; get; }
    
    /// <summary>
    /// 地址
    /// </summary>
    [Description("地址")]
    public string Address { set; get; }
}

,那么此时生成的sql为

ALTER TABLE test.`Customer` ADD `Address` text NULL 
ALTER TABLE test.`Customer` MODIFY `Address` text NULL  COMMENT '地址'

虽然分成了2部分,没有生成的非常完美,但是不影响使用。

2.3.3.3 可以选择执行这些SQL

把生成sql和执行sql分成2部分操作,对于日常而言是更方便的,我们可以快速拿到要执行的sql,进行检查,确认没问题后,可以保存下来,在正式发布应用时,留给dba审查。执行sql的代码如下

var generateSqls = dbGenerator.GenerateSql(new List<Type>() { typeof(Customer) });
foreach (var sqlResult in generateSqls)
{
 dbGenerator.ExecuteGenerateSql(sqlResult);
}

2.3.4 表的命名空间

sqlserver里命名空间即schemas,oracle里命名空间即模式,sqlite和mysql里命名空间即数据库, 如果要定义不同命名空间下的表,可类似添加[Table("CustomerWithSchema", Schema = "test1")]注解即可。

[Table("CustomerWithSchema", Schema = "test1")]
public class CustomerWithSchema
{
 public string Name { set; get; }
        public int Age { set; get; } = 0;
        /// <summary>
        /// 会员号
        /// </summary>
        public string CustomerNo { set; get; }
        /// <summary>
        /// 总消费金额
        /// </summary>
        public decimal TotalConsumptionAmount { set; get; }
}

那么此时生成的sql为

CREATE TABLE test1.`CustomerWithSchema` (
    `Name` text NULL ,
    `Age` int NOT NULL ,
    `CustomerNo` text NULL ,
    `TotalConsumptionAmount` decimal(18,2) NOT NULL 
)

2.3.5 自定义实体类字段到数据库字段的类型映射或名称映射

这里统一使用column注解,如[Column("Age",TypeName = "float")]

public class Customer : BaseEntity
{
 public string Name { set; get; }
  
 [Column("Age",TypeName = "float")]
 public int Age { set; get; } = 0;
 /// <summary>
 /// 会员号
 /// </summary>
 public string CustomerNo { set; get; }
 /// <summary>
 /// 总消费金额
 /// </summary>
 public decimal TotalConsumptionAmount { set; get; }
}

生成的sql如下

CREATE TABLE `Customer2` (
    `Id` int NOT NULL AUTO_INCREMENT,
    `Name` text NULL ,
    `Age` float NOT NULL ,
    `CustomerNo` text NULL ,
    `TotalConsumptionAmount` decimal(18,2) NOT NULL ,
    PRIMARY KEY (`Id`)
)

2.4、根据数据库表自动生成实体类

2.4.1 注入IDbGenerator接口,调用GenerateCsharpClass方法生成C#类的文本

参数为数据库表名的集合和生成的实体类的命名空间

public class TestController : Controller
{
 private readonly IDbGenerator dbGenerator;

 public TestController(IDbGenerator dbGenerator)
 {
  this.dbGenerator = dbGenerator;
 }

 [HttpGet("GenerateClass")]
 public async Task<IActionResult> GenerateClass()
 {
  var generateClasses = dbGenerator.GenerateCsharpClass(new List<string>() { "Customer" },"Test.Model");
  return Content("ok");
 }
}

生成的C#实体类如下,新建一个类文件并把文本黏贴进去即可

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Test.Model
{
   [Table("Customer")]
   public class Customer
   {
      [Key]
      [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
      [Column("Id")]
      public int Id { get; set; }
      /// <summary>
      ///姓名
      /// </summary>
      [Column("Name")]
      public string Name { get; set; }
      /// <summary>
      ///年龄
      /// </summary>
      [Column("Age")]
      public int Age { get; set; }
      /// <summary>
      ///会员号
      /// </summary>
      [Column("CustomerNo")]
      public string CustomerNo { get; set; }
      /// <summary>
      ///总消费金额
      /// </summary>
      [Column("TotalConsumptionAmount")]
      public decimal TotalConsumptionAmount { get; set; }
      /// <summary>
      ///地址
      /// </summary>
      [Column("Address")]
      public string Address { get; set; }
   }
}

三、与仓储接口配合使用

首先定义仓储接口,接口的具体实现类会由SummerBoot框架自动生成

[AutoRepository]
public interface ICustomerRepository:IBaseRepository<Customer>
{
}

接着就可以直接注入使用,整个增删改查操作(支持同步异步)如下所示

[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
  private readonly ICustomerRepository customerRepository;
  private readonly IDbGenerator dbGenerator;

  public HomeController(ICustomerRepository customerRepository, IDbGenerator dbGenerator)
  {
      this.customerRepository = customerRepository;
      this.dbGenerator = dbGenerator;
  }

  [HttpGet("test")]
  public IActionResult Test()
  {
     var results= dbGenerator.GenerateSql(new List<Type>() { typeof(Customer) });
     var generateClasses = dbGenerator.GenerateCsharpClass(new List<string>() { "Customer" }, "Test.Model");
     //执行ddl操作
     foreach (var databaseSqlResult in results)
     {
        dbGenerator.ExecuteGenerateSql(databaseSqlResult);
     }

     var cusotmer = new Customer()
     {
          Name = "三合",
          Age = 3,
          CustomerNo = "00001",
          Address = "福建省",
          TotalConsumptionAmount = 999
     };
     //增
     customerRepository.Insert(cusotmer);
     //改
     cusotmer.Age = 5;
     customerRepository.Update(cusotmer);
     //也可以这样改
     customerRepository.Where(it => it.Name == "三合").SetValue(it => it.Age, 6).ExecuteUpdate();
     //查
     var dbCustomer= customerRepository.FirstOrDefault(it => it.Name == "三合");
     //删
     customerRepository.Delete(dbCustomer);
     //也可以这样删
     customerRepository.Delete(it=>it.Name== "三合");

     return Content("ok");
   }
}

自动生成数据库表与仓储接口配合使用,就会使我们的整个开发过程顺畅无比,犹如行云流水。

四、结尾

SummerBoot文档:https://github.com/TripleView/SummerBoot

更多用法可参考,如果你觉得这篇文章还不错的话,记得推荐。