ASP.NET Core MVC 和 Entity Framework Core 入門教程 - 進階主題 (十)

Contoso 大學示例 Web 應用程序演示如何使用實體框架(EF)Core 2.0 和 Visual Studio 2017 創建 ASP.NET Core 2.0 MVC Web 應用程序。 如欲了解更多本教程相關信息,請參閱 一、入門
在前面的教程中,您實現了 Table-per-Hierarchy 繼承。 本教程介紹幾個有用的知識點,當您掌握如何使用 Entity Framework Core 開發 ASP.NET Core Web 應用并進行部署的基礎知識后, 需要了解這些主題。

原始 SQL 查詢

使用 Entity Framework 的優點之一是,它避免了將代碼綁定到一個特定的數據存儲方法。它通過為您生成 SQL 查詢和命令來實現這一點,同時將你從SQL查詢編寫中解放出來。但是,總有一些意外的場景需要你運行手動創建的 SQL 查詢。 對于這些場景,EF Code First API 包含有讓您能夠將 SQL 命令直接傳遞到數據庫的方法。 在 EF Core 1 中有以下選項。

  • 使用 Dbset.FromSql 返回實體類型的查詢方法。 返回的對象必須是 Dbset 對象期待的類型,然后這些數據將自動被數據上下文(Database Context)進行跟蹤,除非你手動 關閉跟蹤
  • 使用 Database.ExecuteSqlCommand 執行無查詢結果返回的命令。
    如果你需要運行的查詢返回類型不是實體類型,你可以通過 EF 提供的數據庫連接使用 ADO.NET 。 返回的數據將不會被數據庫上下文跟蹤,即使你使用此方法檢索實體類型。
    在 Web 應用程序中執行 SQL 命令時總是如此,您必須采取預防措施來保護您的站點免受 SQL 注入攻擊。 一種方法是使用參數化查詢來確保網頁提交的字符串不能被解釋為 SQL 命令。 在本教程中,您將在把用戶輸入集成到查詢中時使用參數化查詢。

調用返回實體的查詢

DbSet<Tentity> 類提供了一種方法,可以使用該方法執行返回類型為 TEntity 實體的查詢。 想要看它是如何工作的,你可以通過更改 Department 控制器的 Details 方法中的代碼來進行。
在 DepartmentsController.cs 文件, Details 方法中,用 FromSql 方法調用替換檢索部門的代碼,如以下突出顯示的代碼所示:

public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
    var department = await _context.Departments
        .FromSql(query, id)
        .Include(d => d.Administrator)
        .AsNoTracking()
        .SingleOrDefaultAsync();

    if (department == null)
    {
        return NotFound();
    }

    return View(department);
}

為驗證新代碼是否工作正常,請選擇 Deparment 菜單,然后點擊其中任一部門的 Detail 鏈接。

調用返回其他類型的查詢

之前,您為關于頁面創建了一個學生統計表格,顯示每個日期的學生注冊人數。 您從學生實體集合(_context.Students)獲得了數據,并使用 LINQ 將結果投影到 EnrollmentDateGroup 視圖模型對象列表中。 現在假設你不想使用 LINQ 而想直接編寫 SQL 。要做到這一點,你需要運行一個 SQL 查詢,返回一些并非實體對象的東西。 在 EF Core 1.0 中,一種方法是從 EF 獲取數據庫連接并編寫 ADO.NET 代碼。
在 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 方法被調用, 參數 multiplier 得到文本框中輸入的值。 然后代碼執行 SQL 更新 Courses 數據表,并將受影響的行數返回到 ViewData 供視圖使用。 當視圖獲得 RowsAffected 值時,顯示更新的行數。
在解決方案資源管理器中,右鍵單擊 Views/Courses 文件夾,然后單擊 添加>新建項。
在 添加新項 對話框中, 點擊左側面板中 “已安裝” 下的 “ASP.NET", 再點擊 "MVC View Page (中文: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>

通過點擊 Courses 鏈接,然后在地址欄 URL 后面加入 /UpdateCourseCredits (例如: http://localhost:5813/Courses/UpdateCourseCredits) 的方法可以運行 UpdateCourseCredits 方法。 在文本框中輸入一個數字:


點擊 Update 。 你可以看到受影響的行數:

點擊 Back to List 查看修改學分后的課程列表。
請注意,在生產代碼中,須確保更新總是確保數據是有效的。 此處的簡化代碼可能導致學分數字大于 5 (通常學分有一個固定范圍 0-5 )。 更新查詢將可以正常工作,但無效的數據可能將導致系統的其他部分產生意外的結果(因為預先假設學分應該在 0-5 之間)。
有關如何使用原始 SQL 查詢的信息, 請參閱 Raw SQL Queries

檢查發送到數據庫的 SQL 語句

有時能夠看到發送到數據庫的實際 SQL 查詢會很有幫助。 EF Core 自動使用 ASP.NET Core 內置的日志記錄功能記錄查詢和更新中包含的 SQL 日志。 在本節中,您將看到一些SQL 日志記錄的例子。
打開 StudentsController.cs 并在 Details 方法 if (student == null) 語句上設置一個斷點。
在 Debug 模式下運行應用程序, 并轉至 學生詳細 頁面。
轉到輸出顯示調試窗口輸出,你可以看到對應的查詢語句:

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] = N'Student') 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] = N'Student') 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 方法并不是解析為服務器上的一行。 原因如下:

  • 如果查詢可能返回多個行,該方法返回 null。
  • 若要確定查詢是否將返回多個行,EF 必須檢查是否它返回至少為 2。
    請注意,您不必使用調試模式并在斷點處停止以在“輸出”窗口中獲取日志輸出。這只是一個方便的方法來停止日志記錄在你想看看輸出點。 如果你不這樣做,日志繼續輸出,你必須向前滾動滾動條才能找到你感興趣的部分。

