在教學時直接使用 EF 對資料庫跑繫結的方式產生各頁面,但得到了「點一點東西就跑出來了搞不懂呀」的回饋,心想有道理。因此從頭開始實作一遍,並記錄下來。(雖然做完還是覺得,直接用 EF 跑的話果然比較安全方便啊)

目標:實作一個 MVC 架構,具資料庫基本操作功能的網站,其中包含連線至資料庫的 model、對其進行調用的 controller 以及顯示的 view。

註:本文預設已在本地電腦上安裝了 SQL Server,並且建立了測試用的資料庫 Test 及表 card,詳情會在文章內述。另外,由於在寫這邊的時候是為了練習手動從編碼開始嘗試連線,故將不使用 EF 連線產生 Edmx 的方式,而是直接手工編寫程式碼進行操作。

另外,關於直接從資料表自動產生可操作的頁面,亦即使用 Entity Framework 做資料繫結的方式,請見 Asp.net MVC 筆記:Entity Framework 連線資料庫

稍微補充一些簡單說明,給那些對 MVC 概念不是很熟悉的朋友:

MVC 是一種模式,就是做出一個程式的規則和架構,讓大家都按著這個架構撰寫程式碼。如此一來就可以簡化程式的開發過程,也增加了程式的可維護性和可讀性,適合多人同時作業——畢竟什麼該放在哪裡大家都有個共識。

而 MVC 顧名思義,就是將程式碼分成三個區塊:M、V 和 C。

  • Model:演算法、物件、資料處理等。像是數學邏輯、連接資料庫取得資料、狗的物件和拉不拉多的物件等等都放這裡
  • View:使用者會看到的部份,網頁的外觀。Html、Css 就是在這區工作。
  • Controller:流程控制和資料傳輸。也就是取得使用者傳送來的資料,決定讓哪支程式和哪個頁面出來做事,以及把 Model 送來的資料做處理後丟往 View 等等。

在小組報告的時候,Model 通常是默默做事的那個,View 則是專門上台報告的,Controller 負責指揮大家做事和組員間的溝通以及講幹話。

首先我們從建立專案開始操作。

選擇 .NET 的框架,並且在下面替專案取個名字。

確認選擇的框架是 MVC。

建立連線資料庫用的 Model

首先先在方案總管的 Models 資料夾裡新增一個類別。

由於我們是從平地起家,手工連線,因此我們這邊選擇空白類別就可以了,取個淺顯易懂的名字並按下新增。

新增之後應該能看見我們的類別。

在連線到資料庫的部分,最首先的就是連線字串。連線字串就像是地址加上鑰匙,可以想成你請人幫你到倉庫拿東西,必須先告訴他倉庫在哪再給他鑰匙進去拿一樣,程式必須藉由連線字串才能得知資料庫的位置和連線資料,而不同的資料庫軟體的連線字串格式也有可能不同,例如說 MySQL 的連線字串格式就可以參考這篇。在本筆記之中,不同資料庫類型在實作上最大的差異大概就是連線字串了吧。

而在連線至 MSSQL 的部分,我個人習慣用 Visual studio 內建的功能做產生再按照需求作修改。首先讓我們打開畫面左上方的伺服器總管,並在資料連接上加入一個新的連接。

接著就可以看到加入連接的頁面,我們從上往下進行解說:

資料來源:用來更改資料庫來源的類型,例如 Access 或是其他類型資料庫的時候就可以更改這一項。我們這邊使用 SQL Server 進行示範。

伺服器名稱:SQL Server 的名稱,通常會在 SQL Server 安裝的時候做設定,預設是和電腦一樣。當遠端連線的時候,這邊可以改成輸入 IP。

驗證:比較常用的是 Windows 驗證和 SQL Server 驗證。Windows 驗證即是當資料庫位於本機時,直接使用 Windows 登入者的資料驗證進資料庫。SQL Server 驗證則是使用 SQL Server 中設定的帳號密碼來登入,遠端連線至資料庫取資料的時候就會用帳密連線。

