C#.NET 實體與資料庫表的設計思路
一個合格的專案,最基本之一就是合理的設計資料表與實體類,有些剛剛入門,很難理清資料之間的關係,使得資料表設計得非常不合理;So,我也總結一下,記錄下來,也希望對有迷惑的朋友有幫助。
首先,是Model類的設計(ORM使用了EF,Codefirst模式生成的資料表,這裡為了方便, 遵循了EF命名約定,就不用再去寫配置了);
1、Room(學生宿舍)
public class Room { [Key] public int Id { get; set; } public string Number { get; set; } public string Building { get; set; } public int MaxPerson { get; set; } public virtual ICollection<Student> Persons { get; set; } }
2、Student(學生)
public class Student { [Key] public int Id { get; set; } public string Number { get; set; } public string Name { get; set; } /// <summary> /// 是否住校 /// </summary> public bool UseRoom { get; set; } = true; public int? RoomId { get; set; } public Room Room { get; set; } }
3、Product(商品,說明:為了講解,沒有電商SKU的概念了,就先把他看成一個實際的商品)
public class Product { [Key] public int Id { get; set; } public string Number { get; set; } public string Name { get; set; } public double Weight { get; set; } public decimal Price { get; set; } /// <summary> /// 生產日期 /// </summary> public DateTime ProductDate { get; set; } /// <summary> /// 有效期至 /// </summary> public DateTime ValidDate { get; set; } }
4、Order(訂單)
public class Order { [Key] public int Id { get; set; } /// <summary> /// 訂單編號,業務主鍵,唯一性 /// </summary> public string Number { get; set; } public int BuyerId { get; set; } /// <summary> /// 訂單的客戶資訊 /// </summary> public Student Buyer { get; set; } /// <summary> /// 住宿舍的話,Buyer.Room.Building+Buyer.Room.Number,如A棟110號房間;否則手動輸入 /// </summary> public string BuyerAddress{get;set;} /// <summary> /// 訂單的商品資訊 /// </summary> public virtual ICollection<OrderItem> ProductItems { get; set; } /// <summary> /// 訂單的總價 /// </summary> public decimal Account { get; set; } }
5、OrderItem(訂單明細)
public class OrderItem { [Key] public int Id { get; set; } /// <summary> /// 屬於哪一個訂單,注意:是一個,與訂單的關係是1:N,而且是必須屬於某個訂單 /// </summary> public Order Order { get; set; } public int OrderId { get; set; } public int ProductId { get; set; } /// <summary> /// 商品 /// </summary> public Product Product { get; set; } /// <summary> /// 數量 /// </summary> public int Qty { get; set; } /// <summary> /// 單價,為什麼用又拿一個欄位出來記錄? /// 1、商品的價格是變動的,而客戶下單時的價格是按'現在的商品價格'計算; /// 2、個人的話說,訂單的價格具有時效性 /// 已此可理解了,Order裡客戶的地址欄位也是同理 /// </summary> public decimal Price { get; set; } }
整理一下關係:
Student與Room:一般現實情況是,一個學生只能住一個宿舍,反過來,一個宿舍可以住多個學生;但有這種情況,有的學生家比較近,不住宿舍,所以:一個學生可以有且最多有1個宿舍(高中數學的味道了),關係為0:N;
Order與OrderItem:訂單明細必須屬於一個訂單,不可能脫離訂單存在,其實他們是一個整體,但如果把他們的欄位放到一張表裡面,想想有多少冗餘資料啊,所以一定是1:N;
Student與Order:首先,我這裡模擬的是校園內學生點餐場景,所以把學生當做客戶。一個訂單,必須有客戶,且只有一個客戶,反過來,客戶可以下多個訂單,一般的資料表設計,你用我,你就記錄我,這是外來鍵的通俗理解,所以也是1:N;
OrderItem與Product:一般的,一條訂單明細代表一件商品,訂單明細必須指定商品,1:N;
再來看看對應資料表的設計
說明:
1、Id主鍵、Number編碼(編號);
2、OrderItem的Price就是Product的Price資料,下訂單就是商品當前的價格,就是按當前價格計算總價,比如你買了2件商品,3天到貨,不滿意、退一件,這時候這件商品的價格已經發生變化,不可能按現在的價格退,是吧。還是按買的時候的價格來退的,所以這個不是冗餘欄位,是業務場景下必須的,同理還有Order裡的BuyerAddress;
3、個人不推薦用外來鍵,所以把EF自動生成的欄位、外來鍵等刪了;不影響導航屬性的對映,上面說了,遵循了EF的命名規範。據瞭解,大系統一般是不使用物理外來鍵的,而是提到程式程式碼中去驗證;提交之前去驗證導航屬性,不僅僅是驗證外來鍵是否存在,還有業務邏輯上的,比如場景:B員工填寫一個派車申請單,選擇的車輛的狀態為正常的A1234車(業務需求是隻能選正常車哦),估計1分鐘之後提交,在選擇了車之後與提交之前的這一分鐘裡,A1234車的司機把車輛的狀態改為故障,之後B員工提交上來的派車申請單,也就不僅僅外來鍵驗證資料的問題了;這樣的業務還是需要設計程式人員多多思考的;
現在,來個控制檯新增資料測試吧
static void Main(string[] args) { MyDbContext dbContext = new MyDbContext(); //Room r = new Room { Number = "101", Building = "A棟", MaxPerson = 8 }; //dbContext.Room.Add(r); //dbContext.SaveChanges(); Room r = dbContext.Room.FirstOrDefault(); #region Student Add //List<Student> stuList = new List<Student>(); //for (int i = 0; i < 5; i++) //{ // stuList.Add(new Student { Name = "Jack" + i, Number = DateTime.Now.GetHashCode() + "_" + i, RoomId = r.Id }); //} //dbContext.Student.AddRange(stuList); //dbContext.SaveChanges(); #endregion #region Product Add //List<Product> products = new List<Product> { // new Product { Name="西瓜三明治", Number="smz001", Price=3, ProductDate=DateTime.Now, ValidDate=DateTime.Now.AddDays(7), Weight=0.2 }, // new Product { Name="雞腿漢堡", Number="hb001", Price=5, ProductDate=DateTime.Now, ValidDate=DateTime.Now.AddDays(7), Weight=0.3 }, // new Product { Name="八寶粥", Number="bbz001", Price=6, ProductDate=DateTime.Now, ValidDate=DateTime.Now.AddDays(7), Weight=0.8 }, // new Product { Name="蛋炒飯", Number="dcf001", Price=8, ProductDate=DateTime.Now, ValidDate=DateTime.Now.AddDays(7), Weight=1.2 }, //}; //dbContext.Product.AddRange(products); #endregion #region 模擬購物訂單 using (var tran = dbContext.Database.BeginTransaction()) { try { //客戶 Jack2 Student buyer = dbContext.Student.Where(s => s.Name == "Jack2").FirstOrDefault(); //買 八寶粥1份、蛋炒飯2份 Product[] items = dbContext.Product.Where(a => a.Name == "八寶粥" || a.Name == "蛋炒飯").ToArray(); Order order = new Order { BuyerId = buyer.Id, //校園點餐,不住宿舍的,手輸送貨地點(校園內的) BuyerAddress = buyer.UseRoom ? buyer.Room.Building + buyer.Room.Number : "籃球場第1個垃圾桶旁", Number = DateTime.Now.GetHashCode() + "-" + buyer.Id, ProductItems = new List<OrderItem> { new OrderItem { ProductId=items[0].Id, Price=items[0].Price, Qty=1}, new OrderItem { ProductId=items[1].Id, Price=items[1].Price, Qty=2}, }, //訂單合計 Account = items[0].Price * 1 + items[1].Price * 2, }; order = dbContext.Order.Add(order); dbContext.SaveChanges(); Console.WriteLine(order.Id); tran.Commit(); } catch (Exception ex) { tran.Rollback(); throw ex; } } #endregion }
資料庫表效果
Ok,基本上的思路就是這樣。