在資料庫模型設計中,最基本的實體關係有三種:一對一、一對多、多對多。關於一對多和多對多使用的情況較多,之前也有過一些討論,現在來說明一下在資料庫中一對一的模型設計。
首先,關聯式資料庫中使用外來鍵來表示一對多,使用中間表和兩邊的外來鍵來表示多對多,而一對一的話有三種表示方式:一種是使用相同的主鍵值,第二種是使用單邊的外來鍵,第三種就是使用雙邊外來鍵。
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