1. 程式人生 > >用ASP.NET Core MVC 和 EF Core 構建Web應用 (十)

用ASP.NET Core MVC 和 EF Core 構建Web應用 (十)

pan 信息 expr dispose writing AS das ech 任務欄

之前的學習中,已經以每個類一張表的方式實現了繼承。 本節將會介紹在掌握開發基礎 ASP.NET Core web 應用程序之後使用 Entity Framework Core 開發時需要註意的幾個問題。

原生 SQL 查詢

使用 Entity Framework 的優點之一是它可避免你編寫跟數據庫過於耦合的代碼 它會自動生成 SQL 查詢和命令,使得你無需自行編寫。 但有一些特殊情況,你需要執行手動創建的特定 SQL 查詢。 對於這些情況下, Entity Framework Code First API 包括直接傳遞 SQL 命令將到數據庫的方法。 在 EF Core 1.0 中具有以下選項:

  • 使用DbSet.FromSql返回實體類型的查詢方法。 返回的對象必須是DbSet對象期望的類型,並且它們會自動跟蹤數據庫上下文中除非你手動關閉跟蹤。

  • 對於非查詢命令使用Database.ExecuteSqlCommand

如果需要運行該返回類型不是實體的查詢,你可以使用由 EF 提供的 ADO.NET 中使用數據庫連接。 數據庫上下文不會跟蹤返回的數據,即使你使用該方法來檢索實體類型也是如此。

在 Web 應用程序中執行 SQL 命令時,請務必采取預防措施來保護站點免受 SQL 註入攻擊。 一種方法是使用參數化查詢,確保不會將網頁提交的字符串視為 SQL 命令。 在本教程中,將用戶輸入集成到查詢中時會使用參數化查詢。

調用返回實體的查詢

DbSet<TEntity> 類提供了可用於執行查詢並返回TEntity類型實體的方法。 若要查看實現細節,你需要更改部門控制器中Details方法的代碼。

DepartmentsController.cs中的Details方法,通過代碼調用FromSql方法檢索一個部門,如以下高亮代碼所示:

 1 public async Task<IActionResult> Details(int? id)
 2 {
 3     if (id == null)
 4     {
 5         return NotFound();
 6     }
7 8 string query = "SELECT * FROM Department WHERE DepartmentID = {0}"; 9 var department = await _context.Departments 10 .FromSql(query, id) 11 .Include(d => d.Administrator) 12 .AsNoTracking() 13 .SingleOrDefaultAsync(); 14 15 if (department == null) 16 { 17 return NotFound(); 18 } 19 20 return View(department); 21 }

為了驗證新代碼是否工作正常,請選擇Department選項卡,然後點擊某個部門的Detail。

調用返回其他類型的查詢

之前你在“關於”頁面創建了一個學生統計信息網格,顯示每個註冊日期的學生數量。 可以從學生實體集中獲取數據 (_context.Students) ,使用 LINQ 將結果投影到EnrollmentDateGroup視圖模型對象的列表。 假設你想要 SQL 本身編寫,而不使用 LINQ。 需要運行 SQL 查詢中返回實體對象之外的內容。 在 EF Core 1.0 中,執行該操作的另一種方法是編寫 ADO.NET 代碼,並從 EF 獲取數據庫連接。

HomeController.cs中,將About方法替換為以下代碼:

public async Task<ActionResult> About()
{
    List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
    var conn = _context.Database.GetDbConnection();
    try
    {
        await conn.OpenAsync();
        using (var command = conn.CreateCommand())
        {
            string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
                + "FROM Person "
                + "WHERE Discriminator = ‘Student‘ "
                + "GROUP BY EnrollmentDate";
            command.CommandText = query;
            DbDataReader reader = await command.ExecuteReaderAsync();

            if (reader.HasRows)
            {
                while (await reader.ReadAsync())
                {
                    var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
                    groups.Add(row);
                }
            }
            reader.Dispose();
        }
    }
    finally
    {
        conn.Close();
    }
    return View(groups);
}

