net core天馬行空系列-可用於依賴注入的,資料庫表和c#實體類互相轉換的介面實現

2022-06-10 15:00:55

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)