簡單五步驟:以EF Core整合SQLite儲存口罩剩餘數量資訊

簡單五步驟:以EF Core整合SQLite儲存口罩剩餘數量資訊

前篇,我們展示使用 ASP.NET Core Web API 來快速提供口罩剩餘數量查詢API,但這個展示專案有二個較大缺點,我們持續來改進它。

  1. 原始資料採用 Unicode 編碼,尤其是中文欄位,這會造成傳輸資料過程的資料量不小。
  2. 我們只是單純轉拋政府資料開放平臺的口罩剩餘數量的資料,每次請求就重新請求與轉拋,它們的資料更新並不即時。

我們會先處理第二點,這裡我選擇利用 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 鐘會更新一次,但更新前再確認一下資料來源的時間是否有異動,有才進行刪除與重塞作業。

口罩剩餘數量API

小結

  1. 安裝 SQLite
  2. 建立 Model
  3. 建立 DbContext
  4. 進行 Migration
  5. 改寫 MaskController

簡單五步驟,我們就能把 SQLite 資料庫整合進來,並且整合 EF Core 來進行存取操作。

原始碼:QueryMaskSqliteSample

沒有留言:

張貼留言

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