hi,大家好,我是三合。作为一名程序猿,日常开发中,我们在接到需求以后,一般都会先构思一个模型,然后根据模型写实体类,写完实体类后在数据库里建表,接着进行增删改查, 也有第二种情况,就是有些人喜欢先在数据库里建表,然后再添加实体类。前者是code First,后者是db First,如果数据库表和c#实体类可以互相转换的话,那么无疑将大大加快我们的开发速度,很幸运的是,当前依靠一些第三方建模软件或者是efcode就可以实现,我们以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指令进行迁移。
通过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
在我使用这种方式的过程中,发现了ef core的几个缺点。
Script-Migration -From 20220610.cs -To :"202206101.cs"
,执行这命令还要找到2个变更点,这还是很麻烦。
基于以上痛点,在找不到合适组件的情况下,我在SummerBoot框架中定义了IDbGenerator接口,实现了四种数据库(sqlserver,mysql,oracle(仅支持12),sqlite)表和c#实体类的互相转换。具体数据库表字段类型和c#类型之间的映射关系,我则是参考了各个ef core驱动里的实现,确保和ef core生成的表或者实体类相一致。接下来介绍整个使用过程。
PM> Install-Package SummerBoot
services.AddSummerBoot(); services.AddSummerBootRepository(it => { //-----------以下为必填参数--------- //注册数据库类型,比如SqliteConnection,MySqlConnection,OracleConnection,SqlConnection it.DbConnectionType = typeof(MySqlConnection); //添加数据库连接字符串 it.ConnectionString = ""; });
实体类注解大部分来自于系统命名空间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; } }
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"); } }
这里以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部分,没有生成的非常完美,但是不影响使用。
那么生成的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部分,没有生成的非常完美,但是不影响使用。
把生成sql和执行sql分成2部分操作,对于日常而言是更方便的,我们可以快速拿到要执行的sql,进行检查,确认没问题后,可以保存下来,在正式发布应用时,留给dba审查。执行sql的代码如下
var generateSqls = dbGenerator.GenerateSql(new List<Type>() { typeof(Customer) }); foreach (var sqlResult in generateSqls) { dbGenerator.ExecuteGenerateSql(sqlResult); }
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 )
这里统一使用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`) )
参数为数据库表名的集合和生成的实体类的命名空间
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文档,也可以加入QQ群:799648362反馈建议。同时各位看官,如果你觉得这篇文章还不错的话,请记得一键三连哦(推荐+关注+github star)