Database-Design

在訂單表中儲存帳單地址最佳實踐

  • October 31, 2021

有人可以幫我理解這個使用者對CustomerLocation表的回答嗎?我真的想要一個在訂單表中儲存地址的好方法。

我正在尋找的是如何設置我的地址,因此當我編輯它們時,訂單不受客戶更新他的地址或搬遷的事實的影響。

就目前而言,我的架構看起來類似於:

Person           |EntityID|
EntityAddress    |EntityID|AddressID|
Address          |AddressID|AddressType|AddressLine1|AddressLine2|
Order            |OrderID|BillingAddressID|

從概念上講,儘管在您的業務環境中OrderAddress是密切相關的概念,但它們實際上是兩個獨立的實體類型,每個實體類型都有自己的一組適用的屬性(或屬性)和約束。

因此,正如前面在評論中所說,我同意@Erik,您應該組織數據庫的邏輯佈局,在其他元素中聲明:

  • 一個離散的表來保存地址資訊;
  • 一張表來保留客戶特定的詳細資訊;
  • 一張表格,包含訂單數據點;和
  • *一張表,其中包含有關Customer(s)Address(es)*之間關聯的事實;

正如我將在下面舉例說明的那樣。

說明性IDEF1X圖

一張圖片勝過千言萬語,所以我創建瞭如圖 1所示的 IDEF1X 圖表來說明我的建議帶來的一些可能性:

圖 1 - 客戶、訂單和地址說明 IDEF1X 圖

客戶,地址及其關聯

如圖所示,我描繪了實體類型Customer aAddress之間的多對多 (M:N) 基數比的關聯;這種方法將提供未來的靈活性,因為如您所知,客戶可以隨著時間的推移甚至同時保留多個地址,並且多個**客戶可以共享同一個地址

一對多 (1:M)客戶可以通過多種方式使用特定地址;例如,它可以定義為Physical,和/或可以設置為Shipping和/或Billing。也許,同一個Address實例可以同時服務於上述每個目的,或者它可能涵蓋兩種用途,而不同的Address出現涵蓋其餘用途。


a在某些業務環境中,客戶可以是個人組織(這種情況意味著安排略有不同,如本答案中有關超類型-子類型結構的詳細說明),但為了提供一個簡化的範例,我決定這裡不包括這種可能性。如果您需要在數據庫中涵蓋這種情況,之前連結的文章顯示了解決上述要求的方法。


訂單地址客戶地址和地址角色**

通常,一個訂單只需要兩種地址,一種用於運輸,一種用於計費。這樣,同一個Address實例可以為單個Order填充兩個**Roles,但每個Role由各自的屬性描繪,即ShippingAddressIdBillingAddressId

訂單通過CustomerAddress關聯實體類型在兩個多屬性 FOREIGN KEY 的幫助下與Address連接,即

  • ( CustomerNumber , ShippingAddressId ), 和 ( CustomerNumber , BillingAddressId ),

都指向CustomerAddress多屬性 PRIMARY KEY 顯示為

  • 客戶編號,地址 ID

… 這有助於表示一個業務規則,該規則規定 (a)訂單實例必須與 (b)先前與下達該訂單的特定客戶相關聯的地址事件僅與(c) 隨機的非客戶相關聯-相關地址

(1)地址和 (2) CustomerAddress關聯的歷史記錄

如果您想提供修改地址資訊的可能性,那麼您必須跟踪所有數據更改。以這種方式,我將Address描述為一個“可審計”的實體類型,它維護自己的AddressHistory

由於客戶地址之間的連接的性質也可能遭受一次或多次修改,因此我還描述了通過CustomerAddressHistory實體類型將這種關聯處理為“可審計”關聯的可能性。

在這方面, Q & A no.中處理了各種因素。1問答沒有。2,——兩者都關於在數據庫中啟用時間能力——確實相關。

說明性 SQL-DDL 邏輯佈局

因此,就上面顯示和解釋的圖表而言,我聲明了以下邏輯級別的安排(您可以對其進行調整以準確地滿足您的需求):

-- You should determine which are the most fitting 
-- data types and sizes for all your table columns 
-- depending on your business context characteristics.

-- Also, you should make accurate tests to define the 
-- most convenient INDEX strategies based on the exact 
-- data manipulation tendencies of your business domain.

-- As one would expect, you are free to utilize 
-- your preferred (or required) naming conventions. 

CREATE TABLE Customer (
   CustomerNumber      INT      NOT NULL,
   SpecificAttribute   CHAR(30) NOT NULL,
   ParticularAttribute CHAR(30) NOT NULL,  
   CreatedDateTime     DATETIME NOT NULL,
   -- 
   CONSTRAINT Customer_PK PRIMARY KEY (CustomerNumber)
);

CREATE TABLE Address (
   AddressId           INT      NOT NULL,
   SpecificAttribute   CHAR(30) NOT NULL,
   ParticularAttribute CHAR(30) NOT NULL,  
   CreatedDateTime     DATETIME NOT NULL,  
   -- 
   CONSTRAINT Address_PK PRIMARY KEY (AddressId)
);

