乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 数据持久化设计,基于Entity Framework Core和其广泛的数据库提供程序

前言

Entity Framework(EF)Core是轻量化、可扩展、开源和跨平台版的常用Entity Framework数据访问技术

image

EF Core可用作对象关系映射程序(O/RM),这可以实现以下两点:

  • 使.NET开发人员能够使用.NET对象处理数据库。
  • 无需再像通常那样编写大部分数据访问代码。

作为《乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 适用于Entity Framework Core的命令行(CLI)工具集(Dotnet-EF)》以及《乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 浅析ASP.NET Core领域驱动设计,通过MediatR中介者模式实现CQRS和领域事件》的姊妹篇,这里将梳理在Entity Framework Core的广泛数据库提供程序支持下,如何实现数据库的Docker创建和简单对接。

https://github.com/TaylorShi/HelloEfCoreProvider

常见数据库提供程序

Entity Framework Core可通过名为数据库提供程序的插件库访问许多不同的数据库。

数据库系统 配置示例 NuGet 程序包
SQL Server 或 Azure SQL .UseSqlServer(connectionString) Microsoft.EntityFrameworkCore.SqlServer
Azure Cosmos DB .UseCosmos(connectionString, databaseName) Microsoft.EntityFrameworkCore.Cosmos
SQLite .UseSqlite(connectionString) Microsoft.EntityFrameworkCore.Sqlite
EF Core 内存中数据库 .UseInMemoryDatabase(databaseName) Microsoft.EntityFrameworkCore.InMemory
PostgreSQL* .UseNpgsql(connectionString) Npgsql.EntityFrameworkCore.PostgreSQL
MySQL/MariaDB* .UseMySql(connectionString) Pomelo.EntityFrameworkCore.MySql
Oracle* .UseOracle(connectionString) Oracle.EntityFrameworkCore
MySQL .UseMySQL(connectionString) MySql.EntityFrameworkCore

Docker创建数据库实例

通过Docker准备PostgreSQL实例

PostgreSQL,通常简称为"Postgres",是一个对象关系型数据库管理系统(ORDBMS),强调可扩展性和标准符合性。作为一个数据库服务器,它的主要功能是安全地存储数据,并支持最佳实践,随后根据其他软件应用程序的要求进行检索,无论是同一台计算机上的软件还是在网络上的另一台计算机上运行的软件(包括互联网)。它可以处理从小型单机应用到有许多并发用户的大型面向互联网的应用的工作负荷。最近的版本还提供数据库本身的复制,以保证安全和可扩展性。

PostgreSQL实现了SQL:2011标准的大部分内容,符合ACID标准和事务性(包括大多数DDL语句),使用多版本并发控制(MVCC)避免了锁定问题,提供了对脏读和完全序列化的免疫力;使用许多其他数据库所没有的索引方法处理复杂的SQL查询;具有可更新视图和物化视图、触发器、外键;支持函数和存储过程以及其他可扩展性,并有大量由第三方编写的扩展。除了可以与主要的专有和开源数据库一起工作外,PostgreSQL还通过其广泛的标准SQL支持和可用的迁移工具,支持从这些数据库迁移。如果使用了专有的扩展,通过它的可扩展性,可以通过一些内置的和第三方的开放源码的兼容性扩展来模拟许多扩展,例如对Oracle的扩展。

image

准备一个PostgreSQL的Docker实例

https://hub.docker.com/_/postgres

docker run -d --name postgres --restart unless-stopped -p 5432:5432 -e "POSTGRES_USER=postgres" -e "POSTGRES_PASSWORD=xxxxxxxxxxxxxx" postgres:14.5

image

image

image

默认的用户名是postgres,默认端口是5432

docker exec -it postgres /bin/bash

image

通过Docker准备MSSQL实例

准备一个MSSQL(Microsoft SQL Server)的Docker实例

https://hub.docker.com/_/microsoft-mssql-server

docker run -d --name mssql --restart unless-stopped -p 1433:1433 -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=xxxxxxxxxxxxxxxx" mcr.microsoft.com/mssql/server:2022-latest

image

如果想要运行的是SQL Express版本,还可以追加参数MSSQL_PID

-e "MSSQL_PID=Express"

MSSQL_PID其实也是用来控制安装版本的,也和授权有关系

  • Developer(默认值)
  • Express
  • Standard
  • Enterprise
  • EnterpriseCore

连接的账号名是SA,密码是自己设置这个,端口是1433

如果忘记密码,进入容器实例之后,可以查看

docker exec -it mssql /bin/bash
ps -eax

