EFCore实践&Bogus构造Mock数据

2020/10/15 DotNetCore 共 9621 字,约 28 分钟

EFCore快速创建本地LocalDB的增删改查。

首先,创建一个WebApplication项目基于.netcore的,其次在解决方案中创建一个.netLibrary项目,基于.netStandard标准的,这个Library项目中创建实体Models和DataAccess。Models文件夹中创建实体类。DataAccess中创建context类继承至DbContext,这个类里面DbSet到具体的实体类。context子类的options通过父类DbContext传递进来。

DbContext的配置项opitons参数通过WebApplication端Startup.cs中ConfigureServices配置,通过services.AddDbContext

Models

 public class Address
    {
        public int Id { get; set; }
        public string StreetAddress   { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string ZipCode { get; set; }


    }
  public class Email
    {
        public int Id { get; set; }
        public string EmailAddress { get; set; }
    }
public class Person 
    {
        public int Age { get; set; }
        public string FirstName { get; set; }
        public int Id { get; set; }
        public string LastName { get; set; }
        public List<Address> Addresses { get; set; }
        public List<Email> EmailAddresses { get; set; }
    }

DataAccess

 public class PeopleContext:DbContext
    {
        public PeopleContext(DbContextOptions options) : base(options){}
        public  DbSet<Person> People { get; set; }
        public DbSet<Address> Addresses { get; set; }
        public DbSet<Email> EmailAddresses { get; set; }

    }

Startup.cs ConfigureServices

AddDbContext,把DbContext通过依赖注入的方式引入到系统中,并且配置好options参数,设置好对SqlServer的配置。

 public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<PeopleContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("Default"));
            });
            services.AddRazorPages();
        }

addsettings.json

配置好本地数据库地址

  "ConnectionStrings": {
    "Default": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=EFDemoWeb;Integrated Security=True;"
  } 

DataMigration Stript

如果这个Package Manager Console中的字体太小,也是可以调整的:

Migration Tools

非常容易忘记安装Migration Tools的Nuget工具包,会导致如下错误:

定位到EFDataAccessLibray项目,使用Migration Tools,执行Add-Migration InitialDBCreation,得到一个Migration脚本。

在脚本中可以看到两个方法,一个是Up,一个是Down,Up是将要执行的语句,而Down是回滚语句。

数据库外键这里,FK_Addresses_People_PersonId意思是Addresses Link到People表(通过PersonId)。principal是主表, constraints表示含有外键的这个表。PersonId是这个表里面的外键。onDelete: ReferentialAction.Restrict这是一种强关联的模式,主表在删除的时候,如果关联了外键,那么就需要连同外键一并删除。

 constraints: table =>
                {
                    table.PrimaryKey("PK_Addresses", x => x.Id);
                    table.ForeignKey(
                        name: "FK_Addresses_People_PersonId",
                        column: x => x.PersonId,
                        principalTable: "People",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Restrict);
                });

Update-Database命令执行完成之后,数据库得到第一次初始化,这个时候数据库中出了实体表之外,还会有个Migration的记录表,里面记录了Migration迁移所使用到的迁移记录的MigrationId。

对字段的长度进行修改

如果是普通的类,没有对其字段的长度进行限制,那么code first模式生成出来的table里面的string字段就是max的。

我们可以分别对各个类的属性字段进行一定的限制。

修改字段类型

默认情况下string类型的字段会被自动创建为nvarchar(max)类型,如果要修改为varchar类型,使用Column Attribute进行标注。

 [Required]
        [MaxLength(10)]
        [Column(TypeName = "varchar(10)")]
        public string ZipCode { get; set; }

这个Column特性是位于命名空间: System.ComponentModel.DataAnnotations.Schema,使用codefrist模式相对而言在开发过程中是非常灵活方便的。如果是等到Application用户很多了,已经上了正式生产环境了,用户数量都已经非常多了,那个时候再来修改数据表字段类型就会很麻烦。

注意事项:修改字段长度的时候,有可能造成数据丢失。比如把FirstName长度为100的原来的表,修改为长度为50,如果原来的表中含有的数据中存在长度超过50的FirstName,当使用codefirst缩短为50的时候,原来数据库中的某些数据会被截断为50,造成数据丢失。

