为资金转移业务开发数据库,​​其中 (a) 个人和组织可以 (b) 发送和接收资金

aar*_*nes 8 postgresql database-design subtypes

在相关的业务背景,既成员组织需要有一个帐户资金资金可以转移

  • 会员会员
  • 会员组织
  • 组织组织,以及
  • 组织成员

注意事项

为了为这种场景构建数据库,我创建了以下三个表:

CREATE TABLE Members ( 
  memberid serial primary key, 
  name varchar(50) unique, 
  passwd varchar(32), 
  account integer 
);

CREATE TABLE Organizations (  
  organizationid serial primary key, 
  name varchar(150) unique, 
  administrator integer references Members(memberid), 
  account integer 
);

CREATE TABLE TransferHistory  
  "from" integer, -- foreign key?
  "to" integer, -- foreign key?
  quantity integer 
);
Run Code Online (Sandbox Code Playgroud)

我认为TransferHistory表是必要的,以显示谁/什么基金谁/什么

问题是,由于MembersOrganizations是不同的表,我如何从TransferHistory表中引用它们?

例如,所涉及的数据可以显示如下:

Account      Account      Quantity
-----------  -----------  --------
 1072561733  38574637847       500
38574637847   1072561733       281
Run Code Online (Sandbox Code Playgroud)

这表明帐户需要记录在同一个表中,但帐户是针对两种不同类型的所有者成员组织),每种所有者都保留在各自的表中。

我可以创建一个名为 的表Accounts,所以现在我将有四个表:

CREATE TABLE Members (
  memberid serial primary key, 
  name varchar(50) unique, 
  passwd varchar(32), 
  accountid integer references Accounts(accountid) 
);

CREATE TABLE Organizations (
  organizationid serial primary key, 
  name varchar(150) unique, 
  administrator integer references Members(memberid), 
  accountid integer references Accounts(accountid)
);

CREATE TABLE Accounts ( 
  accountid serial primary key, 
  state integer 
);

CREATE TABLE TransferHistory ( 
  "from" integer references Accounts(accountid), 
  "to" integer references Accounts(accountid), 
  quantity integer 
);
Run Code Online (Sandbox Code Playgroud)

...但现在我必须确保来自MembersOrganizations表的每个外键不指向表中的同一AccountAccounts......

...或者我可以有一个Accounts表有两个外键,一个指向Members另一个指向Organizations(并且一个外键列必须始终包含一个 NULL 标记)。但是现在关于查询的事情变得有点混乱。一般而言,数据库设计如下:

CREATE TABLE Members ( 
  memberid serial primary key, 
  name varchar(50) unique, 
  passwd varchar(32) 
);

CREATE TABLE Organizations (
  organizationid serial primary key, 
  name varchar(150) unique, 
  administrator integer references Members(memberid) 
);

CREATE TABLE Accounts ( 
  accountid serial primary key, 
  member integer references Members(memberid), 
  organization integer references Organizations(organizationid),
  state integer 
);

CREATE TABLE TransferHistory ( 
  "from" integer references Accounts(accountid), 
  "to" integer references Accounts(accountid), 
  quantity integer 
);
Run Code Online (Sandbox Code Playgroud)

那么,有人对如何解决这个问题有任何建议吗?

MDC*_*CCL 13

如果打算构建一个关系数据库,首先执行 (a) 对感兴趣的业务上下文的分析——为了描绘一个概念模式——在实体类型方面,在检查它们的属性关联之前,真的很有帮助(b) 根据表、列和约束——对应于逻辑级别的方面——进行思考。按照这个操作过程,准确地捕获业务领域的含义,然后将其反映在实际的、约束良好的 SQL-DDL 设计中要简单得多。

关系范式提供的众多优势之一是它允许在其自然结构中管理数据;因此,在使用关系工具来管理它之前,必须“找到”这样的结构。所讨论的场景是否与个人项目相关并不重要(正如您通过评论指出的那样):您定义的越现实,您从其开发中学到的东西就越多(如果这就是这项工作的目的)。当然,一个现实的个人项目可能会演变成一个相对较小的改编的商业项目。

商业规则

为了呈现您可能喜欢用作参考的第一个进展,我制定了一些最重要的概念级业务规则,并将它们列举如下:

  • 一个拥有零个或多个帐户
  • 一个Person主要通过它的Id来区分
  • 一个交替由他/她的区分姓氏出生日期性别
  • 一个组织拥有零一或一对多的账户
  • 一个组织主要是由它的区分标识
  • 一个组织交替由其分化名称
  • 一个组织开始在工作FoundingDate
  • 一个帐户转让方在零一或一对多的传输
  • 一个帐户受让方以零一或一对多的传输
  • 一个账户主要是由它的识别号码
  • 一个帐号在一个确切的发行CreatedDate
  • 转让中转让人必须不同于受让人
  • 一个可以通过零或一用户配置文件登录