添加 using 語句:

using System.Data.Common;

運行應用並轉到“關於”頁面。 顯示的數據和之前一樣。

調用更新查詢

假設 Contoso 大學管理員想要在數據庫中執行全局更改,例如如更改的每個課程的可修讀人數。 如果該大學有大量的課程,檢索所有實體並單獨更改會降低效率。 在本節中,你將實現一個頁面,使用戶能夠指定一個參數,通過這個參數可以更改所有課程的可修讀人數,在這裏你會通過執行 SQL UPDATE 語句來進行更改。 頁面外觀類似於下圖:

技術分享圖片

在 CoursesContoller.cs 中,為 HttpGet 和 HttpPost 添加 UpdateCourseCredits 方法:

public IActionResult UpdateCourseCredits()
{
    return View();
}
[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
    if (multiplier != null)
    {
        ViewData["RowsAffected"] = 
            await _context.Database.ExecuteSqlCommandAsync(
                "UPDATE Course SET Credits = Credits * {0}",
                parameters: multiplier);
    }
    return View();
}

當控制器處理 HttpGet 請求時,ViewData["RowsAffected"]中不會返回任何東西,並且在視圖中顯示一個空文本框和提交按鈕,如上圖所示。

當單擊Update按鈕時,將調用 HttpPost 方法,且從文本框中輸入的值獲取乘數。 代碼接著執行 SQL 語句更新課程,並向視圖的ViewData返回受影響的行數。 當視圖獲取RowsAffected值,它將顯示更新的行數。

在“解決方案資源管理器”中,右鍵單擊“Views/Courses”文件夾,然後依次單擊“添加”和“新建項”。

在添加新項對話框中,在已安裝下單擊ASP.NET,在左窗格中,單擊MVC 視圖頁,並將新視圖命名為UpdateCourseCredits.cshtml

Views/Courses/UpdateCourseCredits.cshtml中,將模板代碼替換為以下代碼:

技術分享圖片
@{
    ViewBag.Title = "UpdateCourseCredits";
}

<h2>Update Course Credits</h2>

@if (ViewData["RowsAffected"] == null)
{
    <form asp-action="UpdateCourseCredits">
        <div class="form-actions no-color">
            <p>
                Enter a number to multiply every course‘s credits by: @Html.TextBox("multiplier")
            </p>
            <p>
                <input type="submit" value="Update" class="btn btn-default" />
            </p>
        </div>
    </form>
}
@if (ViewData["RowsAffected"] != null)
{
    <p>
        Number of rows updated: @ViewData["RowsAffected"]
    </p>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>
View Code

通過選擇Courses選項卡運行UpdateCourseCredits方法,然後在瀏覽器地址欄中 URL 的末尾添加"/ UpdateCourseCredits"到 (例如: http://localhost:5813/Courses/UpdateCourseCredits)。 在文本框中輸入數字:

單擊Update。 你會看到受影響的行數:

技術分享圖片

單擊Back To List可以看到課程列表,其中可修讀人數已經替換成修改後的數字。

請註意生產代碼將確保更新最終得到有效的數據。 此處所示的簡化代碼會使得相乘後可修讀人數大於 5。 (Credits屬性具有[Range(0, 5)]特性。)更新查詢將起作用,但無效的數據會導致意外的結果,例如在系統的其他部分中加入可修讀人數為 5 或更少可能會導致意外的結果。

檢查發送到數據庫的 SQL

有時能夠以查看發送到數據庫的實際 SQL 查詢對於開發者來說是很有用的。 EF Core 自動使用 ASP.NET Core 的內置日誌記錄功能來編寫包含 SQL 查詢和更新的日誌。

打開StudentsController.cs並在Details方法的if (student == null)語句上設置斷點。

在調試模式下運行應用,並轉到某位學生的“詳細信息”頁面。

轉到輸出窗口顯示調試輸出,就可以看到查詢語句:

Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (56ms) [Parameters=[@__id_0=‘?‘], CommandType=Text, CommandTimeout=30]
SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].[LastName], [s].[EnrollmentDate]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = NStudent) AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (122ms) [Parameters=[@__id_0=‘?‘], CommandType=Text, CommandTimeout=30]
SELECT [s.Enrollments].[EnrollmentID], [s.Enrollments].[CourseID], [s.Enrollments].[Grade], [s.Enrollments].[StudentID], [e.Course].[CourseID], [e.Course].[Credits], [e.Course].[DepartmentID], [e.Course].[Title]
FROM [Enrollment] AS [s.Enrollments]
INNER JOIN [Course] AS [e.Course] ON [s.Enrollments].[CourseID] = [e.Course].[CourseID]
INNER JOIN (
    SELECT TOP(1) [s0].[ID]
    FROM [Person] AS [s0]
    WHERE ([s0].[Discriminator] = NStudent) AND ([s0].[ID] = @__id_0)
    ORDER BY [s0].[ID]
) AS [t] ON [s.Enrollments].[StudentID] = [t].[ID]
ORDER BY [t].[ID]