DbMigration注意事项

为了避免在Update-Database的时候发生各种异常情况:

Your startup project doesn't reference Microsoft.EntityFrameworkCore.Design

貌似是新EFcore的一个BUG,大多数情况下不会有这个问题,解决方案在中途添加其他项目的时候,偶尔会出现这个错误。网络上的解决办法各种各样的都有。

Microsoft.EntityFrameworkCore.Design和Microsoft.EntityFrameworkCore.Tools要一起安装。Startup项目webapplication也得安装。

Add-Migration InitalCreate

在进行Add-Migration InitialCreate的时候,会出现如下错误,需要把Web项目设置为首项目;容易忽略这一点。会导致出现如下错误。

image-20211018112641198

Mysql数据库驱动和微软的EF版本兼容问题

报错信息中提示Mysql的版本太低了,对微软提供的版本进行降级处理。

version_23424.png

常见的迁移

Package Manager Console里面执行迁移操作

 Add-Migration "AddTableDictCache"
 
 Update-Database

image-20231024014200758

生成Mock Data

这个工具只能是针对于轻量级的Api测试,生成使用Bogus,而如果是要构建几十万,上百万数据的测试数据,要使用SQL Data Generator这种工具:

RedGate这个系列工具是针对于Sqlserver的;行业中还有针对Mysql,Oracle类似的工具。

image-20230514190631918

默认创建的web application中index.cshtml.cs文件中注入PeopleContext。创建和生成大量的mock data数据对于开发阶段测试非常有帮助,能够帮助提升开发效率,比如某些情况下SQL去重的问题,数据库中如果只有5条测试数据,是很难发现SQL的重复数据问题,如果数据量有5万条,那么很方便的检测我们写的SQL是否有问题,常见的就是SQL忘记去重,导致上到正式环境,用户发现大量的重复数据,开发初期Bogus可以一键生成几万条数据,方便我们测试,并且这些测试数据的规则我们可以自定义,非常灵活。Bogus的免费版足够使用,付费版本开发效率更高。更多参考:Bogus Library for Fake Data In ASP.NET Core WebAPI

 private readonly ILogger<IndexModel> _logger;
        private readonly PeopleContext _db;

        public IndexModel(ILogger<IndexModel> logger,PeopleContext db)
        {
            _logger = logger;
            _db = db;
        }

web application的Startup.cs中配置好依赖注入项。

public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<PeopleContext>(options =>
            {
                options.UseSqlServer(Configuration.GetConnectionString("Default"));
            });
            services.AddRazorPages();
        }

1.Bogus生成Mock data

为了测试增删改查,我们需要构造一些假数据供自己测试。这里使用Bogus这个开源项目的Nuget包生成Mock Data。把生成出来的文件序列化为Json放到项目配置文件中,便于开发调试接口。Bogus的商业版本捐助地址,捐助开源可以获得商业版本功能,9.99美元/年,大概折合RMB:66元一年;

 Randomizer.Seed = new Random(9353526);

            var EmailGenerator = new Faker<Email>()
                .RuleFor(e => e.EmailAddress, f => f.Internet.Email());
            var AddressGenerator = new Faker<Address>()
                .RuleFor(a => a.City, f => f.Person.Address.City)
                .RuleFor(a => a.State, f => f.Person.Address.State)
                .RuleFor(a => a.StreetAddress, f => f.Person.Address.Street)
                .RuleFor(a => a.ZipCode, f => f.Person.Address.ZipCode);


            var PersonGenerator = new Faker<EFDataAccessLibrary.Person>()
                .RuleFor(p => p.Addresses, f => AddressGenerator.Generate(f.Random.Number(1, 5)).ToList())
                .RuleFor(p => p.Age, f => f.Random.Int(20, 72))
                .RuleFor(p => p.EmailAddresses, f => EmailGenerator.Generate(f.Random.Number(1, 3)).ToList())
                .RuleFor(p => p.FirstName, f => f.Person.FirstName)
                .RuleFor(p => p.LastName, f => f.Person.LastName);

           
            var data = PersonGenerator.Ignore(p=>p.Id).Generate(120);

            var text = JsonSerializer.Serialize(data);
            Console.WriteLine(text);

