簡單五步驟:以EF Core整合SQLite儲存口罩剩餘數量資訊
前篇,我們展示使用 ASP.NET Core Web API 來快速提供口罩剩餘數量查詢API,但這個展示專案有二個較大缺點,我們持續來改進它。
- 原始資料採用 Unicode 編碼,尤其是中文欄位,這會造成傳輸資料過程的資料量不小。
 - 我們只是單純轉拋政府資料開放平臺的口罩剩餘數量的資料,每次請求就重新請求與轉拋,它們的資料更新並不即時。
 
我們會先處理第二點,這裡我選擇利用 EF Core 整 SQLite 來當暫存資料的儲存點,因為原始資料約 6000 筆上下,這樣的處理量 SQLite 非常合適,而且它的特色是帶著就走且免費,發行部署也相當方便。在未超過更新間隔時前,就由 SQLite 裡去取資料,以減輕資料源的壓力。在處理第二點時,會隨便處理第一點,在取回政府資料開放平臺的口罩剩餘數量資料並儲存前,我們利用 Model 裡的一點小技巧來轉換資料源到英文欄位,並以英文欄位來輸出。
安裝 SQLite
EF Core 直接支援 SQLite。
# PowerShell
Install-Package Microsoft.EntityFrameworkCore.Sqlite -Version 3.1.2
# .NET Core
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 3.1.2
建立 Model
依照中文欄位名稱,我們簡單翻譯為英文欄位名稱來建立 Model。
public class MedicalMask
{
    public int Id { get; set; }
    public string Code { get; set; }
    public string Name { get; set; }
    public string Address { get; set; }
    public string Tel { get; set; }
    public int AdultNum { get; set; }
    public int KidNum { get; set; }
    public DateTime UpdateTime { get; set; }
}
建立 MaskContext
繼承 DbContext 建立 MaskContext:
public class MaskContext : DbContext
{
    public DbSet<MedicalMask> MedicalMasks { get; set; }
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlite("Data Source=MedicalMask.db");
}
先 override OnConfiguring 設定使用 SQLite 並指定資料庫名稱,並設定 DbSet<MedicalMask> 以存取資料表。完成後,將 MaskContext 加入 Startup.ConfigureServices() 裡:
services.AddScoped<MaskContext>();
進行 Migration
開啟 cmd.exe 執行以下指令:
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet ef migrations add Initial
dotnet ef database update
如果你未安裝過 dotnet-ef 全域工具,才需要安裝它。之後會產生 Migrations 相關檔案與資料庫在專案根目錄。
改寫 MaskController
public class MaskController : ControllerBase
{
    private readonly MaskContext _maskContext;
    private readonly MaskService _maskService;
    public MaskController(MaskContext maskContext, MaskService maskService)
    {
        _maskContext = maskContext;
        _maskService = maskService;
    }
    public async Task<IActionResult> Get()
    {
        async Task InitialDB(MedicalMask medical)
        {
            if (medical == null)
            {
                var maskInfos = await _maskService.GetMaskInfo();
                foreach (var maskInfo in maskInfos)
                {
                    _maskContext.Add(new MedicalMask()
                    {
                        Code = maskInfo.醫事機構代碼,
                        Name = maskInfo.醫事機構名稱,
                        Address = maskInfo.醫事機構地址,
                        Tel = maskInfo.醫事機構電話,
                        AdultNum = int.Parse(maskInfo.成人口罩剩餘數),
                        KidNum = int.Parse(maskInfo.兒童口罩剩餘數),
                        UpdateTime = DateTime.Parse(maskInfo.來源資料時間)
                    });
                }
                await _maskContext.SaveChangesAsync();
            }
        }
        async Task MaskDataUpdate()
        {
            var nextTime = _maskContext.MedicalMasks.First().UpdateTime.AddSeconds(600);
            var nowTime = DateTime.Now;
            if (nextTime < nowTime)
            {
                _maskContext.RemoveRange(_maskContext.MedicalMasks);
                var maskInfos = await _maskService.GetMaskInfo();
                foreach (var maskInfo in maskInfos)
                {
                    _maskContext.Add(new MedicalMask()
                    {
                        Code = maskInfo.醫事機構代碼,
                        Name = maskInfo.醫事機構名稱,
                        Address = maskInfo.醫事機構地址,
                        Tel = maskInfo.醫事機構電話,
                        AdultNum = int.Parse(maskInfo.成人口罩剩餘數),
                        KidNum = int.Parse(maskInfo.兒童口罩剩餘數),
                        UpdateTime = DateTime.Parse(maskInfo.來源資料時間)
                    });
                }
                await _maskContext.SaveChangesAsync();
            }
        }
        try
        {
            var medical = _maskContext.MedicalMasks.FirstOrDefault();
            await InitialDB(medical);
            await MaskDataUpdate();
            return Ok(_maskContext.MedicalMasks.AsNoTracking());
        }
        catch (HttpRequestException ex)
        {
            return Problem(ex.Message);
        }
    }
}
這一次我們加上 MaskContext。流程上有二段,第一次 Migration 與 Database Update 之後,資料庫為空,我們必須執行 InitialDb 來進行第一次的請求與寫入資料庫,為了不過度複雜範例,就不採用 SeedData 方式。第二次,MaskDataUpdate 會先判斷時間,確認是否需要更新資料庫,Model 裡的 Id 只是為了 EF Core 的 Migration 加上去的,對我們而言沒有用。所以我偷懶將整個表格刪除再全部重塞一次。
另外測試過程發現,政府資料開放平臺的口罩剩餘數量的資料並未如網頁上所寫 30 秒更新一次。像中午或晚上時段未更新資料的情況。我們修正一下判斷式:
if (updateData)
{
    var maskInfos = await _maskService.GetMaskInfo();
    var sourceTime = DateTime.Parse(maskInfos.First().來源資料時間);
    // 0 is same as value
    // Ref: https://docs.microsoft.com/en-us/dotnet/api/system.datetime.compareto?view=netcore-3.1#remarks
    if (sourceTime.CompareTo(updateTime) != 0)
    {
        _maskContext.RemoveRange(_maskContext.MedicalMasks);
        foreach (var maskInfo in maskInfos)
        {
            _maskContext.Add(new MedicalMask()
            {
                Code = maskInfo.醫事機構代碼,
                Name = maskInfo.醫事機構名稱,
                Address = maskInfo.醫事機構地址,
                Tel = maskInfo.醫事機構電話,
                AdultNum = int.Parse(maskInfo.成人口罩剩餘數),
                KidNum = int.Parse(maskInfo.兒童口罩剩餘數),
                UpdateTime = DateTime.Parse(maskInfo.來源資料時間)
            });
        }
        await _maskContext.SaveChangesAsync();
    }
}
做為展示,設定 10 鐘會更新一次,但更新前再確認一下資料來源的時間是否有異動,有才進行刪除與重塞作業。
小結
- 安裝 SQLite
 - 建立 Model
 - 建立 DbContext
 - 進行 Migration
 - 改寫 MaskController
 
簡單五步驟,我們就能把 SQLite 資料庫整合進來,並且整合 EF Core 來進行存取操作。

沒有留言:
張貼留言
感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。