One to One 的資料庫模型設計與NHibernate配置

深藍發表於2013-07-11

在資料庫模型設計中,最基本的實體關係有三種:一對一、一對多、多對多。關於一對多和多對多使用的情況較多,之前也有過一些討論,現在來說明一下在資料庫中一對一的模型設計。

首先,關聯式資料庫中使用外來鍵來表示一對多,使用中間表和兩邊的外來鍵來表示多對多,而一對一的話有三種表示方式:一種是使用相同的主鍵值,第二種是使用單邊的外來鍵,第三種就是使用雙邊外來鍵。

1.主鍵關聯

比如我們在做一個ER系統時,設計了一個Employee表儲存員工的基本資訊(主表),另外有一個EmployeePhoto表(外表),用於儲存員工的證件照,員工和照片之間就是一對一的關係。

public class Employee:Entity
{
    public virtual string EmployeeNumber { get; set; }
    public virtual string Name { get; set; }
    public virtual EmployeePhoto EmployeePhoto { get; set; }
}
public class EmployeePhoto:Entity
{
    /// <summary>
    /// 員工照片,應該是二進位制資料,這裡只是一個例子,為了方便,所以用String型別
    /// </summary>
    public virtual string Photo { get; set; }

    public virtual Employee Employee { get; set; }
}

下面是FluentNHibernate的Mapping配置:

public class EmployeeMap : ClassMap<Employee>
{
    public EmployeeMap()
    {
        Table("EMPLOYEE");
        Id(x => x.Id, "EMPLOYEE_ID").GeneratedBy.HiLo("1000000000");
        Map(x => x.EmployeeNumber, "EMPLOYEE_NUMBER").Not.Nullable();
        Map(x => x.Name, "NAME").Not.Nullable();
        HasOne(x => x.EmployeePhoto).Cascade.All();
    }
}
public class EmployeePhotoMap : ClassMap<EmployeePhoto>
{
    public EmployeePhotoMap()
    {
        Table("EMPLOYEE_PHOTO");
        Id(x => x.Id, "EMPLOYEE_ID").GeneratedBy.Foreign("Employee");
        Map(x => x.Photo, "PHOTO").Not.Nullable();
        HasOne(x => x.Employee).Cascade.None().Constrained();
    }
}

這裡需要注意的是EmployeePhoto的主鍵,不再是普通的生成方式,而是要選擇通過Employee做外來鍵生成。

關於NHibernate 的one to one標籤上的constrained="true",該標籤在外表上設定,千萬不要在主表上設定。就是說明這個表的主鍵與另一個表的主鍵建立外來鍵約束,也就是說在生成SQL指令碼時,會為這個表建立外來鍵,如果不加,是不會建立外來鍵的。另外還有一個作用,就是在查詢外表時,如果沒有設定該屬性,那麼就會Join主表,而設定了該屬性,就只需要查詢外表。

在主鍵關聯的情況下,如果從主表中移除從表的引用,這個時候儲存主表,是不會刪除從表的,也不會刪除這個一對一的關係的。也就是說,我們不能單獨保留Employee和Photo表,同時還要去掉兩者之間的關係。

2.單向外來鍵關聯

比如我們做箇中學的管理系統,設計了一個Class表儲存班級,另一個Classroom表儲存教室,班級和教室是一對一的關係,一個班級有且僅有一個教室,一個教室屬於0到1個班級。

public class Class : Entity,IPermanent
{
    public virtual bool IsDeleted { get; set; }
    public virtual string ClassName { get; set; }
    public virtual int StudentCount { get; set; }
    public override string ToString()
    {
        return "Class Id:" + Id + " Name:" + ClassName;
    }
    public virtual Classroom Classroom { get; set; }
}
public class Classroom:Entity,IPermanent
{
    public virtual string Building { get; set; }
    public virtual string RoomNumber { get; set; }
    public override string ToString()
    {
        return "Classroom[" + Id + "] " + Building + " " + RoomNumber;
    }
    public virtual bool IsDeleted { get; set; }
    public virtual Class Class { get; set; }
}

