乘風破浪,遇見最佳跨平臺跨終端框架.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

對接示例

建立示例領域和上下文

領域模型

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

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章