你會註意到一些可能會讓你覺得驚訝的操作: SQL 從 Person 表最多選擇 2 行 (TOP(2)) 。 SingleOrDefaultAsync方法服務器上不會解析為 1 行。 原因是:

  • 如果查詢返回多個行,該方法會返回 null。
  • 如果想知道查詢是否返回多個行,EF 必須檢查是否至少返回 2。

請註意,你不必使用調試模式,並在斷點處停止,然後在輸出窗口獲取日誌記錄。 這種方法非常便捷,只需在想查看輸出時停止日誌記錄即可。 如果不進行此操作,程序將繼續進行日誌記錄,要查看感興趣的部分則必須向後滾動。

存儲庫和工作單元模式

許多開發人員編寫代碼實現存儲庫和工作模式單元以作為使用 Entity Framework 代碼的包裝器。 這些模式用於在應用程序的數據訪問層和業務邏輯層之間創建抽象層。 實現這些模式可讓你的應用程序對數據存儲介質的更改不敏感,而且很容易進行自動化單元測試和進行測試驅動開發 (TDD)。 但是,編寫附加代碼以實現這些模式對於使用 EF 的應用程序並不總是最好的選擇,原因有以下幾個:

  • EF 上下文類可以為使用 EF 的數據庫更新充當工作單位類。

  • 對於使用 EF 進行的數據庫更新,EF 上下文類可充當工作單元類。

  • EF 包括用於無需編寫存儲庫代碼就實現 TDD 的功能。

自動臟值檢測

Entity Framework 通過比較的實體的當前值與原始值來判斷更改實體的方式 (因此需要發送更新到數據庫)。查詢或附加該實體時會存儲的原始值。 如下方法會導致自動臟值:

  • DbContext.SaveChanges

  • DbContext.Entry

  • ChangeTracker.Entries

如果正在跟蹤大量實體,並且這些方法之一在循環中多次調用,通過使用ChangeTracker.AutoDetectChangesEnabled屬性暫時關閉自動臟值檢測,能夠顯著改進性能。 例如:

_context.ChangeTracker.AutoDetectChangesEnabled = false;

Entity Framework Core 源代碼與開發計劃

Entity Framework Core 源位於 https://github.com/aspnet/EntityFrameworkCore。 倉庫中除了有源代碼,還可包括每夜生成、 問題跟蹤、 功能規範、 設計會議備忘錄和將來的開發路線圖。 你可以歸檔或查找 bug 並進行更改。

盡管源代碼處於開源狀態, Entity Framework Core 是由 Microsoft 完全支持的產品。 Microsoft Entity Framework 團隊將控制接受哪些貢獻和測試所有的代碼更改,以確保每個版本的質量。

現有數據庫逆向工程