image

通过Docker准备MYSQL实例

准备一个MYSQL的Docker实例

https://hub.docker.com/_/mysql

docker run -d --name mysql --restart unless-stopped -p 3306:3306 -e MYSQL_ROOT_PASSWORD=xxxxxxxxxxxxxxxx mysql:5.7.40

image

默认用户是root,默认端口是3306

通过Docker准备Oracle实例

准备一个Oracle 11g的Docker实例

docker run -d --name oracle --restart unless-stopped -p 1521:1521 registry.cn-hangzhou.aliyuncs.com/helowin/oracle_11g

默认用户是root,默认密码是helowin,默认端口1521

对接示例

建立示例领域和上下文

领域模型

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public string Title { get; set; }

    public string Name { get; set; }

    public List<Post> Posts { get; } = new List<Post>();
}
public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

上下文

public class BloggingContext : DbContext
{
    public BloggingContext(DbContextOptions<BloggingContext> options)
    : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

建立示例项目(SQLite)

依赖包

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Sqlite

dotnet add package Microsoft.EntityFrameworkCore.Sqlite

如果是Net Core 3.1项目,最新的版本无法兼容,可以追加版本号参数--version 5.0.17

建立示例DbContext

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    public string DbPath { get; }

    public BloggingContext()
    {
        var folder = Environment.SpecialFolder.LocalApplicationData;
        var path = Environment.GetFolderPath(folder);
        DbPath = System.IO.Path.Join(path, "blogging.db");
    }

    // The following configures EF to create a Sqlite database file in the
    // special "local" folder for your platform.
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite($"Data Source={DbPath}");
}

使用它

using (var db = new BloggingContext())
{
    Console.WriteLine("Ensure Database Created");

    db.Database.EnsureCreated();

    Console.WriteLine($"DbPath:{db.DbPath}");

    Console.WriteLine("Inserting a new blog");
    var blog = new Blog
    {
        BlogId = 16839191,
        Url = "https://www.cnblogs.com/taylorshi/p/16839191.html"
    };
    db.Add(blog);
    db.SaveChanges();
}
Console.WriteLine("Hello World!");

前面使用的是在DbContext内部去定义位置和连接字符串,实际上,可以从外面传进去。

