关系数据模型入门






4.65/5 (15投票s)
基本原理易于理解。然后逐步介绍一个具体示例。
本文的两个后续文章
- 数据绑定入门
对于那些不了解数据绑定的人来说,数据绑定是一个全新的编程世界。但所有数据绑定都需要正确理解什么是数据模型。 - 针对类型化数据集编程
继续展开主题,以便您可以开发由数据绑定驱动的多个窗体应用程序,避免讨厌的代码异味。
目录
层次模型
作为面向对象的程序员,我们总是处理层次结构的对象,这意味着:一个对象包含其他对象,可以是单个对象,甚至是其他对象的列表,例如一个 Form 有一个 ControlCollection,包含许多控件,例如 Textbox,每个 Textbox 有一个(单个)垂直滚动条,当然它有一个 Text,其中包含一些字符。
这就是*层次模型*,其中每个对象要么是另一个对象的(单个)属性/特性,要么是集合的一个元素,该集合是所有者对象的属性。当然,一个对象最好是*仅一个集合*的元素——否则事情会变得困难甚至不一致。
层次模型——无论多么复杂——都可以通过树形视图来可视化——例如,查看一个(不完全简单的)WinForms 窗体的文档大纲
你看到:窗体包含一个 TabControl,TabControl 包含 TabPages,TabPages 包含 SplitContainers,SplitContainers 包含 SplitterPanels,SplitterPanels 包含 Groupboxes,Groupboxes 包含 DatagridViews,DatagridViews 包含 DatagridViewColumns...
在这个层次模型中,一切都有其明确定义的位置。
不够充分
但是,当 OOP 程序员接触到关系建模时,他的思维本身必须改变。因为尽管现实世界通常符合层次模型,但它也经常*不*符合 ;P
看这个小 Xml
<?xml version="1.0" standalone="yes"?>
<PersonProfessionDts>
<Profession Name="Police-Man">
<Person Name="Gustav"/>
<Person Name="Simone"/>
<Person Name="Klaus"/>
</Profession>
<Profession Name="Punk">
<Person Name="Sigi"/>
<Person Name="Flo"/>
</Profession>
</PersonProfessionDts>
它是关于人物和他们的职业——每个人有一个职业,一个职业有很多人。数据模型是层次结构的,一切看起来都很好。
但是,如果古斯塔夫有不止一个职业,你将如何建模——晚上他可能会做发型然后去俱乐部(或者朋克是怎样工作的)?
<?xml version="1.0" standalone="yes"?>
<PersonProfessionDts>
<Profession Name="Police-Man">
<Person Name="Gustav"/>
<Person Name="Simone"/>
<Person Name="Klaus"/>
</Profession>
<Profession Name="Punk">
<Person Name="Gustav"/>
<Person Name="Sigi"/>
<Person Name="Flo"/>
</Profession>
</PersonProfessionDts>
现在我们有两个古斯塔夫,数据不一致、损坏、一派胡言——随你怎么说。模型是错误的,当现实中只有一个古斯塔夫,而你的模型中有两个。
长话短说:层次模型不够充分。更无法建模的是一个完全没有职业的古斯塔夫 :wtf
关系模型
由于层次结构无法修复地 不适用于 建模现实,对象的嵌套被完全禁止从数据库中。人们找到了更好的方法:可以*模拟*嵌套。
关系模型只包含简单的扁平对象列表(称为“表”)。而且,子记录不是嵌套在(父)记录中,而只是获取对其父记录的*引用*。
(顺便说一句,在 Xml 中,一个表简单地由几个具有相同标签名的 Xml 元素表示)
<?xml version="1.0" standalone="yes"?>
<PersonProfessionDts>
<Profession Name="Police-Man" ID="1"/>
<Profession Name="Punk" ID="2"/>
<Person Name="Gustav" ID="1" ProfessionId="1" />
<Person Name="Simone" ID="2" ProfessionId="1" />
<Person Name="Klaus" ID="3" ProfessionId="1" />
<Person Name="Sigi" ID="4" ProfessionId="2"/>
<Person Name="Flo" ID="5" ProfessionId="2"/>
</PersonProfessionDts>
这是最初 Xml 列表的关系版本:您会看到两个表 `Profession` 和 `Person`,每个记录都获得了一个额外的属性 `ID`,它就是“主键”。主键在该表中是唯一的。您会看到两次 `ID="1"` - 这是有效的,因为这些重复出现在不同的表中。(现在,如果我愿意,我可以添加第二个古斯塔夫,使用另一个 `ID`,我仍然可以区分他们。)
此外,`Person` 表的记录现在拥有了承诺的对父记录的引用——“外键”,这里命名为 `ProfessionId`。您看,外键不是唯一的,三个人是警察,两个人是朋克。
好的,但是我们如何实现让古斯塔夫拥有两种职业,以及如何引入“安斯加尔”这样一个完全没有职业的 GUI 呢?
通过改变模型
<?xml version="1.0" standalone="yes"?>
<PersonProfessionDts>
<Profession Name="Police-Man" ID="1"/>
<Profession Name="Punk" ID="2"/>
<Person Name="Gustav" ID="1"/>
<Person Name="Simone" ID="2"/>
<Person Name="Klaus" ID="3"/>
<Person Name="Sigi" ID="4"/>
<Person Name="Flo" ID="5"/>
<Person Name="Ansgar" ID="6"/>
<PersonProfession PersonID="1" ProfessionID="1"/>
<PersonProfession PersonID="1" ProfessionID="2"/>
<PersonProfession PersonID="2" ProfessionID="1"/>
<PersonProfession PersonID="3" ProfessionID="1"/>
<PersonProfession PersonID="4" ProfessionID="2"/>
<PersonProfession PersonID="5" ProfessionID="2"/>
</PersonProfessionDts>
现在我们有了第三个表,`PersonProfession`,它的记录将一个人与一个职业关联起来。正如所承诺的,古斯塔夫与两个职业相关联,还有一个新的 GUI,Ansgar,没有任何职业。
或者将相同的数据视为表格
| ==> |
| <== |
|
技术术语
数据记录
一个包含多个值的对象。您也可以将记录想象成表中的一行,尽管这可能不是100%正确。
我们的数据模型总共包含 14 条数据记录/行
实体/表
记录的抽象。实体说明了记录由什么组成。在数据库中,实体由表表示,将实体想象成表很容易,但通常,实体是一种“思维单元”。
因为实体是记录的抽象,它也可以被视为现实的一部分的抽象。“实体”也许可以定义为数据模型和现实的共同点?抱歉——我对实体有一种感觉,但很难定义它。
尽管如此——我们的数据模型包含 3 个实体,其中两个(`Person`,`Profession`)非常具体,第三个——`PersonProfession`——更抽象,但它也是一个实体:当您将人与职业关联时,这些关联是真实的,意味着:现实的一部分,而我们的模型正是对此进行建模。
主键 / ID / PK
记录的属性,或者如果您愿意,表的列,用于标识记录。因此,主键必须是唯一的。
在我们的数据模型中,`Person` 和 `Profession` 拥有主键,但 `PersonProfession` 还没有。通常建议为每个实体配备主键,但这并非绝对必要。
外键 / FK
记录的属性,指向其父记录。外键不需要唯一。
在我们的数据模型中,`PersonProfession` 有两个外键,一个指向 `Person`,另一个指向 `Profession`。
父/子(实体/表/记录/行)
如关系数据模型所示,记录之间的嵌套被引用系统取代。子记录通过其外键引用另一个实体的父记录。原理很简单:子记录的外键指向其主键等于外键的父记录。
由于主键是唯一的,但外键不是,因此一个父记录可以有许多子记录。但一个子记录(每个外键)只有一个父记录。
(注意:如果父表中没有匹配的主键,则子行无效。)
在我们的模型中,“警察”职业有三行子记录,而“朋克”职业也有。
另一方面,只有“古斯塔夫”有两行子记录,其他人各有一行子记录,“安斯加尔”没有。
关系 / 1:n 关系 / 父子关系 / 一对多关系
如果一个表的外键引用另一个表的主键,那么这两个表之间就存在关系。这就是关系模型“模拟”层次模型的方式,其中一个父对象可能在集合中包含多个子对象。
多对多关系 / m:n 关系
基本上只有 一种关系:一对多。“m:n 关系”这个术语严格来说只是两个 1:n 关系的缩写,它们以我们的数据模型中所示的方式连接起来。
我们的数据模型显示了人与职业之间的多对多关系。
我特意选择这种结构是为了指出关系模型如何超越层次模型的能力:在关系模型中,*一个子记录可以从属于多个父表*。
中介表
中介表用于建模多对多关系——它是从属于两个父表的表。每个中介记录将一个父表中的记录与另一个父表中的记录关联起来。
在我们的数据模型中,`PersonProfession` 是中介表。
中介表通常包含比外键更多的信息。例如,约会的中介将关联约会成员,并添加关于约会发生日期和时间的信息。
建立关系数据模型
建立数据模型最简单的方法是使用类型化数据集的设计器。点击菜单:“项目-添加新项” - 选择“数据集”,输入一个有意义的名称,然后您将看到这里
使用上下文菜单添加一个 DataTable
给它一个有意义的名称,然后——再次通过上下文菜单——添加列。
在属性网格中配置列。然后(上下文菜单)将目标列设置为主键。
请注意属性网格中的配置:`DataType=Int32`,`AllowDBNull=False`,`AutoIncrement=True`。
有许多选项可以配置 DataColumn,对于主键来说,这是一个常见的配置。
以同样的方式创建所需的所有表。请记住:PersonProfession 没有主键,但有两个外键——它们是普通列,当然是 `Int32` 类型,因为外键值只能等于主键值,如果它们是相同的数据类型。
然后用鼠标将主键拖到外键上
这将打开数据关系配置对话框
如图所示的配置是最常见的。尤其是 `DeleteRule.Cascade` 非常有价值,因为它的作用是,当你删除一个人时,他所有的子行也会被删除。否则,它们将变得无效,因为它们的外键将“悬空”,这将导致错误。
最后应该像这样
(请注意,我将 `Name.AllowDbNull=False` 设置为 `False`,因为没有名字的人是没有意义的。)
这是一个类型化数据集,同时它也是我们数据模型的实体-关系图(如果您不了解该术语,请点击链接)。请注意关系线:主键端显示一个非常小的键,外键端显示一个非常小的无限符号。这些符号表示一对多关系的方向。
并且在后台,设计器生成了大量类型化类,以便于舒适地使用它。
针对类型化数据集编码
例如,您现在可以循环遍历所有人员,并输出他们的职业
Dim OutputLines As New List(Of String)()
For Each rwPerson As PersonRow In PersonProfessionDts.Person
OutputLines.Add(rwPerson.Name & ": ")
For Each rwPersonProfession As PersonProfessionRow In rwPerson.GetPersonProfessionRows()
'indent Professions-Namees
OutputLines.Add(" " & rwPersonProfession.ProfessionRow.Name)
Next
Next
MessageBox.Show(String.Join(Environment.NewLine, OutputLines), "Professions of Persons")
输出
Professions of Persons --------------------------- Gustav: Police-Man Punk Sigi: Police-Man Flo: Punk Simone: Punk Klaus: Police-Man Ansgar:
(注意古斯塔夫有两个职业,安斯加尔没有)
反过来:循环职业并输出“他们的”人员
Dim OutputLines As New List(Of String)()
For Each rwProfession As ProfessionRow In PersonProfessionDts.Profession
OutputLines.Add(rwProfession.Name & ": ")
For Each rwPersonProfession As PersonProfessionRow In rwProfession.GetPersonProfessionRows()
'indent Person-Names
OutputLines.Add(" " & rwPersonProfession.PersonRow.Name)
Next
Next
MessageBox.Show(String.Join(Environment.NewLine, OutputLines), "Persons of Professions")
输出
Persons of Professions --------------------------- Police-Man: Gustav Klaus Sigi Punk: Gustav Simone Flo
您看:非常简单的面向对象代码。通过强类型集合的嵌套循环。这种简单性是故意的:正是类型化数据集生成的类,提供了如此简单和面向对象地导航关系数据模型的功能。
数据模型的加载与保存
别担心,复杂的部分已经完成。加载和保存非常容易——当您不需要数据库时
Public Class frmPersonProfession
Private _DataFile As New FileInfo("..\..\PersonProfessionDts.Xml")
Public Sub New()
InitializeComponent()
PersonProfessionDts.ReadXml(_DataFile.FullName)
End Sub
Private Sub Form1_FormClosed(sender As Object, e As FormClosedEventArgs) Handles MyBase.FormClosed
PersonProfessionDts.WriteXml(_DataFile.FullName)
End Sub
End Class
就是这样:类型化数据集可以直接从 Xml 文件读写——根本不需要数据库!:wtf
上面显示的代码完整地为关系数据应用程序提供了完整的CRUD(如果您不了解此术语,请点击链接)支持。
您可以添加、更改、删除任何您想要的数据记录
“仅数据集” - 无数据库的数据应用程序
您看:我们有一个关系数据应用程序,但没有一个 SQL 命令需要调用。尽管大多数程序员认为类型化数据集已过时,但在托管代码中避免使用 SQL 的模式绝对是现代的。Entity-Framework 也隐藏了 SQL 命令,所有其他 ORM 映射器也如此。
不幸的是,目前还没有一个 OR-Mapper 可以直接从磁盘读取和写入完整的数据模型。因此,在易用性、灵活性和可移植性方面,类型化数据集仍然比所有现代 OR-Mapper 具有显著优势。
- 无需包含额外的库
- 无需安装数据库提供程序
- 数据 Xml 文件可以简单地压缩和传输。因此,包括数据在内的完整应用程序源代码可以封装在一个 zip 文件中。这对于讨论问题或原型可能很有价值。
- 数据模型可以通过 Visual Studio 的内置工具轻松更改
对于本地数据应用程序,这种方法——我称之为:“仅数据集”——是嵌入式数据库的一个严肃替代方案。将大约 10000 或 20000 条记录加载到内存中肯定不是问题——如今,一张简单的位图通常会占用更多的内存。
此外,将数据后端从 XML 文件切换到真正的数据库的选项始终可用。数据集不关心它是如何填充的——无论是通过数据适配器从数据库还是直接从 XML 文件。
这种迁移无需更改任何绑定或代码——数据集仍然可以工作,只是填充和保存的方式变得更强大(也更复杂)。
初学者:在数据库访问之前学习数据绑定
有一点需要理解:一个严肃的数据应用程序基于*两个*非常不同的相同抽象数据模型实例:一个实例在数据库中建模,而另一个——也就是我们的代码处理的模型——是客户端应用程序中的本地类型化模型。后者要么设计为类型化数据集,要么设计为 EntityFramework(或其他 ORM 映射器)的实体模型类。
如果您首先开始数据库访问,您很可能会陷入无望的困惑,直接使用 DbCommand,并且可能会错误地使用它们。
直接使用 DbCommand 的主要缺点是,即使没有结构化的类型化模型来存放数据,它们也会获取数据。这就像 去河边 打水 -却忘了带水桶 :wtf:。
如果您继续这样做,您将永远无法学到数据绑定的概念。因为数据绑定概念包括在设计器中配置绑定的概念——例如在表单设计器中——但设计器只能在存在类型化数据模型的情况下支持设计绑定。
所以你永远不会看到数据绑定的功能,你永远不会错过它,你会非常努力地工作,但你的应用程序——从最先进的角度来看:嗯——垃圾,抱歉 :-(。
但是,当您学习在*设计器*中*设计*数据绑定时,您会了解类型化模型的价值,并且您将以尊重该模型的方式组织数据库访问(即通过 DataAdapters 或 OR-Mapper 组件)。
本文展示了通过老式类型化数据集开发数据应用程序的入门——这是最简单的方法,因为尚不需要数据库。
之后,您可以切换到 Entity-Framework,而不会失去您学到的最重要原则,即如何设计数据模型和使用设计器设计的数据绑定进行开发。原理是相同的,尽管一些设计器可能看起来不同,或者——例如使用“代码优先”——您不一定需要实体-关系设计器(这是优点还是缺点?)。
一个重要的注意事项是,开发数据模型是一个经验问题。没有人会立即知道针对每个给定问题的最合适的模型,即使他刚刚理解了这里解释的原理。
在实体和关系中思考的方式需要练习一段时间才能熟练掌握。
创建数据模型的技能也是您即使以后可能不再使用“仅数据集”时也不会失去的宝贵财富。
示例应用程序
字面上就是给定理论的应用:您将在其中找到在数据集设计器中设计的数据模型。
还有两个选项卡页,它们简要展示了数据绑定设计意味着什么
第一个选项卡页只是按原样显示所有表格 - 在 DatagridViews 中(见上图)。
另一个选项卡页面显示了一个更复杂的视图:左侧可以选择一个职业,右侧显示该职业的人员。
此外,通过组合框列,您可以*更改*数据记录引用的 Person。
下面再次完成了同样的事情,但现在左侧是选择人员,然后您会看到每个职业(您可以添加、更改、删除它们)
所有这些都是在设计器中完成的,它工作可靠,完全Crud支持,并且行为一致——无需一行代码。
在我的下一篇文章中,我将更深入地介绍 WinForms 上的数据绑定设计。
总结
我们看到了关系数据模型如何超越了对象导向程序员非常熟悉的传统层次数据模型的一个关键限制:一个*关系*实体可以从属于*多个*父实体——而不会出现冗余。
我们得到了一些常见技术术语的定义。
我们看到了如何使用实体-关系图(具体地说:使用数据集设计器)来设计关系数据模型。
我们大致了解了如何使用数据集设计器为我们生成的类。这意味着:我们看到了如何从类型化数据模型访问类型化数据——例如,输出按职业分组的所有人员,以及按执行这些职业的人员分组的所有职业。
我们大致了解了 通过 设计的 数据绑定 在 GUI 中可以实现什么 (但 没有更深入的 解释,如何配置尤其是高级功能。正如所说,这将是 另一篇文章 的主题。).
我们了解到,完整的关系数据应用程序可以完全不使用数据库进行开发——而且非常简单。这对于学习者和原型开发都可能很有趣。在某些情况下,一个简单的 Xml 文件可以取代嵌入式数据库。