昨天我們重新檢視了資料庫移轉所產生物件關聯對映的資料表,以及資料表間的關聯。在檢視的過程當中也發現了原始對映出來的資料表不十分理想,因此也重新做了一些修正。既然資料庫的資料表已經有了,今天就來看看如何在資料庫的資料表中儲存資料。
為何要預先餵入資料
一般來講資料庫內的資料大多是藉由操作應用程式的介面(包含 IoT 連結的感測器)來收集資料並儲存入資料庫。但是有時在系統上線前有些固定的基礎資料必需事先填入才有辦法操作,例如先前建立的地址有關資料,其中的縣市和鄉鎮(區)的資料應該是不會經常變動的,這些資料是理應是一開始就該依據政府單位所制定的名稱事先餵入資料庫,以後往後應用程式操作之用。
話說要餵入資料,但也不是使用 Microsoft SQL Server Management Studio 手動一筆一筆的 Key 才對,應該是使用整理好的文件整個一起倒進資料庫才對,今天就來先把全台灣(中華民國啦)的所有縣市和鄉鎮區的資料倒入昨天所建立的資料庫中,資料來源使用「中華郵政 3+2郵遞區號查詢應用系統」內的資料。
建立資料庫初始化類別
因為餵入基礎資料屬於資料存取的部分,所以先在 Demae.Data 專案加入一個類別,並取一個具有意義的名稱,因為是要用來初始化 DemaeDb 這個資料庫,所以就命名為 DemaeDbInitializer:
接著加入如下之程式碼:
namespace Demae.Data
{
public class DemaeDbInitializer
{
private DemaeContext _ctx;
public DemaeDbInitializer(DemaeContext ctx)
{
_ctx = ctx;
}
}
}
首先 DemaeDbInitializer
建構函式傳入 DemaeContext
型別的參數,並指定給私有屬性 _ctx
做為接下來的新增資料時與資料庫進行存取之用。
接著在構建函式底下再加入餵入資料的方法:
public async Task Seed()
{
if (!_ctx.Cities.Any())
{
// 加入資料
_ctx.AddRange(_cities);
await _ctx.SaveChangesAsync();
}
}
使用非同步建立該方法,首先程式先以 if (!_ctx.Cities.Any())
檢查 Cities 資料表內是否包含有任何記錄(Record),因為餵資料只要餵一次(也只能餵一次)就可以了,所以只有在 Cities 資料表中不存在任何記錄時才可以使用 _ctx.AddRange(_cities);
加入整批次的資料,其中 _cities 是縣市、鄉鎮區的實際資料,等一下會再建立。加入資料後,最後以 await _ctx.SaveChangesAsync();
非同步的方式將資料回存到資料庫中。
接著再加入底下的程式碼(略掉大部分縣市,只以兩個為代表),將中華民國所以縣市(包含其中的鄉鎮區)以先前建立領域物件時 City 格式整理,只是做苦工整理而已(苦工就讓阿源哥哥來做,以後會開放讓讀者下載使用)
List<City> _cities = new List<City>
{
new City { Name = "基隆市", Areas=
{
new Area{Name="仁愛區"},
new Area{Name="信義區"},
new Area{Name="中正區"},
new Area{Name="中山區"},
new Area{Name="安樂區"},
new Area{Name="暖暖區"},
new Area{Name="七堵區"}
} },
new City { Name = "台北市", Areas=
{
new Area{Name="中正區"},
new Area{Name="大同區"},
new Area{Name="中山區"},
new Area{Name="松山區"},
new Area{Name="大安區"},
new Area{Name="萬華區"},
new Area{Name="信義區"},
new Area{Name="士林區"},
new Area{Name="北投區"},
new Area{Name="內湖區"},
new Area{Name="南港區"},
new Area{Name="文山區"}
} },
.....
.....
.....
};
註冊資料庫初始化類別
資料庫初始化類別既以實作完畢,接下來就是在 Demae.Api 專案的 Startup.cs 中的 ConfigureServices() 中註冊服務,程式碼和加入位置如下所示:
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
var connection = @"Server=keigen;Database=DemaeDb;Integrated Security=True;";
services.AddDbContext<DemaeContext>(options => options.UseSqlServer(connection));
services.AddTransient<DemaeDbInitializer>();
services.AddMvc();
}
其中 services.AddTransient
為加入的程式碼,註冊 DemaeDbInitializer
這項服務。
在 ASP.NET Core 中註冊服務有許多方式,這次是以 AddTransient 方式,讀者暫且照著做,隨後會再以專篇解釋。
接著同樣在 Startup.cs 中的 Configular() 中使用該餵資料的方法,程式碼和加入位置如下所示:
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
DemaeDbInitializer seeder)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMvc();
seeder.Seed().Wait();
}
一切準備就緒,將 Demae.WebApi 專案設定為啟始專案:
執行 Demae.WebApi 專案,即可將縣市、鄉鎮區資料餵入資料庫中儲存:
有時事情並不會像想的那樣順利進行
原本設想應該會利執行並將縣市、鄉鎮區資料餵入資料庫中才對,但是程式跑到建立 City 陣列物件是就發生例外暫停了:
事出必有因,不必慌張,將例外狀況的詳細資料複製起來並貼到記事本,再來由說明內容來判斷問題所在:
發生 System.NullReferenceException
HResult=0x80004003
Message=Object reference not set to an instance of an object.
Source=<無法評估例外狀況來源>
StackTrace:
at Demae.Data.DemaeDbInitializer..ctor(DemaeContext ctx) in C:\MyBooks\Xamarin.Forms\Demae\Demae.Data\DemaeDbInitializer.cs:line 29
由上述說明看來,應該是 City 物件裡的 Areas 物件執行個體未被建立才對,所以打開 Demae.Domain 專案內的 City.cs 類別加入一段程式碼,如下所示:
public class City
{
public City()
{
Areas = new HashSet<Area>();
}
public int Id { get; set; }
[Required]
[StringLength(maximumLength:5,MinimumLength =3)]
public string Name { get; set; }
public virtual ICollection<Area> Areas { get; set; }
}
在 City 類別的建構函式中加入 Areas = new HashSet<Area>(); 建立 Areas 物件執行個體,加入後再執行程式,執行完後,開啟 Microsoft SQL Server Management Studio 查看 Cities 資料表,果然已被塞入縣市資料:
同樣地,查看 Areas 資料表也是已經被塞入了鄉鎮區的資料:
在應用程式設計開發過程當中,類似先前這種錯誤的發生是常有的事,只要找到問題點解決就好,等到經驗累積發生錯誤的次數將會減少。這次這個錯誤是阿源哥哥故意埋在這裡的,用意是要加強讀者的記憶,並回去複習一下設計領域物件那一篇文章。往後的系列文章也會偶而有意無意的埋入錯誤並說明如何處理,以增加學習成效。
好吧!今天就學習到這裡,明天再繼續加油了。