public class PosttingContext : DbContext
{
    public PosttingContext(DbContextOptions<PosttingContext> options)
            : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

这里需要构建一个公开的构造函数,通过这个入口就能将上下文配置从外部传进来。

我们试着修改下使用方式

static void Main(string[] args)
{
    var folder = Environment.SpecialFolder.MyDocuments;
    var path = Environment.GetFolderPath(folder);
    var DbPath = System.IO.Path.Join(path, "EFSqliteConsole.db");

    var services = new ServiceCollection();
    services.AddDbContext<BloggingContext>(opt => opt.UseSqlite($"Data Source={DbPath}"));

    using (var scope = services.BuildServiceProvider().CreateScope())
    {
        var context = scope.ServiceProvider.GetService<BloggingContext>();
        //context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        var blog = new Blog
        {
            BlogId = new Random(16839191).Next(),
            Url = "https://www.cnblogs.com/taylorshi/p/16843914.html"
        };
        context.Add(blog);
        context.SaveChanges();

        var blogs = context.Blogs.ToList();
        if (blogs.Any())
        {

        }
    }

    Console.ReadKey();
}

image

建立示例项目(SQLServer)

依赖包

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

如果是Net Core 3.1项目,最新的版本无法兼容,可以追加版本号参数--version 5.0.17

static void Main(string[] args)
{
    var connectionString = "Server=tcp:localhost,1433;Database=TeslaOrder.EFSqlServerConsole;User Id=sa;Password=beE#Yahlj!Sdgj6x;";
    var services = new ServiceCollection();
    services.AddDbContext<BloggingContext>(opt => opt.UseSqlServer(connectionString));

    using (var scope = services.BuildServiceProvider().CreateScope())
    {
        var context = scope.ServiceProvider.GetService<BloggingContext>();
        //context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        var blog = new Blog
        {
            Url = "https://www.cnblogs.com/taylorshi/p/16843914.html"
        };
        context.Add(blog);
        context.SaveChanges();

        var blogs = context.Blogs.ToList();
        if (blogs.Any())
        {

        }
    }

    Console.ReadKey();
}

image

建立示例项目(InMemory)

依赖包

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.InMemory

dotnet add package Microsoft.EntityFrameworkCore.InMemory

如果是Net Core 3.1项目,最新的版本无法兼容,可以追加版本号参数--version 5.0.17

static void Main(string[] args)
{
    var databaseName = "EFInMeoryConsole";
    var services = new ServiceCollection();
    services.AddDbContext<BloggingContext>(opt => opt.UseInMemoryDatabase(databaseName));

    using (var scope = services.BuildServiceProvider().CreateScope())
    {
        var context = scope.ServiceProvider.GetService<BloggingContext>();
        //context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        var blog = new Blog
        {
            Url = "https://www.cnblogs.com/taylorshi/p/16843914.html"
        };
        context.Add(blog);
        context.SaveChanges();

        var blogs = context.Blogs.ToList();
        if (blogs.Any())
        {

        }
    }

    Console.ReadKey();
}

建立示例项目(Azure Cosmos DB)

依赖包

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Cosmos

dotnet add package Microsoft.EntityFrameworkCore.Cosmos

如果是Net Core 3.1项目,最新的版本无法兼容,可以追加版本号参数--version 5.0.17

static void Main(string[] args)
{
    var connectionString = "AccountEndpoint=https://xxxxxxx.documents.azure.com:443/;AccountKey=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxlUFYU9lbHgWw3FTLNzQB1IDm1DZ5VGHZQwACDbS4IgGA==;";
    var databaseName = "EFCosmosConsole";
    var services = new ServiceCollection();
    services.AddDbContext<BloggingContext>(opt => opt.UseCosmos(connectionString, databaseName));

    using (var scope = services.BuildServiceProvider().CreateScope())
    {
        var context = scope.ServiceProvider.GetService<BloggingContext>();
        //context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        var blog = new Blog
        {
            BlogId = new Random(99999).Next(),
            Url = "https://www.cnblogs.com/taylorshi/p/16843914.html"
        };
        context.Add(blog);
        context.SaveChanges();

        var blogs = context.Blogs.ToList();
        if (blogs.Any())
        {

        }
    }

    Console.ReadKey();
}

image

建立示例项目(PostgreSQL)

依赖包

https://www.nuget.org/packages/Npgsql.EntityFrameworkCore.PostgreSQL

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

如果是Net Core 3.1项目,最新的版本无法兼容,可以追加版本号参数--version 5.0.10

static void Main(string[] args)
{
    var connectionString = "Host=localhost;Port=5432;Database=EFPostgreSQLConsole;Username=postgres;Password=xxxxxxxxxxxxxxxxxxxxx;Pooling=true;";
    var services = new ServiceCollection();
    services.AddDbContext<BloggingContext>(opt => opt.UseNpgsql(connectionString));

    using (var scope = services.BuildServiceProvider().CreateScope())
    {
        var context = scope.ServiceProvider.GetService<BloggingContext>();
        //context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        var blog = new Blog
        {
            Url = "https://www.cnblogs.com/taylorshi/p/16843914.html"
        };
        context.Add(blog);
        context.SaveChanges();

        var blogs = context.Blogs.ToList();
        if (blogs.Any())
        {

        }
    }

    Console.ReadKey();
}

image

建立示例项目(MySQL)

依赖包

https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql

dotnet add package Pomelo.EntityFrameworkCore.MySql

如果是Net Core 3.1项目,最新的版本无法兼容,可以追加版本号参数--version 5.0.4

查看MYSQL版本,构建MySqlServerVersion对象,如果是

select @@version as version;

image

static void Main(string[] args)
{
    var connectionString = "server=localhost;port=16000;user=root;password=xxxxxxxxxxxxxx;database=EFMySQLPomeloConsole;charset=utf8mb4;ConnectionReset=false;Min Pool Size=10;Max Pool Size=200;";
    var serverVersion = new MySqlServerVersion(new Version(5, 7, 40));

    var services = new ServiceCollection();
    services.AddDbContext<BloggingContext>(opt => opt.UseMySql(connectionString, serverVersion));

    using (var scope = services.BuildServiceProvider().CreateScope())
    {
        var context = scope.ServiceProvider.GetService<BloggingContext>();
        //context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

        var blog = new Blog
        {
            Url = "https://www.cnblogs.com/taylorshi/p/16843914.html"
        };
        context.Add(blog);
        context.SaveChanges();

        var blogs = context.Blogs.ToList();
        if (blogs.Any())
        {

        }
    }

    Console.ReadKey();
}

image

这里还可以配置重试机制

services.AddDbContext<BloggingContext>(opt =>
{
    opt.UseMySql
    (
        connectionString,
        serverVersion,
        options => options.EnableRetryOnFailure
        (
            maxRetryCount: 3,
            maxRetryDelay: System.TimeSpan.FromSeconds(10),
            errorNumbersToAdd: new List<int> { 0 }
        )
    );
});

https://dev.mysql.com/doc/mysql-errors/5.7/en/server-error-reference.html

EnableRetryOnFailure方法中errorNumbersToAdd参数是用来设置错误代码的,只有设置了错误代码的错误,才会触发重试。获取错误代码的方法有很多种,通过异常信息进行获取,比如,使用MySql数据时,触发的异常类型是MySqlException,此类的Number属性的值EnableRetryOnFailure方法所需要的Number

简单数据库日志记录

默认情况下Entity Framework Core都可以和Microsoft.Extensions.Logging很好的配合,只需要在平时我们配置数据库的后面追加相关策略即可。

根据日志级别输出到控制台

services.AddDbContext<BloggingContext>(opt =>
    opt.UseMySql(connectionString, serverVersion)
    // 日志输出到控制台
    .LogTo(Console.WriteLine, LogLevel.Information)
);

根据日志类别来输出到控制台

services.AddDbContext<BloggingContext>(opt =>
    opt.UseMySql(connectionString, serverVersion)
    // 日志输出到控制台
    .LogTo(Console.WriteLine, new[] { DbLoggerCategory.Database.Name })
);

系统会将每条日志消息分配到一个已命名的分层记录器类别,这些类别包括

类别 消息
Microsoft.EntityFrameworkCore 所有 EF Core 消息
Microsoft.EntityFrameworkCore.Database 所有数据库交互
Microsoft.EntityFrameworkCore.Database.Connection 使用数据库连接
Microsoft.EntityFrameworkCore.Database.Command 使用数据库命令
Microsoft.EntityFrameworkCore.Database.Transaction 使用数据库事务
Microsoft.EntityFrameworkCore.Update 正在保存实体,不包括数据库交互
Microsoft.EntityFrameworkCore.Model 所有模型和元数据交互
Microsoft.EntityFrameworkCore.Model.Validation 模型验证
Microsoft.EntityFrameworkCore.Query 查询,不包括数据库交互
Microsoft.EntityFrameworkCore.Infrastructure 常规事件,例如上下文创建
Microsoft.EntityFrameworkCore.Scaffolding 数据库反向工程
Microsoft.EntityFrameworkCore.Migrations 迁移
Microsoft.EntityFrameworkCore.ChangeTracking 更改跟踪交互

日志输出记录敏感数据

services.AddDbContext<BloggingContext>(opt =>
    opt.UseMySql(connectionString, serverVersion)
    // 日志输出记录敏感数据
    .EnableSensitiveDataLogging()
);

日志输出记录详细异常

services.AddDbContext<BloggingContext>(opt =>
    opt.UseMySql(connectionString, serverVersion)
    // 日志输出记录详细异常
    .EnableDetailedErrors()
);

启用上下文池提高吞吐

DbContext对用于和数据库打交道的上下文,创建和释放它在高性能场景下仍然存在可优化空间,可通过AddDbContextPool来替代AddDbContext以便启用上下文池。

services.AddDbContextPool<BloggingContext>(opt => opt.UseMySql(connectionString, serverVersion));

需要注意的是,AddDbContextPool的最大保留实例数(>= EFCore 6.0 默认值为1024,< EFCore 6.0 默认值为128),一旦超过这个保留数,将不再缓存新的上下文实例,会恢复到非池模式进行创建。

但是如果上下文池的连接数超过了数据库连接池的连接数时,就可能引发数据库连接池连接数超限的问题,为了避免这个问题,要么我们将上下文池的默认值改小,要么在数据库连接字符串那里把数据库连接池的值改大。

对MYSQL和MSServer而言,数据库连接池最小值默认是0,最大值默认是100

image

image

var connectionString = "........;Min Pool Size=10;Max Pool Size=200;";

查看MYSQL的查询连接数

show processlist

image

使用加密Sqlite+EfCore

Sqlite的免费版默认是不支持加密的.

已知的Sqlite加密工具有

SQLCipher是一个开源的,基于免费版SQLite的加密数据库。它采用256-bit AES进行加密,主要的接口和SQLite相同,另外增加了一些加解密相关的接口。

依赖包

https://www.nuget.org/packages/Microsoft.Data.Sqlite.Core

https://www.nuget.org/packages/SQLitePCLRaw.bundle_e_sqlcipher

dotnet add package Microsoft.Data.Sqlite.Core --version 5.0.17
dotnet add package SQLitePCLRaw.bundle_e_sqlcipher --version 2.1.2

使用SqliteConnectionStringBuilder来创建带密码的SQLite数据库

internal class Program
{
    static void Main(string[] args)
    {
        var folder = Environment.SpecialFolder.MyDocuments;
        var path = Environment.GetFolderPath(folder);
        var dbPath = System.IO.Path.Join(path, "postting.db");
        var baseConnectionString = $"Data Source={dbPath}";

        var oldPassword = "xxxxxxxxxxxxxxxx";
        var connectionString = new SqliteConnectionStringBuilder(baseConnectionString)
        {
            Mode = SqliteOpenMode.ReadWriteCreate,
            Password = oldPassword
        }.ToString();

        // 设置密码
        using (SqliteConnection connection = new SqliteConnection(connectionString))
        {
            connection.Open();

            using (var cmd = connection.CreateCommand())
            {
                cmd.CommandText = @"CREATE TABLE Users (
                    ID INTEGER PRIMARY KEY AUTOINCREMENT
                );";
                cmd.ExecuteNonQuery();
            }
        }
        Console.ReadKey();
    }
}

