天天看點

net core天馬行空系列-可用于依賴注入的,資料庫表和c#實體類互相轉換的接口實作

1.前言

hi,大家好,我是三合。作為一名程式猿,日常開發中,我們在接到需求以後,一般都會先構思一個模型,然後根據模型寫實體類,寫完實體類後在資料庫裡建表,接着進行增删改查, 也有第二種情況,就是有些人喜歡先在資料庫裡建表,然後再添加實體類。前者是code First,後者是db First,如果資料庫表和c#實體類可以互相轉換的話,那麼無疑将大大加快我們的開發速度,很幸運的是,目前依靠一些第三方模組化軟體或者是efcode就可以實作,我們以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. 很多資料庫的efcore驅動不支援實體類注釋遷移到資料庫表的注釋,這麼重要的功能不知道為啥不實作。
  4. 整個ef core自成一派,沒有提供接口給到我們,這樣就無法将資料庫表和c#實體類互轉的功能內建到我們自己的系統裡。
  5. 要擷取遷移生成的sql以供備份,還要執行指令
Script-Migration -From 20220610.cs -To :"202206101.cs" 
           

,執行這指令還要找到2個變更點,這還是很麻煩。

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; }
   }
}
           

3.與倉儲接口配合使用

首先定義倉儲接口,接口的具體實作類會由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");
   }
}
           

自動生成資料庫表與倉儲接口配合使用,就會使我們的整個開發過程順暢無比,猶如行雲流水。

4.結尾

更多用法,可參考SummerBoot文檔,也可以加入QQ群:799648362回報建議。同時各位看官,如果你覺得這篇文章還不錯的話,請記得一鍵三連哦(推薦+關注+github star)