讀者 Ho.Chun 問了一個問題:在 EF Core 透過 DbContext.Database.GetDbConnection() 取得的連線字串,使用完需不需要關閉?

依我的理解,DbContext.Database.GetDbConnection() 的用意是允許我們存取底層連線物件,透過 IDbCommand 或 Dapper 直接進行資料庫操作,以克服 EF Core API 不支援或沒效率的情境。要達成這個目標,這條連線必須是 EF Core 正在使用的那一條,才能共享 Transaction 緊急整合。EF Core 使用的資料庫連線物件,其生命週期應由 EF Core 管理,呼叫端不宜插手,理論上用完也不必關閉,以免妨礙 EF Core 後續使用。

官方文件的說明更完整:

services.AddDbContextPool<MyDbContext>(options => options.UseSqlServer("...")... 時是傳入連線字串(大部分的情境是這種),EF Core 會負責建立連線,應用程式端不應將其關閉;但若 UseSqlServer(connObject) 時傳入的是連線物件,則應用程式要負責關閉連線。

借用之前的ASP.NET Core 新增修改刪除(CRUD)介面傻瓜範例,我設計了一個實驗驗證這點:

public class IndexModel : PageModel
{
    private readonly CRUDExample.Models.JournalDbContext _context;
    private readonly ILogger<IndexModel> _logger;

    public IndexModel(CRUDExample.Models.JournalDbContext context, ILogger<IndexModel> logger)
    {
        _context = context;
        _logger = logger;

        var conn = _context.Database.GetDbConnection();
        _logger.LogDebug($"Conn State Before BeginTransaction = {conn.State}");
        // 驗證 Connetion 共用
        using (var tran = context.Database.BeginTransaction())
        {
            _logger.LogDebug($"Conn State After BeginTransaction = {conn.State}");
            var first = context.Records.OrderBy(o => o.Id).First();
            _logger.LogDebug($"Orig Remark = {first.Remark}");
            first.Remark = $"Updated-{DateTime.Now:mmss.fff}";
            context.SaveChanges();

            // 使用 Database.GetDbConnection() 連線進行查詢
            var cmd = conn.CreateCommand();
            cmd.CommandText = "SELECT Remark FROM Records Where Id=@id";
            cmd.Parameters.Add(new SqlParameter("@id", first.Id));
            // 由於連線已啟動 Transaction,因此必須將 Transaction 物件傳入 Command
            cmd.Transaction = tran.GetDbTransaction();
            var dr = cmd.ExecuteReader();
            dr.Read();
            _logger.LogDebug($"Remark by ADO = {dr[0]}");
            dr.Close();

            // 另外建立連線嘗試查詢,預期會 Timeout
            var anotherConn = new SqlConnection(conn.ConnectionString);
            anotherConn.Open();
            var cmd2 = anotherConn.CreateCommand();
            cmd2.CommandText = cmd.CommandText;
            cmd2.Parameters.Add(new SqlParameter("@id", first.Id));
            // 將等待逾時縮短為三秒
            cmd2.CommandTimeout = 3;
            try
            {
                var dr2 = cmd2.ExecuteReader();
                dr2.Read();
                _logger.LogDebug($"Remark From Another Conn = {dr2[0]}");
            }
            catch (Exception ex)
            {
                _logger.LogError($"Another Connection Error = {ex.Message}");
            }
            finally
            {
                anotherConn.Close();
            }
            tran.Rollback();
        }

    }
}

一開始 var conn = _context.Database.GetDbConnection(); 取得連線物件,先檢查 conn.State 應為 Closed。接著 EF Core 啟用 Transaction 後理應開啟連線,此時再次檢查 conn.State 應為 Open。接著先用 LINQ 查詢並修改某資料的 Remark 欄位,SaveChanges() 後,用 conn.CreateCommand() 建立 SqlCommand 下 SELECT Remark FROM Records Where Id=@id 查詢該筆資料(需透過 cmd.Transaction = tran.GetDbTransaction() 參與交易),可查到更新後的值。這個動作只有同一條連線並參與同一交易時才能做到,由此證明 Database.GetDbConnection() 的連線就是 SaveChanges() 用的連線;進一步,我用相同連線字串再建一個新連線,也執行相同查詢,由於資料尚未 Commit 被鎖定中,最後以 Timeout 收場,示範若不是共同一條連線會發生什麼事。

實測結果如下,印證了我們的假設。

接著,再做一個實驗,這次不信邪把 _context.Database.GetDbConnection() 拿到的連線 .Close() 或 .Dispose() 看看會怎樣?

public IndexModel(CRUDExample.Models.JournalDbContext context, ILogger<IndexModel> logger)
{
    _context = context;
    _logger = logger;

    var conn = _context.Database.GetDbConnection();
    _logger.LogDebug($"Conn State Before foreach = {conn.State}");
    foreach (var rec in context.Records)
    {
        _logger.LogDebug($"Event Summary = {rec.EventSummary}");
        _logger.LogDebug($"Conn State in foreach = {conn.State}");
    }
    _logger.LogDebug($"Conn State After foreach = {conn.State}");
    conn.Close();
    Console.WriteLine($"Records.Count = {context.Records.Count()} / after conn.Close()");
    conn.Dispose();
    try
    {
        _logger.LogDebug($"Records.Count = {context.Records.Count()} / after conn.Dispose()");
    }
    catch (Exception ex)
    {
        _logger.LogError($"context.Records.Count() Error = {ex.Message}");
    }
}

我做了幾組測試,一開始就取得 var conn = _context.Database.GetDbConnection(),比較 foreach (var rec in context.Records) 前、中、後的連線狀態。接著我們將 conn.Close(),查一次 context.Records.Count();再來狠一點,conn.Dispose() 再看看還能不能用?

由觀察結果,EF Core 平時會保持連線關閉,foreach (var rec in context.Records) 過程開啟,做完就關閉。所以 EF Core 的原則是有必要才開啟,用完就關。因此,conn.Close() 不會影響一般查詢(註:如果有 context.Database.BeginTransaction() 連線必須長期開啟,就不能隨便 Close());但如果 .Dispose(),EF Core 便無法再開啟,這樣會有問題。

結論:EF Core 自己會管理連線的開啟關閉,Database.GetDbConnection() 拿到的連線物件用就好,沒事不要亂 .Close(),更不可 .Dispose()。

This article presents connection management in EF Core, including when to open and close connections, and explains why we should not close connections from Database.GetDbConnection().


Comments

# by 小黑

謝黑哥~

# by Ho.Chun

目前遇到一個問題 var conn = _context.Database.GetDbConnection(); using (var tran = context.Database.BeginTransaction()) { // 某個 table 處理,必須於同個 transaction 內 // 紀錄 log 於 DB,不可使用 transaction (避免 log 被回滾) // 某個 table 處理,必須於同個 transaction 內 } 這種情境下,除了像上面的範例一樣 直接使用 var anotherConn = new SqlConnection(conn.ConnectionString); 開啟一個獨立連線 有辦法使用 DbContext 再去拿到一個獨立連線嗎 ? 🤔

Post a comment