这里使用SqliteConnectionStringBuilder来构建一个带有密码的Sqlite连接字符串对象,然后使用Microsoft.Data.Sqlite名下的SqliteConnection来创建连接,特别注意的是,创建完之后,插入一张空表,不然可能会还是未加密的。

Sqlite可以通过PRAGMA命令来进一步修改密码

internal class Program
{
    static void Main(string[] args)
    {
        var folder = Environment.SpecialFolder.MyDocuments;
        var path = Environment.GetFolderPath(folder);
        var dbPath = System.IO.Path.Join(path, "postting.db");
        var baseConnectionString = $"Data Source={dbPath}";

        var oldPassword = "BkBqwG3ps25qQExj";
        var connectionString = new SqliteConnectionStringBuilder(baseConnectionString)
        {
            Mode = SqliteOpenMode.ReadWriteCreate,
            Password = oldPassword
        }.ToString();

        // 修改密码
        var newPassword = "BkBqwG3ps25qQEx";
        using (SqliteConnection connection = new SqliteConnection(connectionString))
        {
            connection.Open();

            using (var command = connection.CreateCommand())
            {
                command.CommandText = "SELECT quote($newPassword);";
                command.Parameters.AddWithValue("$newPassword", newPassword);
                var quotedNewPassword = command.ExecuteScalar() as string;

                command.CommandText = "PRAGMA rekey = " + quotedNewPassword;
                command.Parameters.Clear();
                command.ExecuteNonQuery();
            }
        }
        Console.ReadKey();
    }
}