若想要通過對現有數據庫中的實體類反向工程得出數據模型,可以使用scaffold-dbcontext。

使用動態 LINQ 來簡化對所選內容排序的代碼

本系列的第三節演示如何通過在switch語句中對列名稱進行硬編碼來編寫 LINQ 。 如果只有兩列可供選擇,這種方法可行,但是如果擁有許多列,代碼可能會變得冗長。 要解決該問題,可使用 EF.Property 方法將屬性名稱指定為一個字符串。 要嘗試此方法,請將 StudentsController 中的 Index 方法替換為以下代碼。

技術分享圖片
 public async Task<IActionResult> Index(
     string sortOrder,
     string currentFilter,
     string searchString,
     int? page)
 {
     ViewData["CurrentSort"] = sortOrder;
     ViewData["NameSortParm"] = 
         String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
     ViewData["DateSortParm"] = 
         sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";

     if (searchString != null)
     {
         page = 1;
     }
     else
     {
         searchString = currentFilter;
     }

     ViewData["CurrentFilter"] = searchString;

     var students = from s in _context.Students
                    select s;
     
     if (!String.IsNullOrEmpty(searchString))
     {
         students = students.Where(s => s.LastName.Contains(searchString)
                                || s.FirstMidName.Contains(searchString));
     }

     if (string.IsNullOrEmpty(sortOrder))
     {
         sortOrder = "LastName";
     }

     bool descending = false;
     if (sortOrder.EndsWith("_desc"))
     {
         sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
         descending = true;
     }

     if (descending)
     {
         students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
     }
     else
     {
         students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
     }

     int pageSize = 3;
     return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), 
         page ?? 1, pageSize));
 }
View Code

常見錯誤

ContosoUniversity.dll 被另一個進程使用

錯誤消息:

無法打開“...bin\Debug\netcoreapp1.0\ContosoUniversity.dll‘ for writing -- ”進程無法訪問文件“...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll”,因為它正在由其他進程使用。

解決方案:

停止 IIS Express 中的站點。 請轉到 Windows 系統任務欄中,找到 IIS Express 並右鍵單擊其圖標、 選擇 Contoso 大學站點,然後單擊停止站點。

遷移基架的 Up 和 Down 方法中沒有代碼

可能的原因:

EF CLI 命令不會自動關閉並保存代碼文件。 如果在運行migrations add命令時,你未保存更改,EF 將找不到所做的更改。

解決方案:

運行migrations remove命令,保存你更改的代碼並重新運行migrations add命令。

運行數據庫更新時出錯

在有數據的數據庫中進行架構更改時,很有可能發生其他錯誤。 如果遇到無法解決的遷移錯誤,你可以更改連接字符串中的數據庫名稱,或刪除數據庫。 若要遷移,創建新的數據庫,在數據庫尚沒有數據時使用更新數據庫命令更有望完成且不發生錯誤。

最簡單方法是在appsettings.json中重命名數據庫。 下次運行database update時,會創建一個新數據庫。

若要在 SSOX 中刪除數據庫,右鍵單擊數據庫,單擊刪除,然後在刪除數據庫對話框框中,選擇關閉現有連接,單擊確定。

若要使用 CLI 刪除數據庫,可以運行database dropCLI 命令:

dotnet ef database drop

定位 SQL Server 實例出錯

錯誤消息:

建立到 SQL Server 的連接時出現與網絡相關或特定於實例的錯誤。 未找到或無法訪問服務器。 請驗證實例名稱是否正確,SQL Server 是否已配置為允許遠程連接。 (提供程序:SQL 網絡接口,錯誤:26 - 定位指定的服務器/實例時出錯)

解決方案:

請檢查連接字符串。 如果你已手動刪除數據庫文件,更改數據庫的構造字符串中數據庫的名稱,然後從頭開始使用新的數據庫。

*****************************
*** Keep learning and growing. ***
*****************************

用ASP.NET Core MVC 和 EF Core 構建Web應用 (十)