CREATE TABLE CustomerAddress (
   CustomerNumber  INT      NOT NULL,  
   AddressId       INT      NOT NULL,
   IsPhysical      BIT      NOT NULL,
   IsShipping      BIT      NOT NULL,  
   IsBilling       BIT      NOT NULL,
   IsActive        BIT      NOT NULL,
   CreatedDateTime DATETIME NOT NULL,  
   -- 
   CONSTRAINT CustomerAddress_PK           PRIMARY KEY (CustomerNumber, AddressId),
   CONSTRAINT CustomerAddressToCustomer_FK FOREIGN KEY (CustomerNumber)
       REFERENCES Customer (CustomerNumber),
   CONSTRAINT CustomerAddressToAddress_FK  FOREIGN KEY (AddressId)
       REFERENCES Address  (AddressId)  
);

CREATE TABLE MyOrder (
   CustomerNumber      INT      NOT NULL,  
   OrderNumber         INT      NOT NULL,
   ShippingAddressId   INT      NOT NULL,
   BillingAddressId    INT      NOT NULL,    
   SpecificAttribute   CHAR(30) NOT NULL,
   ParticularAttribute CHAR(30) NOT NULL,  
   OrderDate           DATE     NOT NULL,
   CreatedDateTime     DATETIME NOT NULL,  
   -- 
   CONSTRAINT Order_PK                  PRIMARY KEY (CustomerNumber, OrderNumber),
   CONSTRAINT OrderToCustomer_FK        FOREIGN KEY (CustomerNumber)
       REFERENCES Customer        (CustomerNumber),
   CONSTRAINT OrderToShippingAddress_FK FOREIGN KEY (CustomerNumber, ShippingAddressId)
       REFERENCES CustomerAddress (CustomerNumber, AddressId),
   CONSTRAINT OrderToBillingAddress_FK  FOREIGN KEY (CustomerNumber, BillingAddressId)
       REFERENCES CustomerAddress (CustomerNumber, AddressId)          
);

CREATE TABLE AddressHistory (
   AddressId           INT      NOT NULL,
   AuditedDateTime     DATETIME NOT NULL,
   SpecificAttribute   CHAR(30) NOT NULL,
   ParticularAttribute CHAR(30) NOT NULL,  
   CreatedDateTime     DATETIME NOT NULL,  
   -- 
   CONSTRAINT AddressHistory_PK          PRIMARY KEY (AddressId, AuditedDateTime),
   CONSTRAINT AddressHistoryToAddress_FK FOREIGN KEY (AddressId)
       REFERENCES Address  (AddressId)    
);

CREATE TABLE CustomerAddressHistory (
   CustomerNumber  INT      NOT NULL,  
   AddressId       INT      NOT NULL,
   AuditedDateTime DATETIME NOT NULL,    
   IsPhysical      BIT      NOT NULL,
   IsShipping      BIT      NOT NULL,  
   IsBilling       BIT      NOT NULL,
   IsActive        BIT      NOT NULL,
   CreatedDateTime DATETIME NOT NULL,  
   -- 
   CONSTRAINT CustomerAddressHistory_PK                  PRIMARY KEY (CustomerNumber, AddressId, AuditedDateTime),
   CONSTRAINT CustomerAddressHistoryToCustomerAddress_FK FOREIGN KEY (CustomerNumber, AddressId)
       REFERENCES CustomerAddress (CustomerNumber, AddressId)
);

如果您想看一下,我在 SQL Server 2017 上執行的這個 db<>fiddle中對其進行了測試。

History桌子_

您的問題的以下摘錄非常重要:

我正在尋找的是如何設置我的地址,這樣當我編輯它們時,訂單不會受到客戶更新地址或搬遷的影響。

和表有助於確保訂單不受地址AddressHistory更改的影響,因為所有“以前的”行都應保留在相應的表中,並可能在必要時參與 SELECT 查詢。CustomerAddressHistory``History

區間包含在包含的值之間,AddressHistory.CreatedDateTime代表AddressHistory.AuditedDateTime整個“過去”行被視為“目前”、“目前”或“有效”的整個期間。Address類似的考慮適用於CustomerAddressHistory行。

反過來,CustomerAddress.IsActiveBIT(布爾)列是為了指出某一Address行是否可以被一行“使用” Customer;例如,如果它設置為“假”,它將傳達客戶不再使用該地址的情況,因此它不能用於新訂單

數據操縱因素

修改和擦除

如果客戶想要更改有關給定“目前”**地址的一條或多條資訊,則必須確保(a)Address在修改發生之前“存在”的相應行被插入到AddressHistory表中,並且( b) 有Address問題的行用新值更新。我建議您將此過程作為單個ACID TRANSACTION中的單個工作單元來執行。

應該禁止對兩個表進行 UPDATE 操作History(試圖“更改”歷史甚至可能會產生負面的法律影響),因為保留在那種“類型”表中的每一行都代表過去發生的事實,因此它不能被修改和關於它的一行不應該被改變。