基于EFCore来使用带有密码(SQLCipher加密机制)的Sqlite

依赖包

https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Sqlite

https://www.nuget.org/packages/SQLitePCLRaw.bundle_e_sqlcipher

dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 5.0.17
dotnet add package SQLitePCLRaw.bundle_e_sqlcipher --version 2.1.2
internal class Program
{
    static void Main(string[] args)
    {
        var folder = Environment.SpecialFolder.MyDocuments;
        var path = Environment.GetFolderPath(folder);
        var dbPath = System.IO.Path.Join(path, "postting.db");
        var dbPassword = "BkBqwG3ps25qQExj";
        var connectionString = $"Data Source={dbPath};Password={dbPassword};";

        var services = new ServiceCollection();
        services.AddDbContext<PosttingContext>(opt => opt.UseSqlite(connectionString));

        using (var scope = services.BuildServiceProvider().CreateScope())
        {
            var context = scope.ServiceProvider.GetService<PosttingContext>();
            context.Database.EnsureCreated();

            var blog = new Blog
            {
                BlogId = new Random(16839191).Next(),
                Url = "https://www.cnblogs.com/taylorshi/p/16839191.html"
            };
            context.Add(blog);
            context.SaveChanges();

            var blogs = context.Blogs.ToList();
            if (blogs.Any())
            {

            }
        }

        Console.ReadKey();
    }
}

如何在已激活后的Navicat 16中打开它呢?

先运行或者编译程序,前往bin\Debug\netcoreapp3.1\runtimes\win-x64\native目录

image

e_sqlcipher.dll改名成sqlite3.dll,然后将改名后的sqlite3.dll复制替换C:\Program Files\PremiumSoft\Navicat Premium 16目录下的sqlite3.dll即可

接下来新建SQLite 3的连接

image

并在高级中填写密码

image

就可以打开了。

image

参考

posted @ 2022-11-05 23:01  TaylorShi  阅读(317)  评论(0编辑  收藏  举报