由于协会-or relationships-(1)之间的账户之间;(2)组织帐户非常相似,这一事实表明,账户是实体亚型(基本上,无论是个人一群人) ,这反过来又是他们的实体超类型。这是一个经典的信息结构,在不同种类的多个概念模型中经常出现。通过这种方式,可以断言两个新规则:

  • 一种 PartyType进行分类零一或一对多的缔约方
  • 一个可以是一个或一个组织

之前的两个业务规则可以合并为一个:

  • 拥有零一或一对多的账户

也可以从账户的角度来表述实体类型:

  • 一个账户实际只由一资

说明性IDEF1X图

因此,我创建了一个说明(简化)IDEF1X 图,它综合了上面制定的规则,如图 1所示:

图 1 - 资金转移 IDEF1X 模型

党、人、组织:超类型-子类型结构

如图所示,Person并且Organization被描述为互斥的的亚型Party

所述Party超型保持鉴别器(即,PartyTypeCode)和所有所共有其亚型,这反过来,具有适用于它们中的每属性的属性(或属性)。

帐户

Account实体类型是直接与连接Party,它提供之间(I)的随后的连接AccountPerson之间,和(ⅱ)AccountOrganization

由于在现实世界中,(a) 银行Account是不可转让的,即它Owner不能更改,并且 (b)Account不能在没有 的情况下开始当前或启用Owner,因此该实体类型的 PRIMARY KEY可能包括属性PartyId AccountNumber,因此您应该更彻底地分析场景以高精度定义这一点。

转移

在另一方面,Transfer实体类型提出了一个复合主键由三个属性,最多也就是TransferorAccountNumberTransfereeAccountNumber角色名称我分配了两个中的每一个区分Account涉及每个属性Transfer和实例)TransferDateTime(它告诉exaxct即时Transfer发生率执行)。

关于帐号的因素

还要注意,在实际的银行系统中,AccountNumber数据点的格式通常比“仅仅是”整数值更复杂。有不同的格式安排,例如,对应于ISO 13616定义的国际银行帐号 (IBAN) 的格式安排标准。这方面显然意味着 (1) 概念分析和后面的 (2) 逻辑定义需要更详尽的方法。

说明性的逻辑 SQL-DDL 声明

然后,作为之前分析的推导,我声明了一个逻辑设计,其中

  • 每张桌子代表一个实体类型,
  • 代表各自实体类型的一个属性,并且
  • 设置了多个约束(以声明方式)以保证所有表中保留的形式的断言符合在概念层确定的业务规则。

我提供了注释作为注释,解释了我认为在上述结构方面特别重要的一些特征,如下所示:

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

-- Also, you should make accurate tests to define the
-- most convenient physical implementation settings; e.g.,
-- a good INDEXing strategy based on query tendencies.

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

CREATE TABLE PartyType (
    PartyTypeCode CHAR(1)  NOT NULL, -- This one is meant to contain the meaningful values 'P', for 'Person', and 'O' for 'Organization'.
    Name          CHAR(30) NOT NULL,
    --
    CONSTRAINT PartyType_PK PRIMARY KEY (PartyTypeCode)
);

CREATE TABLE Party ( -- Represents the supertype.
    PartyId         INT       NOT NULL,
    PartyTypeCode   CHAR(1)   NOT NULL, -- Denotes the subtype discriminator.
    CreatedDateTime TIMESTAMP NOT NULL,  
    Etcetera        CHAR(30)  NOT NULL,  
    --
    CONSTRAINT Party_PK            PRIMARY KEY (PartyId),
    CONSTRAINT PartyToPartyType_FK FOREIGN KEY (PartyTypeCode)
        REFERENCES PartyType (PartyTypeCode)
);

CREATE TABLE Person ( -- Stands for a subtype.
    PersonId        INT      NOT NULL, -- To be CONSTRAINed as PRIMARY KEY and FOREIGN KEY at the same time, enforcing an association cardinality of one-to-zero-or-one from Party to Person.
    FirstName       CHAR(30) NOT NULL,
    LastName        CHAR(30) NOT NULL,
    GenderCode      CHAR(3)  NOT NULL,
    BirthDate       DATE     NOT NULL,
    Etcetera        CHAR(30) NOT NULL,  
    --
    CONSTRAINT Person_PK        PRIMARY KEY (PersonId),
    CONSTRAINT Person_AK        UNIQUE ( -- Composite ALTERNATE KEY.
        FirstName,
        LastName,
        GenderCode,
        BirthDate
    ),
    CONSTRAINT PersonToParty_FK FOREIGN KEY (PersonId)
        REFERENCES Party (PartyId)
);

CREATE TABLE Organization ( -- Represents the other subtype.
    OrganizationId  INT      NOT NULL, -- To be CONSTRAINed as PRIMARY KEY and FOREIGN KEY simultaneously, enforcing a association cardinality of one-to-zero-or-one from Party to Organization.
    Name            CHAR(30) NOT NULL,
    FoundingDate    DATE     NOT NULL,
    Etcetera        CHAR(30) NOT NULL,  
    --
    CONSTRAINT Organization_PK        PRIMARY KEY (OrganizationId),
    CONSTRAINT Organization_AK        UNIQUE      (Name), -- ALTERNATE KEY.
    CONSTRAINT OrganizationToParty_FK FOREIGN KEY (OrganizationId)
        REFERENCES Party (PartyId)
);