Mapping的程式碼是:

public class ClassMap : ClassMap<Class>
{
    public ClassMap()
    {
        Table("CLASS");
        Id(x => x.Id, "CLASS_ID").GeneratedBy.HiLo("1000000000");
        Map(x => x.ClassName, "CLASS_NAME").Not.Nullable();
        Map(x => x.StudentCount, "STUDENT_COUNT").Not.Nullable();
        Map(x => x.IsDeleted, "IS_DELETED");
        References(x => x.Classroom, "CLASSROOM_ID").Cascade.All();
        ApplyFilter<IsDeletedFilter>("IS_DELETED = :DeleteFlag");
    }
}
public class ClassroomMap : ClassMap<Classroom>
{
    public ClassroomMap()
    {
        Table("CLASSROOM");
        Id(x => x.Id, "CLASSROOM_ID").GeneratedBy.HiLo("1000000000");
        Map(x => x.Building, "BUILDING");
        Map(x => x.RoomNumber, "ROOM_NUMBER");
        Map(x => x.IsDeleted, "IS_DELETED");
        HasOne(x => x.Class).PropertyRef(r => r.Classroom);
        ApplyFilter<IsDeletedFilter>("IS_DELETED = :DeleteFlag");
    }
}

這裡兩個表中只需要有一個表持有對方的主鍵作為外來鍵即可,我們可以在CLASS表中新增CLASSROOM_ID來作為外來鍵,也可以在CLASSROOM表中新增CLASS表作為外來鍵。選擇哪一個好呢?如果相互之間都對應的是0到1個對方,那麼其實選哪邊都無所謂,但是如果我們假定一個Class必須要對應一個Classroom,而一個Classroom可以對應0到1個Class,那麼我們就必須在CLASS表中新增CLASSROOM_ID,因為我們必須先建立Classroom,然後再建立Class,然後可以在資料庫中將CLASS表中的CLASSROOM_ID設定為不允許為空(當然,設定為允許為空也沒有問題,這樣可以幫助NHibernate在級聯儲存時能夠正確儲存而不報錯)。

單向外來鍵關聯時,如果資料庫允許CLASSROOM_ID為空,那麼是可以打斷Class和Classroom的關係的,而使得這兩個物件獨立存在,這一點是和主鍵關聯所不一樣的地方。

另外,這個配置還存在一個問題,就是對於一個存在的Classroom A,我接下來建立Class X,Class Y,都可以將這些 Class的班級指向A,同時這也是儲存成功的。但是這顯然是不對的,我們需要的是一對一,不是一對多。如果查詢Classroom A的Class屬性,那麼就會報錯,因為根本不知道應該是X還是Y。所以我們需要在CLASS表的CLASSROOM_ID上建立唯一約束,體現在Mapping上就是:

References(x => x.Classroom, "CLASSROOM_ID").Cascade.All().Unique();
這樣我們在儲存X和Y的時候,就只能儲存成功一個,第二個儲存時就會報錯。

這其實又帶來了另外一個問題,這可能是NHibernate沒有考慮到的地方,那就是我們採用的是軟刪除,也就是說根本不會從資料庫刪除資料,只是把IS_DELETED置為1。 那麼,我們如果先儲存了A和X的關係,接下來由於X被取消,所以我刪除了X,接下來新增Y與A關聯就會失敗。所以需要取消唯一約束,就可以儲存Y了,但是在取A的Class屬性時仍然會出現異常,取不出正確的Class Y,這個暫時無解。

3.雙向外來鍵關聯

就是說CLASS表中有CLASSROOM_ID,然後在CLASSROOM表中也有CLASS_ID。這是非常不推薦的方式,一來導致資料維護重複,二來導致資料可能存在不一致。所以,這裡我就不再累述這種方案的實現了。

示例程式碼下載:

http://files.cnblogs.com/studyzy/One2OneTest.7z

 

相關文章