倉儲和單元工作模式

許多開發人員編寫代碼來實現倉儲和單元工作模式,作為與 Entity Framework 一起工作的包裝器。 這些模式旨在創建應用程序中數據訪問層和業務邏輯層之間的抽象層。實現這些模式可以幫助您將應用程序與數據存儲區中的更改隔離開來,并且可以促進自動化單元測試或測試驅動開發(TDD)。但是,編寫額外的代碼來實現這些模式并不總使用 EF 開發應用程序的最佳選擇,原因如下:

  • EF 上下文類本身已經將你的代碼與特定的數據存儲代碼分離開了。
  • EF 上下文類也可以充當 Unit of Work 類來更新數據庫。
  • EF 包含實現 TDD 的特性,無需編寫倉儲庫代碼。
    有關如何實現倉儲和單元工作模式, 請參閱 the Entity Framework 5 Version of this tutorial series.
    Entity Framework Core 實現了一個內存數據庫提供程序。有關更多信息, 請參閱 Testing with InMemory

自動更改檢測

實體框架通過將實體的當前值與原始值進行比較來確定實體如何改變(以及因此需要將哪些更新發送到數據庫)。 原始值在實體被查詢或附加時被存儲。 一些導致自動更改檢測的方法如下:

  • DbContext.SaveChagnes
  • DbContext.Entry
  • ChangeTracker.Entries
    如果您正在跟蹤大量實體,并且您在循環中多次調用其中一個方法,則可以通過使用 ChangeTracker.AutoDetectChangesEnabled 屬性臨時關閉自動更改檢測來顯著提高性能。例如:
_context.ChangeTracker.AutoDetectChangesEnabled = false;

Entity Framework Core 源代碼及開發計劃

Entity Framework Core 的源代碼托管在 https://github.com/aspnet/EntityFrameworkCore 。 源碼倉庫中包含 夜間構建, 問題跟蹤, 功能規范, 設計會議記錄以及未來發展的路線圖。 你可以提交或發行錯誤,并作出貢獻。
盡管源代碼是開放的,但 Entity Framework Core 得到了微軟的全面支持。 微軟 Entity Framework Core 團隊保持對貢獻的控制,哪些可以被接受,并對所有的代碼修改進行測試,以確保每個發行版的質量。

從現有數據庫進行逆向工程

要從現有數據庫中反向得到數據模型和實體類, 使用 scaffold-dbcontext 命令, 請參閱 getting-started tutorial / 入門教程

使用 dynamic 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));
}

后續步驟

本篇完成了 如何使用 Entity Framewrok Core 創建 ASP.NET MVC Web 應用程序 的教程。
如欲了解有關 EF Core 的更多信息, 請參閱 Entity Framework Core 文檔 。 還有一本書可供選擇: Entity Framework in Action
有關如何部署一個 WEB 應用,請參閱 Host and depoly
與 ASP.NET Core MVC 相關的其他主題(如身份驗證和授權)的信息,請參閱 ASP.NET Core 文檔

致謝

Tom Dykstra 和 Rick Anderson(twitter @RickAndMSFT)編寫了這個教程。 Rowan Miller,Diego Vega 和 Entity Framework 團隊的其他成員協助代碼審查,并幫助調試
在編寫教程代碼時出現的問題。

常見錯誤

ContosoUniversity.dll used by another process
錯誤信息:

Cannot open '...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' for writing -- 'The process cannot access the file '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll' because it is being used by another process.

解決方案:
停止 IIS Express 中的站點。 找到 Windows 系統托盤中的 IIS Express ,右鍵點擊圖標,選擇 Contoso University 站點,單擊停止站點。
Migration scaffolded with no code in Up and Down methods
可能的原因:
EF CLI 命令不會自動關閉和保存文件。 如果你在運行 migrations add 命令時有未保存的修改, EF 將找不到你所做的修改。
解決方案:
運行 migrations remove 命令, 保存你的代碼修改,然后重新運行 migrations add 命令。
Errors while running database update(運行數據庫更新命令時出錯)
在已有數據的數據庫中進行架構更改,可能會發生其他錯誤。如果遇到無法解析的遷移錯誤,則可以更改連接字符串中的數據庫名稱或刪除數據庫。 使用新的數據庫,不需要遷移任何數據,update-database 命令更可能完成而不會出錯。
最簡單的方法是在appsettings.json中重命名數據庫。 下一次運行數據庫更新時,將創建一個新的數據庫。
要在 SSOX 中刪除一個數據庫, 右鍵點擊數據庫,單擊 刪除 , 在 刪除數據庫 對話框中選項關閉現有連接,然后單擊 確定
若要使用 CLI 刪除數據庫,運行 database drop CLI 命令:

dotnet ef database drop

Error locating SQL Server instance(無法定位SQL Server 實例)
錯誤信息:

A network-related or instance-specific error occurred while establishing a connection to SQL Server. The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server is configured to allow remote connections. (provider: SQL Network Interfaces, error: 26 - Error Locating Server/Instance Specified)

解決方案:
檢查連接字符串。 如果您手動刪除了數據庫文件,則更改構造字符串中的數據庫名稱以重建新的數據庫。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內容