再談資料庫移轉

上個星期五,我們第一次使用資料庫移轉將領域物件對映(Mapping)產生資料表,今天就來看看兩者的關聯對應,以及是否有需要改進的地方。

物件關聯對映

首先來看看先前的 DemaeContext 物件中的三個屬性:

  • public DbSet<City> Cities { get; set; }
  • public DbSet<Area> Areas { get; set; }
  • public DbSet<Address> Addresses { get; set; }

分別對映成三個資料表:

  • Cities
  • Areas
  • Addresses
public class DemaeContext : DbContext
{
    public DemaeContext(DbContextOptions<DemaeContext> options) 
        : base(options) { }           
        
    public DbSet<City> Cities { get; set; }
    public DbSet<Area> Areas { get; set; }
    public DbSet<Address> Addresses { get; set; }
}

接著如圖針對個別的資料表開啟【設計】模式,看看資料表內各個資料行名稱和資料類型與領域物件內的各個屬性間的對映關係:

以 City 領域物件為例,如下圖所示,命名為 Id 的屬性(Property)會對映為資料表的主索引鍵(Primary Key),且屬性名稱會對映成同名的資料行名稱,而物件的屬性型別也與資料表的資料類型有相對應關係,例如 string 型別會對映成 nvarchar(MAX) 資料類型。主索引鍵的型別如果為 int 也會被對映成增量為 1 的識別規格(也就是會按順序 1、2 、3、4 ........ 自動下去編號)。

註:
近期的一些程式設計模式通常會遵循一種稱為《約定優於配置(Convention over Configuration)》的規範,例如目前的例子,領域物件的命名只要遵守既定的原則(約定),就會自動對映成資料表名稱,當然如果不喜歡這樣的約定,也可透過額外的設定(配置)改成自己喜歡的。阿源哥哥是比較怕麻煩的人(隨遇而安的人吧),如果不是有特殊的需求,一律按照原有的命名原則,省去一些設定的麻煩。

相同的原則,其他另外兩個資料表的對映如下:

物件間的關聯 vs 資料表間的關聯

接著如下圖所示,為目前的三個資料表建立【資料庫圖表】:

如下圖所示,請回憶一下先前在建立領域物件時,我們以領域物件間的屬性設計出各個領域物件間的關聯(目前是一對多關聯),而這些關聯對映到資料表是以主索引鍵(Primary Key)與外部索引鍵(Foreign Key)來做關聯。

例如 Cities 和 Areas 兩個資料表間是以主索引鍵 Id 與外部索引鍵 CityId 建立出《一對多》的關聯(一個縣市內有許多的鄉鎮區)。

註:
雖然主索引鍵與外部索引鍵可以隨意命名,但是阿源哥哥習慣(讀者不一定要遵守)會將主索引鍵一律命名為 Id 而外部索引鍵會命名為主索引鍵資料表名稱(取單數英文名) + Id,這樣的命名方式可以很簡單地一眼看出,誰是主索引鍵,誰又是外部索引鍵,誰跟誰又是什麼關係。當然讀者也可以自己採用自己的命名方式。

商業邏輯

在討論商業邏輯之前,請先看一下如下圖所示的 O/R Mapping(物件關聯對映)所產生的資料表,是否覺得有點怪怪的!對的,以 Addresses 資料表為例,其中 AreaId 和 Line 都允許 Null 也就是說,可以允許保持空白,什麼都不填,這樣收集到的地址資訊又有什麼義意呢?有等於沒有,不知道要將貨品外送到哪裡?

先前在設計領域物件時曾經說過:「為解決某領域問題而設計出來的物件」,該物件不只是要有足夠的資訊(物件中的各個屬性)來描述該領域問題,且也要確保各個屬性所收集資料的正確性,這也就是所謂的商業邏輯,所以該為這些領域物件加入一些限制才可以。

public class City
{
    public int Id { get; set; }  
    [Required]
    [StringLength(maximumLength:5,MinimumLength =3)]
    public string Name { get; set; }
    public virtual ICollection<Area> Areas { get; set; }
}


public class Area
{
    public int Id { get; set; }
    [Required]
    [StringLength(maximumLength: 5, MinimumLength = 2)]
    public string Name { get; set; }
    public int CityId { get; set; }
    [ForeignKey("CityId")]
    public City City { get; set; }
}


public class Address
{
    public int Id { get; set; }
    [Required]
    public int AreaId { get; set; }
    [ForeignKey("AreaId")]
    public Area Area { get; set; }
    [Required]
    [StringLength(maximumLength: 40, MinimumLength = 5)]
    public string Line { get; set; }
}

例如在上述的程式碼中加入了 [Required] 限制該屬性為必填欄位,而加入了 [StringLength(maximumLength: 40, MinimumLength = 5)] 限制該屬性的字串長度不可超過某個字元(本案例是 40 個),且不可少於某個字元(本案例是 5 個)。

註:
在所謂的商業邏輯中,當然不會只有限制欄位必填,以及字串長度的長短等簡單的限制,配合商業行為的運作,其他還會有許多更為複雜的商業邏輯,往後有機會會再更加深入地探討這個課題。

Add Migration

請回憶一下,上個星期五曾經討論過的:「在應用程式的設計開發過程當中,一定需要經常修改或增加領域物件(Domain Object)。因此,在開發過程中會經歷無數次的 Add-Migration 和 Update-Database 過程。」這次,因為加入了限制條件,因此改變了資料庫的結構,所以有需要再執行一次 Add-Migration 和 Update-Database 。

請在《套件管理器主控台》的 PM> 下達 Add-Migration AddConstrainToAddressRelativeTables 指令,如下:

PM> Add-Migration AddConstrainToAddressRelativeTables

請回憶一下,在 Add-Migration 指令之後可加入一個字串參數,可用來說明這次執行 Add-Migration 的目的,因此可用一個有意義的字串來說明這次是為了要在與住址有關的資料表加入限制條件。下達指令之後,請按下鍵盤的【enter】鍵。

在按下【enter】鍵之後,同樣地會在 Migrations 資料夾底下產生兩個檔案,如下圖所示:

接著同樣是在《套件管理器主控台》下達 Update-Database 指令,然後按下【enter】,然後經過些許時間後,出現 Done. 字元,表示資料庫已修改完成。

好吧!今天就暫時先學習到這裡,明天再繼續了。