至於這兩項的選擇需要看使用時的需求以及資料庫本身的設定進行選擇。本篇是直接在 SQL Server 所在的本機上進行架站,同時也沒有特別設定權限,因此直接使用 Windoes 驗證登入就可以了。

資料庫名稱:從 Server 裡面選擇要使用的 Database。在此處選擇了示範用的 Test 資料庫。

以上部分設定完之後,點選左下角的測試連接。若是測試連接沒有問題後,在進階的部分就可以看見已經產生好連線字串了。同時在這個頁面按下確定的話,將會在伺服器總管中連線至資料庫,也能從資料連接的部分對資料庫進行操作。

↑ 可以從進階的部分看見連線字串。可以稍微看得出來連線字串的格式,其中 Data Source 放伺服器主機、Initial Catalog 放資料庫名稱、Integrated Security 放集成驗證(即 Windows 驗證),此外還有可能會有 UserID 放帳號、Password 放密碼等等。

↑ 如果選擇確定的話,就會建立連接。可以直接在這邊右鍵選擇各種功能以操作資料庫。

現在我們已經有了連線字串,先在我們的類別中將其宣告為一個私有且唯獨的字串常數。

註:在實務上的部份,連線字串通常會放置在網頁設定檔 Web.config 裡,因為網頁設定檔在使用者端是看不見的,所以這樣的做法比較安全,再利用 ConfigurationManager 的函式取得連線字串以使用。關於詳細的操作方式請參考這篇或搜尋「連線字串 Web.config」。本篇為了教學及操作上的方便,才直接放置於程式碼內。

解決連線字串的問題,接著我們來做一個 model 放我們的資料。可以從上面伺服器總管的圖看見我的資料表 card 有 id、 char_name、 card_name、card_level 四個欄位。

因此我們新建一個 card 類別如下:

namespace DBconnTest.Models
{
    public class Card
    {
        public int ID { get; set; }
        public string Char_name  { get; set; }
        public string Card_name  { get; set; }
        public string Card_level { get; set; }
    }
}

新建完成之後就回到我們的 資料庫連線類別 開始工作。

取得所有資料

第一個目標是能夠取得 Card 資料表的所有資料,因此我們先將方法的部分建立出來,也就是一個會回傳 Card 的 List 的方法。

我們將會使用 SqlConnection 這個資料庫連線用的工具來操作資料庫,因此需要先在上面 using System.Data.SqlClient

接著我們想要 GetCards 方法做的事有:

  1. 利用連線字串連線至資料庫
  2. 下 SQL 指令拿到資料
  3. 將拿到的資料做成 Card 物件
  4. 將 Card 們組成一個 List
  5. 回傳 List 給前端使用。

程式碼如下,其後逐步說明。

public List<Card> GetCards() {
    List<Card> cards = new List<Card>();
    SqlConnection sqlConnection = new SqlConnection(ConnStr);
    SqlCommand sqlCommand = new SqlCommand("SELECT * FROM card");
    sqlCommand.Connection = sqlConnection;
    sqlConnection.Open();

    SqlDataReader reader = sqlCommand.ExecuteReader();
    if (reader.HasRows) {
        while (reader.Read()) {
            Card card = new Card {
                ID = reader.GetInt32(reader.GetOrdinal("id")),
                Char_name  = reader.GetString(reader.GetOrdinal("char_name")),
                Card_name  = reader.GetString(reader.GetOrdinal("card_name")),
                Card_level = reader.GetString(reader.GetOrdinal("card_level")),
            };
            cards.Add(card);
        }
    }
    else {
        Console.WriteLine("資料庫為空!");
    }
    sqlConnection.Close();
    return cards;
}

首先,我們宣告了一個 Card 的 List 叫做 Cards 用來保存我們等等取得的資料。

接著我們利用連線字串 ConnStr 來建立了一個 SQL 連線物件 SqlConnection。並宣告了一個 SQL 命令物件 SqlCommand 來放置我們要執行的 SQL 指令。