至於這些表上的 DELETE,在某些非常特殊的情況下,必須啟用它們才能遵守例如保密法/政策——但是,像往常一樣,這完全取決於重要業務環境的確切要求——。

檢索和物理級調整

地址出現的“目前”、“目前”或“有效”版本必須作為一行包含在表中,但是從(或來自)表中選擇地址Address的先前“狀態”很容易,並且可能是一個有趣的練習來提高你的 SQL 編碼技能。AddressHistory``CustomerAddressHistory

關於您在評論中提到的一種情況,如果您想Address從其檢索單個行的“倒數第二個版本” AddressHistory,您必須考慮MAX(AddressHistory.AuditedDateTime)與手頭AddressHistory.AddressId的特定Address.AddressId值匹配的 和 。

在這方面——至少在建構關係數據庫時——首先定義相應的概念模式(基於適用的業務規則)然後聲明其後續的邏輯DDL 安排是非常方便的。一旦您獲得了這些基本元素的穩定可靠版本(當然,它們會隨著時間的推移而發展),就該分析和確定最佳的操作方式(通過 INSERT、UPDATE、DELETE 和 SELECT 操作或其組合)關於數據,注意優化相應的物理級處理(例如,通過索引調整、硬體升級、適當的軟體設置等)。

最終使用者感知、觀點和應用程序幫助

顯然,在抽象的外部級別,地址資訊(最終使用者)被認為是訂單的一部分——這並沒有錯——但這並不一定意味著建模者必須設計重要部分與所述感知相對應的相關數據庫。在這一點上,如果需要,例如,列印一個“完整”訂單(非常可行),您可以在一些 JOIN 運算符和 WHERE 子句的幫助下按需“複製”它(考慮到相關的有效期等)可能會固定在視圖中以供將來使用,將相關數據集發送到相關應用程序,進而可以根據需要增強其格式。

當然,在訂單生效時,應用程序也會非常有幫助;例如,桌面/移動應用程序視窗或網頁可以:

  • 僅顯示相關客戶已確定為“可用”的地址(通過);CustomerAddress.IsActive
  • 列出客戶為計費服務啟用的所有地址(通過*)*;和CustomerAddress.IsBilling
  • 客戶為運輸服務定義的所有地址分組(通過);CustomerAddress.IsShipping

以這種方式促進 GUI 上所有涉及的過程(這自然是電腦化系統的外部抽象級別的一部分)。


選擇

另一方面,我見過一些系統,其中每次執行新訂單時都必須輸入相關的地址資訊(有時重複),並且用於過去訂單的**地址永遠不會被刪除,因此訂單不受地址更改的影響。

這一行動過程肯定會涉及大量的數據重複,但根據您的業務領域的確切資訊要求,它有可能起作用,因此您可能還想評估它的優缺點。


評論互動

你能解釋一下我們需要Address實體的原因嗎?有還不夠CustomerAddress嗎?—— @克里斯蒂亞諾3 月 11 日 19:38:05Z

在這種特定情況下,原始發帖人(現在已刪除帳戶)提出了一個場景,其中包含一個包含四個感興趣的離散屬性的地址實體類型,即AddressIDAddressTypeAddressLine1AddressLine2(如問題本身所示),每個其中 (a) 具有一組特定的有效值,(b) 涉及一組潛在的排他性約束,並且 (c) 意味著必須單獨操作的值。此外,(d) 任何給定的地址實例——即正確的地址實體——可能與n次出現的其他類型的實體相關聯;因此, (e)地址本身就是一個實體類型。

很可能還有其他情況,根據確切的業務需求,應將Customer.Address視為單個屬性,在這種情況下,必須在抽象的邏輯級別以原子方式處理其值,即您始終操縱每個地址值作為一個整體,永遠不要僅對某些值部分執行操作(插入、更新、刪除、選擇)。當然,如果需要,應該首先為相應的原子Customer.Address列設置約束,因為這些對於列“分數”來說不是必需的。其中一種情況可能是上一節中討論的題為“替代方案”的場景。

推薦閱讀

您要求(在現已刪除的評論中)一些關於健全數據庫文獻的指針;因此,至於理論材料,我強烈建議您閱讀EF Codd 博士的所有著作,他是圖靈獎獲得者,當然也是數據關係模型的唯一創始人*(*現在可能比以往任何時候都更相關)。這份清單包括他的一些極具影響力的文章和論文。

不包含在上述列表中的兩部重要著作,正是他 1981 年的 ACM 圖靈獎講座題為《關係數據庫:生產力的實用基礎》,以及他的著作《數據庫管理的關係模型:第 2 版》,已出版1990 年。

概念設計方面,資訊建模的集成定義(IDEF1X) 是一項非常值得推薦的技術,它於 1993 年 12 月被美國國家標準與技術研究院(NIST) 定義為標準。

引用自:https://dba.stackexchange.com/questions/111101