第五章 详细设计阶段的数据库结构设计 详细设计阶段包括:数据库设计、模块设计和界面设计。 下面我们探讨一下数据库设计的有关问题。 第一节 关系数据库的结构设计 一、面向过程的设计与 实体关系图 关系数据库的单元是表,在面向过程的设计中,建立一个关系数据库的第一步,需要仔细考虑实体-关系图(ERD),给每个实体建立一张表,每张表的数据域要与已经定义的实体相一致。然后,可以为每个表建立一个主键,如果没有合适的字段作为主键,可以自己创造一个,主键的数据必须是唯一的。 1 )实体 实体指的是某些事物,企业需要存储有关这些事物的数据。实体实例表达的是实体的具体值。 2 )属性 实体的描述特形称为 属性,某些属性可以逻辑上被组合,称为 组合属性,它在不同的数据建模语言中也被称作串联属性、合成属性或者数据结构。 3 )域 于是属性的一个参数,定义了这个属性所能定义的合法值。事实上这个值和使用的数据库特点有关。 4 )标识符 然后考虑实体之间的关系,关系基数符号如下: 以此可以建立表与表之间的关系: 实体之间的关系有三种,最简单的而且最有代表性的例子也就是学生、老师、锁柜和书的关系。 也就是一对一( 1:1)或一对多(1:N)或多对多(N:N)关系。 比如: Student和 Locker之间可以是一个一对一关系,因为每个学生都有一个相应的锁柜,而每个锁柜只给一个学生使用。Student和Book之间可以有一个一对多关系,因为每个学生可以有多本书,但每本书只归一个学生所有。Student和Teacher之间有一个多对多关系,每个学生可以有多个教师授课,而每个教师又可以为多个学生讲课。 注意,一个实体可以参与多个关系,而每个关系可以有不同的对应关系。 这里还有一个问题,就是多对多关系需要增加一个关联实体,比如如下的多对多关系: 我们会发现,学生某门课的成绩应该放在什么地方呢?尽管模型中表达了学生选修了某门课程,但没有放置这门课程成绩的地方,所以需要增加一个关联实体。 这样,我们就可以得到设计关系数据库的一般步骤: 1,为每个实体类型建立一张表。 2,为每张表选择一个主键(如果需要,可以定义一个)。 3,增加外部码以建立一对多关系。 4,建立几个新表来表示多对多关系。 5,定义参照完整性约束。 6,评价模式质量,并进行必要的改进。 7,为每个字段选择适当的数据类型和取值范围。 二、执行参照完整性 建立关系主要需要建立主键和外键的关系,执行参照完整性表达了外键和主键间一致的状态。执行参照完整性表达的是:一个一致的关系数据库状态,每个外键的值必须有一个主键值与之对应。 规则: 1,当建立一个包含外键的记录的时候,应确保主表中相应的主键值要存在。 2,当删除一条记录的时候,要确保所有相应外键的记录也被删除。 3,当更改一个主键值的时候,要确保所有相应表外键值也跟随改变。 三、评价模式质量 一个高质量的数据模型应该具备以下特点, 1,表中每行数据及主键是唯一的。 2,冗余数据较少。 3,容易实现将来数据模型的改变 不过,提高数据库设计质量的方法有很多,但量化方法又很少,很大程度上依赖于设计师的经验和判断,下面提供几个注意点。 1 ,行和关键字的唯一性 主键是能够唯一定义一行数据的一列或者多列,主键中的列值不能为null,主键为数据库引擎提供了获取使据库表中某个特定行的方法,主键还用于保证引用的完整性。如果多个用户同时插入数据,则必须保证不会出现重复的主键。 由于主键是必须存在的,而主键是不可重复的,显然表中的每一行也都是唯一的,这就是所有关系数据库模型都有一个基本要求,那就是主键和表中的行是唯一的。 但我们应该如何来选择主键呢? 智能键、常规建和代理键 智能键是一种基于商业数据表示的键,例如SKU(Stock Keeping Unit 常用保存单元)就是智能键的例子。它定义一个10个字符的字段(Char(10)),它的可能分配如下,前4个字符为供应商代号,随后3个字符保存产品类型代号,最后3个字符保存一个序列号。 常规键由现有商业数据中一个或多个列组成,比如社会保险号。 尽管智能键和常规键不尽相同,但他们都是用商业相关数据组成,下面统一成为智能键。 代理键是由系统生成的,与商业数据无关,比如自动增值列(Identity),GUID(globally unique identifier 全局唯一代码,16字符),这是通过取值算法得到的键值,后面将称之为GUID键。 下面的例子包含三张表(作者Author,书籍Book,而AuthorBook是一张多对多的连接表,因为一个书籍可能由多个作者完成)。 注意,在代理键完成的时候,需要多增加一个列值,这是因为代理键是系统自动生成,用户不可见的。 数据大小 使用智能键或代理键数据的大小是不一样的,因此一定要计算键的使用引起数据量的变化,显然,基于int的自动增加列数据量最小,但也要注意,int的最大值是有限的,而且不便于移动数据,这些都是考虑的因素。 键的可见性 智能键是可见的而且是有意义的,而代理键一般不对用户开放而且是无意义的,从维护的角度来说,似乎智能键更优,因此,如果智能键的数据确实存在,可以考虑智能键。 在非连接对象确保唯一性 在非连接对象中,两个人同时加入行智能键重复的几率是存在的,但代理键不可能重复。而自动增加列值存在着诸多限制(移动性,最大值),所以GUID键是最合适的。 在数据库中移动数据 自动增加列值事实上无法在数据库中移动数据,智能键除非仔细设计,逐渐重复也不是没可能,比较好的是GUID。 使用的方便性 自动增加键是最方便的,但由于上面种种讨论,并不推荐使用,智能键需要多列组成,但方便性可以接受,GUID的使用往往叫人不太习惯,但合理的设计以后,这并不是问题,所以GUID主键还是最常用的。 2 ,数据模型的灵活性 在关系型数据库最早的规范当中,数据库的灵活性和可维护性是最主要的目标。如果对数据库模式进行更改,对已经存在的数据内容和结构造成的影响最小,那么就可以认为这个关系数据库模型是灵活的而且是可维护的。 比如,增加一个新的实体,不需要对原有的表进行重新定义。增加一个新的一对多关系,只要求给已存在的表添加一个外部码。增加一个新的多对多关系,只需要在模式中添加一个单独的新表。 在判断数据模型的灵活性的时候,要特别注意属于冗余的影响,一般来说,数据存在多个地方,那么在进行增、删、改、查操作的时候会增加额外的操作,而且维护上也更复杂和低效,在操作失败的时候,数据不一致的危险性也会更大。 多表关联的时候,外键是必须的,但这样也会造成关键字段操作的复杂性。 关系型数据库管理系统通过参照完整性的约束来保证主键和外键一致,但并没有自动化的方法来保证冗余数据项的一致,为了避免关键字段的数据冗余,可以采用数据库的规范化。 数据库规范化是用来评价关系数据库模式质量的有效技术,它可以确定一个数据库模式是不是包含了任何错误冗余,它基本的表述如下: 第 1 范式( 1NF ):没有重复子段和字段组的数据库表结构。 函数相关:两个字段 值之间一一对应。如果对于任意子段B的值有而且只有一个A的值与之对应,则称A函数相关B。 这里主要研究的是字段内容,保证表中数据没有允于余。 第 2 范式( 2NF ):每个非关键字段对于主键或者主键组函数相关。 这里主要研究其它字段与关键字段的关系 第 3 范式( 3NF ):各个非关键字段之间不能函数相关。 这个范式保证了数据没有冗余。 下面对这些概念作一些解释。 设想有这样的实体,并以此构造了两张表。 第一范式: 这是对表格行定义的一个结构限制,事实上关系数据库管理系统本身就是在一张表中拒绝由两个相同的字段的,所以要实现第一范式并不困难。 函数相关: 这是一个比较难以描述以及应用的概念。 比如考察“产品项目”表中“编号”和“标准价格”两个字段,现在已知“编号”是一个内部主键,在表中一定是唯一的,为了判断“标准价格”是不是函数相关“编号”,只需要描述:对于字段“编号”的值有而且只有一个“标准价格”的值与之对应,则“标准价格”函数相关“编号”。 我们来考察一下对于“产品项目”表这个表述对不对?事实上只要字段“编号”数据是唯一的,这个表述就是正确的,也就是“标准价格”函数相关“编号”。 一个不太确切但很简单的方式如下,“产品项目”表中对于每个产品产品价格应该是唯一的,这就是函数相关的,如果每个产品有多种价格,这就不是函数相关的。 第二范式: 为了判断“产品项目”表是不是属于第二范式,我们必须首先判断它是不是第一范式,因为它不包括重复的字段,所以它是属于第一范式,然后我们需要判断每个非关键字段都函数相关与“标准价格”(也就是每个字段都来替换函数相关定义中的A),如果每个非关键字段都函数相关与“标准价格”,那“产品项目”表就是属于第二范式。 当主键是由两个或者多个字段组成的时候,判断表是不是第二范式就比较复杂,例如考虑如下的“目录产品”表,这个表示为了表示“销售目录”表和“产品项目”表之间的多对多关系。所以,表达这个关系的表的主键由“销售目录”的主键(“目录号”)和“产品项目”的主键(“产品号”)组成,这个表还包含了一个非关键字段公布价格。受市场影响,公布的价格往往是浮动的。 如果这张表属于第二范式,那么非主关键字段“公布价格”必定函数相关于“目录号”与“产品号”组合。我们可以通过替换函数相关定义中的词语来验证函数相关: 对于“目录号”与“产品号”组合的值有而且只有一个“公布价格”的值与之对应,则“公布价格”函数相关“目录号”与“产品号”组合。 分析这样的语句正确性还是需要技巧的,因为你必须考虑“产品目录”表中所有所有可能出现的关键字值得组合。比较简单的分析的方法不是机械的对照,而是考虑这个实体本身的问题。 一个产品可能在多个不同的销售目录中出现,如果在不同的目录中它的价格不同,那么上述的陈述就是正确的。如果产品不论在哪个目录中,它的价格是相同的(或者说一个价格数据对应于多个目录),那上述的陈述就是错误的,而且这个表不是第二范式。所以正确的判断不是依赖于陈述,而往往是依赖于对问题本身的理解。 如果非关键字段只是函数相关于主键组的一部分,那么这个非关键字段必须从当前表中移出去,并且放在另一个表中。 例如如下的“目录产品”表,增加了一个目录的“发布日期”字段,显然,这个字段函数相关于“目录号”,但不函数相关于“产品号”,也就是同一个“产品号”可能在多个“发布日期”中使用,这就会造成冗余,这个表不属于第二范式。 正确的做法是把“发布日期”移出来,放在“销售目录”这张表中,这时候就正确了。 要判断一张表是不是第三范式,则必须考虑每一个非关键字段是不是函数相关于其它的非关键字段,如果是,就要考虑结构上的修正。 对于一个大型表来说,这实际上是一个非常复杂的工作,而且工作量随着字段数的增加而快速增。当非关键字段数是N时,要考虑的函数相关数目为N*(N-1)。而且要注意函数相关要两方面考虑(即A相关B,B相关A)。 我们来考虑下面一个简单的“职工状况”表。 这张表有三个非关键字段,所以需要考虑六个函数相关: “部门”与“住址”?一个住址可能有多个部门的人,不是。 “住址”与“部门”?一个部门相同住址的可能不止一个,不是。 “部门编号”与“住址”?一个住址可能牵涉到的部门编号是多个,不是。 “住址”与“部门编号”?一个部门编号有相同住址的可能不止一个,不是。 “部门编号”与“部门”?一个部门只有一个部门编号,是。 “部门”与“部门编号”?一个部门编号只对应一个部门,是。 注意,有时候两方面考虑只有一个是,比如“省份”和“邮政编码”,一个省份有多个邮政编码,而一个邮政编码只能对应一个省份。 这样一来,当部门有多个人的时候就会出现大量的冗余,解决的办法是再增加一张表,表达“部门”和“部门编号”的对应关系。 如果出现了需要几个字段数据计算出结果的字段,也不属于第三范式,可以把这个字段取消,由调用方通过程序来解决。 第二节 面向对象数据库设计 一、模式:把数据对象表表示成类 把对象表示成表(Representing Objects as Tables)的模式建议,在RDB中对每一个持久化对象类定义一个表,对象的原始数据类型(数字、字符串、布尔值)的属性映射为列。 如果对象只有原始数据类型的属性,就可以直接映射。但对象如果还包含了其它复杂对象的引用属性,事情就比较复杂,因为关系型模型需要的值是原子性的(第一范式),所以除非有充分的例有,尽可能不要这样来做。 在UML中,虽然可以很好的表达类,但是,为了确切的表达数据,还需要有一些扩展,这个关于数据建模的扩展标准,已经提交给了OMG组织。 比如:PK:主键(primary Key); FK:外建(foreign Key)。 等。 注意,如果仅仅把对象表达成类,和基于结构的思维实际上一样的,面向对象的数据表达最大的特点是数据具有继承性,这是和面向过程的设计完全不同的地方。也就是说,对象数据库设计的时候,和关系数据库很大的区别,在于类可以实现继承,比如如下的例子。 这为我们优化系统提供了更大的思维空间。 二、持久化对象 目前常用的存储机制主要由两种.。 对象数据库: 如果用对象数据库来存储和检索对象,就不需要第三方持久化服务,这是使用对象数据库吸引人的地方之一。 关系数据库: 由于RDB(关系型数据库)的流行,通常我们遇到的数据库都是RDB 而不是更方便的ODB,这样一来, 面向记录和 面向对象的数据表示之间会有一系列的问题。往往我们会需要一个O-R映射服务。 其它: 出RDB以外,有时候我们还会使用其它存储机制(XML结构、层次数据库等)来存储数据,同样也需要有某种服务,来使这些机制和对象一起协调工作。 绝大多数应用,都需要从一个持久化存储机制(例如关系型数据库)存储和检索信息。 通常更好的办法,是购买或者获得工业的持久化框架,而不是自己开发。 开发工业级的数据库持久化O-R(对象关系映射)服务需要数人年的时间,其中许多细节问题需要专门的专家。 三、解决方案:来自持久化框架的持久化服务 持久化框架(persistentce framework)是多用途的、可重用的和可扩展的一组类型,它提供了支持持久化对象的功能。 持久化服务(persistentce service)(或子系统)实际上是提供了这种服务,而且使用持久化框架来提供这种服务。 一般来说,持久化服务是属于技术服务层的子系统。 典型的比如:Hibernate。 这种持久化服务需要关注下面两个问题。 1 、使用外观模式访问持久化服务 我们已经讨论过,外观模式是为子系统提供一个统一的接口,事实上,通过给定ID,就可以返回一个对应的(代表一行)的对象。这里的外观可以是一个单件模式。 2 、使用模板方法设计框架 是用模板方法,也可以实现持久类根据数据变化。 至少在目前关系型数据库还是非常流行的情况下,O-Rmaping还是非常有意义的,因为这样一来,设计的时候就可以专注于对象的特点,可以使用抽象和泛化的方法来处理问题。 六、数据库的关联设计 在设计数据库系统的时候,必须作出如下重要决策: 应该加载多少数据? 在涉及多个关联表的时候,如何更新数据? 实现何种类型主键(最重要的决策问题)? 我们下面将逐一进行讨论。 设想我们有一个简化的订单数据库,该数据库包含5个关联表。 1 )应加载什么数据 数据选择: 应该只加载用户需要处理的非连接数据,几乎所有的情况下只需要获取数据库的一个数据子集。 数据量: 数据量的选择会影响加载时间、更新时间、以及内存需求量。记住,非连接对象是一个基于内存的对象。因此要注意所获取的数据量大小,如果不能确信获取这些数据是必要的,就不要获取它。 分割数据: 根据数据对象的使用目的,最好把数据分割成多个部分,并分别存入相应的本地数据集对象。 例如,当用一个非连接对象保留数据的时候,这个对象会包括所有5个表。 再仔细分析一下,首先假定主体是销售部门,销售部门的主要关心点是客户: 针对特定客户,一个客户在Customer(消费者)和Address(地址)表只有一行信息,但是Order(订单)表和OrderItem(订单项)却可以包含多行与该客户相关的信息(一个客户可以有多个订单)。这样,我们可以把订单和客户信息放在一个DataSet里面(Customer DataSet)。 如何处理Product(产品)表呢?不同的情况可能对Product表的要求是不一样的。 如果假设本地数据集对象包含该客户在某个时刻的全部数据,则Product表可能只包含与OrderItem表相关的行。 同样,如果希望能够为不同的产品增加更多的订单,又可能必须保证Product表是完整的,因此,可能需要再次将该表存入原先其所属的本地数据集对象中。这样就能独立的在客户之间传递产品列表。 我们希望能够删除Product表中不再使用的产品,但不强制删除它们在OrderItem表中的引用。 由于情况比较复杂,我们可以专门设置一个本地数据集(Product DataSet)来装载Product表。也就是使用一个本地数据集保留客户数据,另一个本地数据集保留产品数据。 记住,不能在不同的本地数据集对象的数据表之间建立外键约束。但可以很容易的通过OrderItem表的ProductId定位Product数据,如下图所示。 在下载数据的顺序上,首先决定Order的要求,再由它的Id决定下载哪些OrderItem内容,以及由CustomerId决定下载哪些Customer和Address。 由OrderItem的ProductId决定下载哪些Product。 这样就可以免掉许多无效数据的下载。 当然,上面这些讨论并不是规则,需要具体情况具体分析。 第三节 并发问题及其应对 数据访问的一个挑战性问题,就是多个用户可能同时访问数据库同一个数据。比如一个用户正在编辑数据,另一个用户正在利用这个数据形成报表。 这就要注意两个问题: 1)防止两个用户同时编辑数据; 2)防止报表显示不完整的数据。 并发是使多个用户访问数据库,并得到一个相互一致的数据视图的能力。通过锁定必须的行、表或数据库,防止用户访问可能不一致的数据。 例如: 假定小张正在贷方帐户A和借方帐户B之间进行一项转账业务。 而此时小李正在帐户A中提取资金,则小张看到的帐户A的余额应该是多少呢?如果小张的事务还没有完成,如何处理小李的事务呢? 解决方案: 在一个事务还没有完成的时候,数据库服务器通过锁定数据库来接决这样的问题,在小张的事务还没有完成的时候,小李必须等待小张的事务完成以后才能进行操作,我们的目标是尽可能快地处理事务,并使锁定等待的时间尽可能短。 但是,我们如果使用非连接对象,把一部分数据库数据复制到客户段,等修改后再发回数据库,而把这个期间作为一个完整的事务,就会产生严重的锁定问题。 正是这种情况,迫使非连接对象获得数据的时候不启动一项事务(因而不会锁定),但是在提交数据的时候,检查和处理多个更新操作引起的冲突。 设想有一个表“账务”,包含字段为编号、姓名、金额。 把数据读入DataTable表以后,DataRow对象将包括Current版本和Original版本数据。 更新命令如下: Sql="UPDATE 账务 SET 姓名 = @姓名, 金额 = @金额 WHERE ( 编号 = @Original_编号) AND (姓名 = @Original_姓名 OR @Original_姓名 IS NULL AND 姓名 IS NULL) AND ( 金额 = @Original_金额 OR @Original_金额 IS NULL AND 金额 IS NULL); SELECT 编号, 姓名, 金额 FROM 账务 WHERE (编号 = @编号)"; 这条命令的Where子句指出,只有在数据库当前列值与原来的列值都相同的时候,才会执行这条命令,这可能是解决并发冲突最安全、最容易的办法。此外还要注意,在提交一行数据的时候,所有的列值都将改变。 这时,小张修改金额,提交应该没有问题 但小李修改姓名,提交发生并发错误 但这样的策略是不是正确呢?毕竟两个人改的不是同一列数据,这样处理起来比较简单(一行数据全部改变),如果希望修改这个行为,可以改变Sql语句。 解决并发问题的策略 如何解决并发冲突是一个商业决策,下面列出了一些主要决策原则: l 时间优先:第一次更新优先,也称为“时间顺序优先”,只保留第一次更新的结果,上面的例子实现了这个策略。 l 时间优先:最后一次更新优先,也称为“逆时间顺序优先”,只保留最后一次更新结果,这个方法最简单,因为可以把Where子句的内容全部去掉。 l 角色优先:卖方人员优于买方人员,因为卖方人员更了解产品。这个方式实现起来难度较大,因为必须知道每个用户的角色,而且如果角色相同,还是应该保留实现顺序优先作为备用机制. l 位置优先:位置优先,总店优于分店,这个方式必须知道角色位置,事实上权限往往决定了位置,所以可以用权限优先来代替这个策略,同样也需要用顺序优先作为备用机制。 l 用户解决冲突:当发生冲突的时候,弹出一个冲突解决界面,由用户决定下一步处理方式,这种方式虽然直观,事实上并不常用,而且出现问题的机率也比较大。 一般来说,对于顾客数据可采用角色优先策略,对于账目数据可采用位置优先策略。 第四节 处理事务 一、为什么要关注事务处理 执行事务事务是一组组合成逻辑工作单元的操作,虽然系统中可能会出错,但事务将控制和维护事务中每个操作的一致性和完整性。 例如,在将资金从一个帐户转移到另一个帐户的银行应用中,一个帐户将一定的金额贷记到一个数据库表中,同时另一个帐户将相同的金额借记到另一个数据库表中。由于计算机可能会因停电、网络中断等而出现故障,因此有可能更新了一个表中的行,但没有更新另一个表中的行。如果数据库支持事务,则可以将数据库操作组成一个事务,以防止因这些事件而使数据库出现不一致。如果事务中的某个点发生故障,则所有更新都可以回滚到事务开始之前的状态。如果没有发生故障,则通过以完成状态提交事务来完成更新。 在一个操作中,如果牵涉到多个永久存储,而且是多步完成,并且需要修改数据的时候,就一定要考虑加上事务处理。 二、事务处理的基本概念 事务是一个原子工作单位,必须完整地完成其中的所有工作,如果提交事务,则事务执行成功,如果终止事务,则事务执行失败。事务具备以下4个关键属性:原子性、一致性、孤立性和持久性,这被称之为ACID属性。 l 原子性:事务的工作不能划分为更小的部分,尽管其中包括了多条Sql语句,但它们要么全部执行,要么都不执行。这意味着出现一个错误的时候,将全体恢复到启动事务前的状态。 l 一致性:事务必须操作一致性的视图,并且必须使数据处于一致的状态,事务再提交之前,它的工作绝不会影响到其他的事务。 l 孤立性:事务必须是独立运行的实体,一个事务不会影响到其它正在执行的事务。 l 持久性:在提交一个事务的时候,必须永久性的存储它,以免发生停电或系统失败丢失事务。在重新供电或者恢复系统以后,将只会恢复已经提交的事务,而退回没有提交的事务。 1 )并发模型和数据库锁定 数据库使用数据库锁定机制来防止事务互相影响,以实现事务的一致性和孤立性,事物在访问数据的时候,将强制锁定数据,而其它要访问的事物将强制处于等待状态,这说明长时间的运行事务是不可取的,这会严重影响系统性能和可测量性。这种用锁定来阻止访问的做法,称为“悲观的”并发模型。 在“乐观的”并发模型中,将不使用锁,而是检查数据在读取之后是不是发生了变化,如果发生了变化,则抛出一个异常,由商业逻辑进行恢复,前面DataAdapter的UpDate方法就是使用了这种模型,但它无法解决我们在前面应行的例子中出现的问题。 2 )事务的孤立级别 实现完全孤立的事务当然好,但代价太高,完全孤立性要求只有在锁定事务的情况下,才能读写任何数据,甚至锁定将要读取的数据。 根据应用程序的目的,可能并不需要实现完全的孤立性,通过调整事务的孤立级别,就可以减少使用锁定的次数,并提高可测量性和性能,事物孤立性将影响下列操作: l Dirty 读取操作:该操作能够读取还没有提交的数据,在退回一个添加数据的事务的时候,该操作会造成大问题。 l Nonrepeatable 读取操作:该操作指一个事务多次读取同一行数据的时候,另一个事务可以在第一个事务读取数据期间,修改这行数据。 l Phantom 读取操作:该操作指一个事务多次读取同一个行集的时候,另一个事务可以在第一个事务读取数据期间,插入或删除这个行集中的行。 下表列出了典型数据库中的孤立级别。
级别 | Dirty 读取操作 | Nonrepeatable 读取操作 | Phantom 读取操作 | 并发模型 |
Read Uncommitted | 是 | 是 | 是 | 无 |
Read committed With Locks | 否 | 是 | 是 | “悲观的” 并发模型 |
Read committed With Snapshots | 否 | 是 | 是 | “乐观的” 并发模型 |
Repeatable Read | 否 | 否 | 是 | “悲观的” 并发模型 |
Snapshot | 否 | 否 | 否 | “乐观的” 并发模型 |
Serializable | 否 | 否 | 否 | “悲观的” 并发模型 |
下面对各个级别进行讨论: l Read Uncommitted 级别:其它事务的修改情况将对某个事务的查询造成影响,如果设置为该级别,则在读取该数据的时候,即不会获取锁,也不愿意使用锁。 l Read committed With Locks 级别:这是Sql Server的默认设置,已提交的更新在事务间是可见的,长时间运行的查询,不需要实时保持一致。 l Read committed With Snapshots 级别:已提交的更新在事务间是可见的,如果设置为该级别,则不会获取锁。但行数据的版本信息将用于跟踪行数据的修改情况,长时间运行的查询需要实时保持一致,该级别将带来使用版本存储区的开销,版本存储区可以提高吞吐量,并且减少对锁的依赖。 l Repeatable Read 级别:在一个事务中,所有的读取操作都是一致的,其它事务不能影响该事务的查询结果,因为在完成该事务并取消锁定之前,其它事务一直处于等待状态。该级别主要用在读取数据后希望同一个事物中修改数据的情况。 l Snapshot 级别:在需要精确的执行长时间运行的查询和多语句事务的时候,如果不准备更新事务,则使用该事务。使用这个级别的时候,不会获取读取操作的锁,以防止其它事务修改数据,因为在读取快照(Snapshot)并提交修改数据的事务之前,其它事务看不到修改情况,数据可以在该级别事务中进行修改,但是在快照事务(Snapshot transaction)启动后,可能和更新相同数据的事务发生冲突。 l Serializable 级别:在所访问的行集上放置一个范围锁,这是一个多行锁,在完成事务之前,防止其它用户更新数据集或者在数据集中插入行。在事务的生命周期中,保持数据的一致性和正确性,这是孤立级别中的最高级别,因为这个级别要使用大量的锁,所以只是在必要的时候才考虑使用这个级别。 在提交Update或者Delete语句后,版本存储区将保存行版本记录,直到提交所有的活动记录位置,事实上,在提交或者结束下列事务类型之前,版本存储区将一直保存行版本记录。 l 在 Snapshot 孤立级别下运行的事务。 l 在 Read committed With Snapshots 孤立级别下运行的事务。 l 在提交事务之前启动的所有其它事务。 三、使用容错恢复技术 在准备产品化应用程序的时候,如何知道数据库是不是能够经受众多用户对应用程序反复的使用呢?如果数据库服务器关机,会出现什么情况呢?如果数据库服务器需要快速重启,会出现什么情况呢? 首先,在停止和重启服务器的情况下,我们可以清除连接池并重新建立它。 但如何才能保证结果和服务器关闭之前完全相同呢? 这就要用到容错恢复技术了。 请看下面的场景: 使用三台数据库服务器,“主服务器”、“镜像服务器”、“观察者服务器”,使用数据库镜像的时候,客户只和主服务器联系,镜像服务器处于数据恢复状态(不能进行任何访问),当向主服务器提交一个事务的时候,也会把这个事物发给镜像服务器。 观察者服务器只是观看主服务器和镜像服务器是不是正在正常工作。 在主服务器关机的时候,观察者自动把镜像服务器切换为主服务器,见下面两张图。 注意,当发现主服务器有问题的时候,则自动清除连接池,并转而使用备用服务器。 第五节 数据库结构设计案例 我们用前面讨论的“电源设备销售客户服务子系统”作为例子,来具体分析一下数据库设计的有关问题,这个案例的说明见于第三章第八节综合案例的研究,表达了TB公司电源设备销售部为了实现信息技术战略规划(ITSP)构思的销售服务系统,我们用这个案例来具体说明一下数据库的设计方法。 一、获取实体 仔细研究需求分析文档,从而可以获取实体,注意实体要按照业务词汇来定义,而不要使用技术词汇来定义,例如,为了简化问题,这里分析上面项目的订单和促销活动部分,在这一部分里,订单和促销活动联系在一起,而其它的购买方式暂时不予考虑。在这样的情况下,本项目定义的实体如下。
实体名称 | 业务定义 |
合同 | 合同是记录客户购买产品的状况,当购买产品达到一定数额的时候,可以得到规定比例的返点。 |
基本客户 | 基本客户是目前比较活跃的客户,一是公司必须关注的客户群体,促销活动主要针对的是这样的客户群。 |
客户订单 | 基本客户发出的订单记录, |
事务 | 客户服务系统必须响应的一个业务事件。 |
产品 | 可以用于销售的电源产品。 |
促销 | 由市场部组织的促销活动,在促销期间,对于不同类型的客户可以提供不同的产品价格。 |
二、领域数据模型 利用我们已经讨论过的领域数据模型(早期称作上下文数据模型),我们可以根据对业务的理解,建立包括业务实体类和它们之间的自然关系,也就是构造了如下关系。 整个过程关键是对于业务的理解,没有对于业务的理解系统的分析和设计简直是不可能的,下面我们做一些说明: 1,合同绑定一个或多个基本客户,反之一个基本客户只绑定一个合同。 2,一个客户执行零个、一个或者多个事务,反之一个给定事务只能一个客户执行。 3,一个客户订单是一个事务,事实上一个客户订单也可能是多个事务(新增订单、删除订单、修改订单等),反之一个事物只能对于对应一个订单。 4,一个促销包用于打包推广一个或者多个产品,反之一个产品可能在一个或者多个促销包中存在,注意这是个多对多关系。 5,一个促销包会产生多个客户订单,反之一个订单为零个或者一个促销包。零个的原因在于,有的客户不符合促销打包的条件,将在另外的系统中解决。 6,如果不同的关系沟通不同的业务事件和关联,那么两个实体间允许存在多个关系。比如对于促销包所产生的订单,一个客户可以响应零个、一个或多个订单。另外,客户也可以主动发出零个、一个或者多个订单。当然这样的关系也可以直接写出“发出 or 响应”来表达。 7,一个订单销售一个或多个产品,反之一个产品可以有零个、一个或者多个客户,注意这是个多对多关系。 这样一个数据模型构建过程,事实上也加深了分析人员对于业务模型的理解,这是非常有意义的。 三、基于键的数据模型 一旦领域模型被构造起来,我们就可以考虑给模型建立主键,我们已经讨论了关于主键的有关问题,这里需要说明的,一般来说尽可能用简单的单一属性作为主键。 对于多对多关系,需要增加一个关联表。 四、概化层次体系的数据模型 如果促销政策比较复杂,可以考虑把促销商品通过泛化实现层次体系。 五、具有完整属性的数据模型以及规范化分析 体系结构完成以后,可以考虑写出属性,每个属性对应一个字段。由于这个例子太复杂,而且详细写出属性并不能给我们提供更多的知识,所以这里就不再讨论了。 然后必须进行规范化分析,也就是根据第1范式(1NF),函数相关,第2范式(2NF),第3范式(3NF)对表结构和数据进行检查,修改其中的不合理成分,这样数据库分析和设计可以告一段落。 上面的分析过程不论是面向过程还是面向对象,方法上几乎是相同的。