這邊使用的是「由 card 資料表選取全部資料(*)」的指令。不論使用的是何種語言,後端連線到資料庫最重要的就是傳送指令至資料庫執行。關於 SQL 指令的使用方式,可以參照 1keyData 的 SQL 語法 或是 w3school

此外要注意,實務上如果真的需要自己寫連線,下 SQL 的命令和參數時應該用 SqlParameter 的方式讓系統檢查後放入,不應該偷懶直接將字串和參數用 + 接起來就傳送,否則很容易遭遇 SQL injection 的攻擊。

我們將指令的目標連線指向我們的連線 sqlConnection,完成後開啟(open)連線連至資料庫。

現在已經和資料庫連線了,但還沒執行我們的指令,宣告一個 sql 資料讀取物件 SqlDataReader 來抓取執行指令(sqlCommand.ExecuteReader)時回傳的資料。

要注意,當 SqlDataReader 在讀取資料庫時,是一行一行地讀取。因此,我們先判斷是否有讀到資料(HasRows = 有資料行嗎?),若有則逐行進行處理。在 Read() 的時候,運作的方式和我們人類 Read(讀) 書本是一樣的,會讀取一行的資料,因此我們利用 while 迴圈一直閱讀直到不能讀為止。

當 reader 在讀取資料行時,我們先宣告一個 Card 來放置我們要的資料,這邊為了方便直接用 new 物件 { 資料 } 的方式宣告,跟單純宣告的和定義好的建構式看起來有些不同,但結果會建出一個 card 是一樣的,端看需求選用。

首先是取得資料的部份,利用 reader.GetOrdinal 去取得該欄位的索引值\位置。例如說 card_name 是位於資料列的第三欄,那我們就會取得它的索引值為 2 (從 0 開始)。再利用 Get(類型,這邊的類型要配合資料庫內欄位的設定,例如 int32 配 int,string 配 nvarchar 之類) 的指令去取出對應索引值的資料。

雖然本篇示範的資料庫較小,能夠一目了然欄位所在的位置,下 reader.GetString(2) 也是能夠取得資料的。但其缺點也很明顯,若是資料庫欄位有所變更,抑或是資料庫欄位數量很多的時候,錯誤的風險就會相當大,因此用欄位名稱尋找索引值,再利用索引值取得資料內容的方式相對比較保險

另外,在處理回傳的資料時,會根據習慣和需求而有不同的做法,大多數的作法並不會一樣。例如說也有將回傳資料封裝成 Json 就丟給前端讓前端自己拆包自己頭痛的狀況存在,這部份在看其他人的連線教學或相關文章後就會比較了解。若是需要查詢相關的命令,例如說只執行命令時不回傳值,或單純只傳一個值,可以查詢微軟文件的 SqlCommand 頁面SqlConnection 頁面

將資料做成一個 card 後,我們將其放入我們的 List 也就是 cards。當全部的資料都讀完之後,絕對不能忘記關閉連線。最後回傳我們的 cards,到這邊 GetCards 就告一段落了。

為了測試,我們先到 HomeControlls 的 Index 做一點小修改。using 我們的 model 並且試試看用 GetCards 來取得資料。

接著把 Home/Index.cshtml 也進行修改。將原本的全部砍掉,為了示範方便,直接 using models 並且用 Viewbag 拿出來看。(註:這邊熟練之後若想了解實務上較建議使用的傳值方式,可以搜尋 “ViewModel”)

可以看見資料有正常地取得了

接著就可以開始處理前端顯示的部份,例如使用 Bootstrap 的表格 進行排版,或是其他 css 啦模板啦,都是沒問題的囉!

新增資料

既然能夠取得資料了,接下來就來做一些基本的操作吧。首先從新增資料開始。

為了後續操作方便和美觀,先從我們顯示資料的 Index 開始做處理。先將它改成bootstarp 的表格樣式,並且留一格之後放操作項。