2.Mock Data入库

如果生成mock data的时候没有对自增Id设置规则,Id全部默认是零,Id=0。这样子方便进行批量替换,移除掉这些Id,交给数据库自己生成。

在razor page页面中的后台代码中,单独写一个方法,页面加载的时候mock data被写入数据库中。

  public void OnGet()
        {
            LoadSampleData();
        }

        private void LoadSampleData()
        {
            if (!_db.People.Any())
            {
                string file = System.IO.File.ReadAllText("generated.json");
                var people = JsonSerializer.Deserialize<List<Person>>(file);
                _db.AddRange(people);
                _db.SaveChanges();
            }
        }

这样子就非常轻松的一下子生成了几万条数据,并且模拟的都是类似真实的数据,大部分的数据都是通过mock数据源随机生成。默认情况下这个Bogus不支持中文,需要我们自己写Json格式的数据源来进行扩充,详细的扩充在本贴下文描述。

私人git仓库demo地址:https://gitee.com/caianhua/youtube-dot-net-core.git

3.Mock data支持中文

默认情况下Bogus是不支持中文字符串的,需要自己扩充,提供json文本,具体的扩展方法下面是官方提供的demo:

If you want to add your own lorem at runtime, you can try the following

 var myLorem = new Bogus.Bson.BObject

              {

                 {"words", new BArray{"猫", "狗"}}

              };

 var zhCN = Bogus.Database.GetLocale("zh_CN");

 zhCN.Add("lorem", myLorem);
 var faker = new Faker("zh_CN");

faker.Lorem.Word().Dump();


// OUTPUT:

猫

For future, if you have questions, please create a GitHub issue this way these answers can help other people too. If you'd like more code examples of how to "patch" a locale with extra data, you can find those in the following tests:

https://github.com/bchavez/Bogus/blob/b9049abf8b40203c09079741bcb328da95899f81/Source/Bogus.Tests/BsonTests.cs

4.Bogus Premium

如果是有Bogus的授权码,则可以安装:Bogus.Tools.Analyzer,Bogus.Location。其中Locations这个是支持gps地图坐标数据;Analyzer是自动辅助实现Bogus填充mock数据的代码。

Bogus_Premium_2837.png

5.mock数据源

https://github.com/wainshine/Chinese-Names-Corpus

6.构造关联数据

比如在构造数据的时候,经常需要Table和Table之间进行关联。left join 这种,就需要用到下面的类似的语法。

image-20220425143513287

image-20220425150125332

生成mock数据的时候,做了外键外联的属性之后,忽略掉这个属性不进行json输出,不进行database数据库映射处理,入库的时候就不会映射报错,经过测试这是可行的。

监听EFcore

使用sql server Management studio 连接localDb之后监听Efcore执行sql的过程。

开启sql server management studio,server name填入(LocalDB)\.,连接到local临时数据库。

通过SQL Server Management Studio监控到数据库中执行的sql,可以截获EF最终发往sql server的sql。

过滤出我们想要的监控数据,这里面显示的duration时间单位是微秒。 1微秒 μs =0.001 毫秒 ms

Efcore查询数据

关联查询

通过Efcore把我们构造的Mock data数据全部加载出来。通过sql server management studio 监控到的sql如下:

SELECT [p].[Id], [p].[Age], [p].[FirstName], [p].[LastName], 
[a].[Id], [a].[City],[a].[PersonId], [a].[State], [a].[StreetAddress], 
[a].[ZipCode], [e].[Id], [e].[EmailAddress], [e].[PersonId] 
FROM [People] AS [p]  
LEFT JOIN [Addresses] AS [a] ON [p].[Id] = [a].[PersonId]  
LEFT JOIN [EmailAddresses] AS [e] ON [p].[Id] = [e].[PersonId]  
ORDER BY [p].[Id], [a].[Id], [e].[Id]

上面这个语句,实际上查询出来了724行数据。模拟数据中Pepole只有120个,但是Efcore查出来的结果有724行,其中有很多行的“部分数据”出现了重复。如果有非常多的Table间的LEFT JOIN查询,实际上查出来的数据量是非常大的,对于性能是有很大的影响的。特别是同时有很多人同时请求数据库的时候,这个数据行数会成几何倍数的增长,严重时可能造成数据库响应缓慢超时。