CREATE TABLE UserProfile (
    UserId          INT       NOT NULL, -- To be CONSTRAINed as PRIMARY KEY and FOREIGN KEY at the same time, enforcing an association cardinality of one-to-zero-or-one from Person to UserProfile.
    UserName        CHAR(30)  NOT NULL,
    CreatedDateTime TIMESTAMP NOT NULL,
    Etcetera        CHAR(30)  NOT NULL,  
    --
    CONSTRAINT UserProfile_PK         PRIMARY KEY (UserId),
    CONSTRAINT UserProfile_AK         UNIQUE      (Username),
    CONSTRAINT UserProfileToPerson_FK FOREIGN KEY (UserId)
        REFERENCES Person (PersonId)  
);

CREATE TABLE Account (
    AccountNumber   INT       NOT NULL,
    OwnerPartyId    INT       NOT NULL, -- A role name assigned to PartyId in order to depict the meaning it carries in the context of an Account.
    CreatedDateTime TIMESTAMP NOT NULL,
    Etcetera        CHAR(30)  NOT NULL,  
    --
    CONSTRAINT Account_PK        PRIMARY KEY (AccountNumber),
    CONSTRAINT AccountToParty_FK FOREIGN KEY (OwnerPartyId)
        REFERENCES Party (PartyId)
);

CREATE TABLE Transfer (
    TransferorAccountNumber INT       NOT NULL, -- Role name assigned to AccountNumber.
    TransfereeAccountNumber INT       NOT NULL, -- Role name assigned to AccountNumber
    TransferDateTime        TIMESTAMP NOT NULL,  
    Amount                  INT       NOT NULL, -- Retains the Amount in Cents, but there are other possibilities.
    Etcetera                CHAR(30)  NOT NULL, 
    --
    CONSTRAINT Transfer_PK             PRIMARY KEY (TransferorAccountNumber, TransfereeAccountNumber, TransferDateTime), -- Composite PRIMARY KEY.
    CONSTRAINT TransferToTransferor_FK FOREIGN KEY (TransferorAccountNumber)
        REFERENCES Account (AccountNumber),
    CONSTRAINT TransferToTransferee_FK FOREIGN KEY (TransfereeAccountNumber)
        REFERENCES Account (AccountNumber),
    CONSTRAINT AccountsAreDistinct_CK  CHECK       (TransferorAccountNumber <> TransfereeAccountNumber),
    CONSTRAINT AmountIsValid_CK        CHECK       (Amount > 0)
);
Run Code Online (Sandbox Code Playgroud)

如前所述,不需要在任何基表的列中保留不明确和有问题的 NULL 标记。

如果您想知道某个转账中涉及的Account是属于Organization还是Person,您可以在单个 SELECT 语句中通过例如 the 、 the和 theTransfer.TrasnferorAccountNumberAccount.PartyIdParty.PartyTypeCode列。

为了确保一方最多可以拥有一个帐户(如评论中所述),那么您应该为该Account.PartyId列修复一个 UNIQUE 约束。然而,在现实世界的场景中,例如在银行中,一个可以拥有零个或多个Accounts,因此我认为一对零或一的关联并不现实。

如前所述,本答案中提出的方法应该用作参考,您可以自行扩展和调整。自然地,在概念层面进行的扩展和改编应该反映在逻辑模型中。

我在 (i)这个 db<>fiddle和 (ii)这个 SQL Fiddle 中测试了这个结构的声明,两者都在 PostgreSQL 9.6 上运行(你最初附加了这个数据库管理系统的标签)。

关于表 Party、Person 和 Organization 的完整性和一致性注意事项

对于上述布局,必须保证每个“超类型”行始终处于被其相应的“子类型”对应补充,进而,确保所述与包含在超类型值“子类型”的行是相容的“鉴别器“ 柱子。

以声明方式强制执行这种情况会非常方便和优雅,但不幸的是,没有一个主要的 SQL 平台提供适当的机制来这样做(据我所知)。因此,使用ACID TRANSACTIONS非常方便,以便在数据库中始终自信地满足这些条件。

类似场景

如果您对出现超类型-子类型结构的其他业务领域感兴趣,您可能希望看到我对以下问题的回答

相关资源

  • 这些 Stack Overflow 帖子涵盖了与在 PostgreSQL中保存货币数据的列的数据类型相关的要点,例如Transfer.Amount

尾注

信息建模集成定义( IDEF1X ) 是一种非常值得推荐的数据建模技术,美国国家标准与技术研究院(NIST)于 1993 年 12 月将其确立为标准。它完全基于 (a) 关系模型的创始人,即EF Codd 博士撰写的一些早期理论著作;关于 (b)实体关系视图,由PP Chen 博士开发;以及 (c) 逻辑数据库设计技术,由 Robert G. Brown 创建。