<table class="table">
    <thead>
        <tr>
            <th>資料庫編號</th>
            <th>角色名稱</th>
            <th>卡片名稱</th>
            <th>卡片等級</th>
            <th></th>
        </tr>
    </thead>
    <tbody>

        @foreach (Card card in cards) {
            <tr>
                <th>@card.ID</th>
                <th>@card.Char_name</th>
                <th>@card.Card_name</th>
                <th>@card.Card_level</th>
                <th><th>
            </tr>
         }

    </tbody>
</table>

比剛剛好多了:

既然要新增資料,那一定要有個頁面能夠輸入新增的資料內容。因此我們回到 HomeController 來準備新增一個頁面。首先先把預設的 About 和 Contact 刪掉,也將 Views 裡的這兩頁給刪掉,我們不會用到它們了。

接著就來建立 CreateCard :

之後也必須建立 View 才可以。在 CreatrCard 上點右鍵,選擇新增檢視。

在這一步直接按下 Add 就會新增一個空白的 View 可以使用了。如果想讓系統自己幫你產生,Template 指得就是樣板,裡面已經包含新增刪除等操作會使用到的介面,選擇後再於 Model class 選取要操作的 Model 物件的話,就會自動產生好一個功能頁面讓你修改。不過開頭已經說過這次要純手工,因此我們就直接按下 Add 吧。

要把資料從 View 丟到 Controller 的方法很多,例如 form 表單或是 ajax 傳值都是可行的。我們這邊用 form 表單做回傳,另外使用 bootstarp 簡單排個版,程式碼如下

<h2>新增 Card</h2>
<br />

<form method="post" action="/Home/CreateCard">
    <div class="form-group row">
        <label for="inputCharName" class="col-sm-2">角色名稱</label>
        <div class="col-sm-10">
            <input type="text" id="inputCharName" name="Char_name" class="form-control" />
        </div>
    </div>
    <div class="form-group row">
        <label for="inputCardName" class="col-sm-2">卡片名稱</label>
        <div class="col-sm-10">
            <input type="text" id="inputCardName" name="Card_name" class="form-control" />
        </div>
    </div>
    <div class="form-group row">
        <label for="inputCardLevel" class="col-sm-2">卡片等級</label>
        <div class="col-sm-10">
            <input type="number" id="inputCardLevel" name="Card_level" class="form-control" />
        </div>
    </div>
    <input class="btn btn-default" type="submit" value="新增"/>
</form>

可以看見我們首先做了一個 form 表單,並指定這張表單將用 post 的方式將資料傳送到 /Home/CreateCard 這個路徑。屆時我們送出的時候,就會送到 HomeController 中有規定收取 post 的 CreateCard() 方法做處理。

表單中最重要的就是各個 input 的 name 屬性,當資料傳送的時候,會使用 name 做為資料的欄位名稱。因此這邊的 name 一定要能和我們的 card 物件的資料對應上,才能讓它自動轉換。

接著用 bootstrap 的格式做了三組輸入框,以及一個提交按鈕。運行後頁面正常就可以進行下一步了。

首先我們再度回到 HomeController,做一個 POST 才能進來的 CreateCard 方法來接取表單,並且讓它將傳入的表單資料做成我們建立的 Card 物件。

這個方法中應該要能將 card 的資料傳入資料庫,但我們還沒有實做傳入資料庫的方法,因此我們要回到當初建立的資料庫連線用的 model(本範例中為 DBconn)去實作一個新增用的函式。

新增的函式跟前面做過的查詢相差無幾,最大的差別在於新增的 SQL 語法是 INSERT INTO 。必須再提醒一次,最注意的一點是,在@字串中放置的參數,需要用 SqlParameter 的方式讓系統檢查後放入,不應該偷懶直接將字串和參數用 + 接起來就傳送,否則很容易遭遇 SQL injection 的攻擊,被亂下 SQL 指令。

處理好 model 中控制資料庫的部分之後,我們再回到 Controller 把這段添加上去。