如果不对导航属性进行关联查询,得到的数据量是120行,得到的查询语句如下:

  # Csharp代码
  var people = _db.People
                //.Include(a => a.Addresses)
                //.Include(e => e.EmailAddresses)
                .ToList();
  # EF CORE 产生的SQL
SELECT [p].[Id], [p].[Age], [p].[FirstName], [p].[LastName]  FROM [People] AS [p]

通过监控EF Core产生的SQL,可以让我们清楚的知道EF底层到底给我们生成了什么样的SQL,防止编写出性能低下的代码。

LINQ查询注意事项

下面是对于某一年龄段的人员数据进行查询,单独编写了一个C#方法,放到EFcore查询中。相比于Where(x=>x.Age>=18 && x.Age <=65)这种,C # 这种代码放到Linq中会导致运行时报错,提示C#代码无法翻译为sql.

 public void OnGet()
        {
            LoadSampleData();
            var people = _db.People
                .Include(a => a.Addresses)
                .Include(e => e.EmailAddresses)
                .Where(x=>ApprovedAge(x.Age))
               // .Where(x=>x.Age>=18 && x.Age <=65)
                .ToList();
        }

        private bool ApprovedAge(int age)
        {
            return (age >= 18 && age <= 65);
        }

正常情况下Where(x=>x.Age>=18 && x.Age <=65)这种,上面的Linq得到的SQL如下:

SELECT [p].[Id], [p].[Age], [p].[FirstName], [p].[LastName], [a].[Id], [a].[City], 
[a].[PersonId], [a].[State], [a].[StreetAddress], [a].[ZipCode], [e].[Id],
[e].[EmailAddress], [e].[PersonId]  
FROM [People] AS [p]  
LEFT JOIN [Addresses] AS [a] ON [p].[Id] = [a].[PersonId]  
LEFT JOIN [EmailAddresses] AS [e] ON [p].[Id] = [e].[PersonId]  
WHERE ([p].[Age] >= 18) AND ([p].[Age] <= 40)  ORDER BY [p].[Id], [a].[Id], [e].[Id]

关于EF Core

1.开发速度快。只需要少量的代码,就可以实现SQL一长串代码才能实现的功能,弊端是没有原生SQL性能高。如果想追求高性能,所有的查询都使用原生SQL,那么开发速度又会降低。

2.不必知道太多的SQL。这对于某些不太熟悉SQL的人而言,使用EF可能是一个福利。但是话有说回来,如果不太懂SQL的话,那就不会去用sql server management studio去监控EF生产的SQL代码从而进一步去优化SQL,也就无法写出高性能代码,无法对性能低下的代码做优化。根据监控到的sql 耗时来优化我们的查询。所以,使用EF的前提是必须要非常熟悉SQL才能更好驾驭EF。当应用程序的用户数量不多的时候,前期使用EF可能不会导致性能问题,但是随着应用程序的用户规模变得庞大,这个时候EF引发的性能问题就需要非常熟悉SQL优化的人才能够驾驭得了。

过度自信于上面2个‘好处’,会给平时的代码开发带来一定的风险,主要是性能上面的隐藏风险,而这些东西在项目上线的前期,用户数量少的时候是很难发现的。性能低,内存开销大的程序,会导致硬件服务器需要支付更多的内存,消耗更多的电力,给公司造成更高的成本。EF的坏处是,如果给到经验不足的人去驾驭EF,会引发潜在的性能问题,并且这种潜在的问题当被发现的时候已经太迟了。

关于Dapper

1.相比于Ef而言具备更高的生产运行性能。

2.对于熟悉SQL的开发人员更加友好。精确控制生产出来的SQL的同时,修改起来也非常快捷。

3.可以更好的解耦代码。如果是要在不同应用层之间传递数据,用Dapper只需要使用Class传递数据即可,而如果使用EF,在每个层上面都要依赖EF这个包,使得程序更加臃肿耦合度更高。

如果说要二选一的话,一般建议是EF Core和Dapper结合使用,但是作为比较熟悉SQL的人而言会更加倾向于Dapper。

文档信息

Search

    Table of Contents