可以看到當我們收到表單後,就會嘗試呼叫資料庫物件調用新增的函式做新增,如果新增時出了錯就會把錯誤訊息列印在輸出欄,最後回到首頁。

最後只要在首頁把新增的按鈕放上去就大功告成囉!

試跑幾次流程,可以看到有成功新增資料囉:

修改資料

接著要嘗試修改資料,修改資料和新增最大的不同是會多一步先將原本的該筆資料取出。首先先將首頁修改一下,在每一行的結尾放上修改資料的連結。由於做成連結的 ActionLink 並不能放進像 model 這麼複雜的資料,所以在實務上都是以傳遞 id 為主。( ActionLink 會把我們的資料做成連結、用 get 變成網址。所以 editCard.html?ID=2 這個它還看得懂,editCard.html?card=&$%* 這種它就不知道怎麼表達了)

我們一樣回到 Controller 新增一個方法,只是這次我們要求先把要修改的資料也傳到頁面做處理。

現在我們遇到的問題是:我們需要把 id 變成實際的卡片資料,必須利用這個 id 去取得資料。所以我們到 model 新增一個用 id 搜尋卡片資料的方法。

可以看見單筆查詢的做法基本上和前面做過的拿全部資料所差無幾,只是一個搜全部一個搜條件而已。最大的差異在於我們傳遞了一個 ID 進去,並且在 SQL 指令的地方利用 WHERE 下了搜尋條件,並且只回傳一項資料。

現在我們回到 Controller 將我們的 ID 搜尋方法放進去,並將回傳的 card 用傳遞 model 的方式傳到 View,期待它能聽話。

接著我們要新增編輯的頁面(View)了,一樣右鍵新增檢視,並複製新增頁面的程式碼修改如下。

特別標示紅色的部分要多加注意。

  • 首先我們傳入了 model,因此要先用 @model 告訴它我們傳遞的是哪個 model
  • 修改我們表單的回傳目標
  • 新增一格用來放置 ID,但是 ID 是不能修改的,因此把它設成唯讀
  • 接著我們將 model 的值先放入各個欄位,才能在修改的時候顯示原本的資料。

做完這一步的時候可以先從首頁執行是不是可以順利取得單筆資料。

修改完的資料必須回傳然後告訴資料庫修改的結果,因此我們和新增一樣要做一個 Post 的編輯方法來接收上面的表單。

現在我們該再度前往 資料庫控制的 model 加上修改資料庫內容的方法了。

一樣是和前面的新增資料很像,但這邊使用的 SQL 語法是更新資料用的 UPDATE 語法並且結合了前面的 WHERE 來指定要更改的項目。完成之後我們就可以把它加到 Controller 的方法上囉:

這樣就宣告完工了!實際上來編輯一次吧

將寫程式修改成打呼看看

可以看到成功變更囉!

刪除資料

如果前面都大致上了解的話,刪除就是小菜一碟。

首先我們先回到首頁列表,在編輯後面加上刪除的連結(我是用|隔開而已,也可以另外新增一格甚至做成按鈕沒關係)

接著我們到 Controller 增加一個刪除的方法,因為我們並沒有要做一個「確認刪除嗎?」的頁面,所以最後直接回到首頁列表就可以了。

接著就到資料庫互動的物件來實作刪除的部分。

非常簡單,只有 SQL 語法的部分有更動,使用的是 DELETE 指令,由於只是刪除,因此我們果斷地打開連線,砍下去,關閉連線走人。

回到 Controller 把這個函式添加上去

然後就可以試試看能不能刪除資料囉!

點下刪除了之後--

資料就不見了!(其實蠻危險的?)

結語

已經實作了顯示列表、新增、修改、查詢和刪除。所謂萬變不離其宗,基本的資料庫操作都離不開這些方法,熟悉了這些做法的概念之後就沒有問題了,剩下大多是顯示的時候做美化、排序等等的剩餘工作。Good Job!