Rss & SiteMap
Foxtable(狐表) http://www.foxtable.com
编程阶段我们通常使用Java/.NET这样面向对象语言工具,可以带来很多设计上的好处,但是也存在一个奇怪的现象:很多程序员虽然在使用OO语言,但是却在code非OO的代码,最终导致系统性能降低或失败,这个现象在Java语言尤其显得突出,难怪有些人就把问题归结于Java语言本身,睡不着觉怪床歪,又为了面子问题,说自己转向.NET,实际上是在 回避自己的问题和弱点。
那么,这些人的问题和弱点体现在什么地方呢?从上面软件生产过程来看,每个阶段都对前面有所依赖, 在编程阶段出问题,追根溯源,问题无疑出在分析和设计阶段,分析设计作为一个软件产生的龙头,有着映射实际需求世界 到计算机世界这样一个拷贝任务,如何做到拷贝不走样,是衡量映射方法好坏与否的主要判断标准。
目前,将需求从客观现实世界映射到计算机软件世界主要有两种方式:传统数据库分析设计和面向对象建模( object-oriented class model), 当前软件主要潮流无疑是面向对象占据主流,虽然它可能不是唯一最好最简单的解决方案,但是它是最普通,也是最恰当的。
也就是说:在分析设计阶段,采取围绕什么为核心(是对象还是数据表为核心)的分析方法决定了后面编码阶段的编程特点,如果以数据表为核心进行分析设计, 也就是根据需求首先得到数据表名和字段,然后培训程序员学会SQL语句如何操作这些数据表,那么程序员为实现数据表的前后顺序操作, 必然会将代码写成过程式的风格。
相反,如果分析设计首先根据需求得出对象模型(class Model),那么程序员使用对象语言,再加上框架辅助,就很顺理成章走上OO编程风格。 至于OO代码相比传统过程编码的好处不是本文重点,可参考J道(jdon.com)相关讨论,扩展性和维护性好,开发越深入开发速度越快无疑是OO系统主要优点。
本文重点主要是比较OO建模和数据表建模两者特点,这两者我们已经发现属于两个不同方向,也就是说,属于两个完全不同的领域,在J道其他文章里我们 其实已经把这两个领域上升为不同的学科,数据表建模属于数学范畴思维;而OO建模属于哲学思维。
下面我们看看面向对象的Class Model和Database Model是如何来表达客观世界的,也就是他们在表达需求上有些什么不同?
面向对象模型(Class Model)
类代表一个对象类型,类在代码运行阶段将被创建为一个个对象实例, 每个类由两个部分组成:属性和行为,属性通常是一些数据状态值,也就是说:类将数据封装隐藏在自己内部了, 访问这些数据属性必须通过类公开的方法,或者接口。
别小看这样一个小小包装,却决定了以后代码的维护性和扩展性, 打个比喻,日常生活中我们经常用各种盒子和袋子包装一些东西,这样做就是为了方便这些东西的携带或储藏,小到生活, 大到客观世界每个地方,都是包装分类的影子,无论大小公司都是一个封装,行政部分单位划分,仓库物流更需要包装, 我们从来不会因为嫌麻烦而不愿意引入一个似乎多余的盒子或袋子,那么有什么理由不在我们赖之生存的软件中(靠编软件吃饭) 引入封装概念呢?
这里可以再深入想像一下:不愿意用盒子和袋子的携带东西大部分是一些急脾气的毛头小伙子,而偏偏这些小伙子又从事 软件工作,看来软件的非对象化是注定的,只是一个玩笑。
类的方法行为也有多种类型,如公开 私有等,我们可以设计一些方法为公开接口,而将另外一些行为隐藏起来, 这样一个看似简单灵活的选择,却能够应付我们日后频繁的修改,软件不修改就不叫软件,软件修改了就崩溃是业务软件, 专业的软件是抗修改的,而且能够极其方便快速地被修改。这些都依靠接口公开和隐藏这样一个简单魔术。
类的关系
我们不能只用一个一个单独的类来表达客观世界,因为客观世界存在千丝万缕的各种关系,在计算机领域无疑我们使用 类的关系来表达映射这些关系。这里我们只探讨类在建模方法上的关系,而不是UML中类的通用关系。 类在建模上主要有如下几个关系:
类与类关系经常是这样:一个类包含一个类(构造性structural),或者借助另外一个类达到某个功能(功能性), 在对需求建模分析中,构造性的这种关系,也称为关联(Association)是我们关注重点,当然这种关系很显然表达的是一种 静态的结构,比如电脑包含屏幕,他们之间的关系就是一种关联。
聚合(Aggregation)是一种表格式样的关联,表示一个类包含多项子类,这种关系是一种整体与部分的关系。 一个汽车有四个轮子,四个轮子是汽车的部分。
组成(Composition)是一种更强烈的聚合关系,一个对象实际是由其子对象组成,子对象也唯一属于父对象。
继承也是类建模中经常用到的关系,继承可以将一些数据属性抽象到父类中,避免重复,如入库单和出库单有 很多属性是差不多的,唯一不动的就是入库和出库的行为,那么我们可以抽象一个库单为父类,使用继承关系分别 表达入库单和出库单。
好了,下面我们谈论关系数据表模型,以前我们朴素的分析设计都是根据需求直接建立数据表的方式来进行的,为什么称为朴素, 是因为我们好像只有数据结构 算法方面的知识,也认为只有这样做才叫做软件。 那么既然这条路能够走出来,我们看看这个领域是如何映射客观世界的。
数据表由于技术提供庞大数据存储和可靠的数据访问,正在不断从技术领域走向社会领域,很多不懂计算机的人 也知道需要建立数据库来管理一些事务,但是不代表我们就必须围绕数据库的分析设计。
数据表是类似前面的“类”,也是一种表达客观世界的基本单元,表有多列字段,表的字段是保存数据的,每个字段有数据类型。 注意,这里没有数据的封装和公开,表的字段是赤裸的,只要有数据库访问权限,任何人都可以访问,没有结构层次关系, 都是扁平并列的,如果你想在数据表字段之间试图看出客观世界中的层次和封装,那就错了,在拷贝不走样这个条件下, 这个映射方法至少把这个信息拷贝丢了。
数据表也有一些行为,这些行为是基于实体的一些规则:
约束(Constraints) 能够保证不同表字段之间的关系完整安全性,保证数据库的数据安全。
触发器(Triggers)提供了实体在修改 新增和删除之前或之后的一些附加行为,
存储过程(Database stored procedures)提供数据专有的脚本性语言,存储过程象一个数学公式虽然具有抽象简洁美学,但是这种简洁是闷葫芦美学,不是大众美学,只有公式存储 过程发明者自己了解精通,别人无法插手,软件不是科学,不是比谁智商高,科研水平高,软件是人机工程,更讲究集体,讲究别人是否方便与你协同扩展软件。
关系数据表的遍历访问是通过列字段遍历或表join等方式实现,SQL语句是这样标准语言, 只要会写SQL语句,就能访问那些失去层次,失去客观世界特征的苍白的数据,这样的系统能够多少真实 反映客观需求,是有问号的?SQL语句是否方便修改,是否经得起频繁修改而不出错,都是有疑问的地方,是否 SQL语句越复杂,修改越快,或者另外一个程序员能够很快修改不是自己写的SQL语句,这些都是问题所在。
数据表关系
数据表的关系主要是通过外健或专门关联表来表达的,这种关系虽然可以反映1:1或1:N这样关系,但是无法 表达关系的性质,是紧密组成关系式的关联,还是无关紧要的普通关系,正因为如此,使用数据表分析设计时, 我们会有蜘蛛网的关系表,这些关系由于在后期无法分辨性质,无法进行整理,增加了系统复杂性。
更重要的是:分析就是对一个可能陌生领域进行探寻,如果使用数据表的分析设计方法,那么我们实际就是 在陌生领域中寻找数据表这样一个形式,那么有可能产生误判断,将一个实则是表达关系的东东误认为是一个实体表, 因为关系表必然带来关系,这样,就必然产生蜘蛛网式的数据表模型,将简单问题复杂化。
总结
要谈方法,这个世界其实只存在两种:一是将复杂问题简单化的方法;一个是将简单问题复杂化的方法。 你使用什么样的方法,你就有什么样的世界观,就是什么样的人,但是对于软件这个领域,你只能选择前者。
因为方法的不同,软件路线也就存在下面几个路线:完全面向对象类建模路线(J道网站和笔者一直致力于这种路线的推介); 一种是对象和关系数据库混合型,还有一种就是过去的完全关系数据库类型软件(如Foxpro/VB/Delphi等)。
我接触过N多医院信息管理系统,大部分都是C/S结构的,狐表使我这个业余爱好者也能“制造”出这样的系统。真的很棒!
而你所说的B/S结构的程序,狐表似乎不行吧?
您太抬举俺了。
浅谈数据库设计技巧
说到数据库,我认为不能不先谈数据结构。1996年,在我初入大学学习计算机编程时,当时的老师就告诉我们说:计算机程序=数据结构+算法。尽管现在的程序开发已由面向过程为主逐步过渡到面向对象为主,但我还是深深赞同8年前老师的告诉我们的公式:计算机程序=数据结构+算法。面向对象的程序开发,要做的第一件事就是,先分析整个程序中需处理的数据,从中提取出抽象模板,以这个抽象模板设计类,再在其中逐步添加处理其数据的函数(即算法),最后,再给类中的数据成员和函数划分访问权限,从而实现封装。
数据库的最初雏形据说源自美国一个奶牛场的记账薄(纸质的,由此可见,数据库并不一定是存储在电脑里的数据^_^),里面记录的是该奶牛场的收支账目,程序员在将其整理、录入到电脑中时从中受到启发。当按照规定好的数据结构所采集到的数据量大到一定程度后,出于程序执行效率的考虑,程序员将其中的检索、更新维护等功能分离出来,做成单独调用的模块,这个模块后来就慢慢发展、演变成现在我们所接触到的数据库管理系统(DBMS)——程序开发中的一个重要分支。
下面进入正题,首先按我个人所接触过的程序给数据库设计人员的功底分一下类:
1、没有系统学习过数据结构的程序员。这类程序员的作品往往只是他们的即兴玩具,他们往往习惯只设计有限的几个表,实现某类功能的数据全部塞在一个表中,各表之间几乎毫无关联。网上不少的免费管理软件都是这样的东西,当程序功能有限,数据量不多的时候,其程序运行起来没有什么问题,但是如果用其管理比较重要的数据,风险性非常大。
2、系统学习过数据结构,但是还没有开发过对程序效率要求比较高的管理软件的程序员。这类人多半刚从学校毕业不久,他们在设计数据库表结构时,严格按照教科书上的规定,死扣E-R图和3NF(别灰心,所有的数据库设计高手都是从这一步开始的)。他们的作品,对于一般的access型轻量级的管理软件,已经够用。但是一旦该系统需要添加新功能,原有的数据库表差不多得进行大换血。
3、第二类程序员,在经历过数次程序效率的提升,以及功能升级的折腾后,终于升级成为数据库设计的老鸟,第一类程序员眼中的高人。这类程序员可以胜任二十个表以上的中型商业数据管理系统的开发工作。他们知道该在什么样的情况下保留一定的冗余数据来提高程序效率,而且其设计的数据库可拓展性较好,当用户需要添加新功能时,原有数据库表只需做少量修改即可。
4、在经历过上十个类似数据库管理软件的重复设计后,第三类程序员中坚持下来没有转行,而是希望从中找出“偷懒”窍门的有心人会慢慢觉悟,从而完成量变到质变的转换。他们所设计的数据库表结构有一定的远见,能够预测到未来功能升级所需要的数据,从而预先留下伏笔。这类程序员目前大多晋级成数据挖掘方面的高级软件开发人员。
5、第三类程序员或第四类程序员,在对现有的各家数据库管理系统的原理和开发都有一定的钻研后,要么在其基础上进行二次开发,要么自行开发一套有自主版权的通用数据库管理系统。
我个人正处于第三类的末期,所以下面所列出的一些设计技巧只适合第二类和部分第三类数据库设计人员。同时,由于我很少碰到有兴趣在这方面深钻下去的同行,所以文中难免出现错误和遗漏,在此先行声明,欢迎大家指正,不要藏私哦)
一、树型关系的数据表
不少程序员在进行数据库设计的时候都遇到过树型关系的数据,例如常见的类别表,即一个大类,下面有若干个子类,某些子类又有子类这样的情况。当类别不确定,用户希望可以在任意类别下添加新的子类,或者删除某个类别和其下的所有子类,而且预计以后其数量会逐步增长,此时我们就会考虑用一个数据表来保存这些数据。按照教科书上的教导,第二类程序员大概会设计出类似这样的数据表结构:
类别表_1(Type_table_1)
名称 |
类型 |
约束条件 |
说明 |
type_id |
int |
无重复 |
类别标识,主键 |
type_name |
char(50) |
不允许为空 |
类型名称,不允许重复 |
type_father |
int |
不允许为空 |
该类别的父类别标识,如果是顶节点的话设定为某个唯一值 |
这样的设计短小精悍,完全满足3NF,而且可以满足用户的所有要求。是不是这样就行呢?答案是NO!Why?
我们来估计一下用户希望如何罗列出这个表的数据的。对用户而言,他当然期望按他所设定的层次关系一次罗列出所有的类别,例如这样:
总类别
类别1
类别1.1
类别1.1.1
类别1.2
类别2
类别2.1
类别3
类别3.1
类别3.2
……
看看为了实现这样的列表显示(树的先序遍历),要对上面的表进行多少次检索?注意,尽管类别1.1.1可能是在类别3.2之后添加的记录,答案仍然是N次。这样的效率对于少量的数据没什么影响,但是日后类型扩充到数十条甚至上百条记录后,单单列一次类型就要检索数十次该表,整个程序的运行效率就不敢恭维了。或许第二类程序员会说,那我再建一个临时数组或临时表,专门保存类型表的先序遍历结果,这样只在第一次运行时检索数十次,再次罗列所有的类型关系时就直接读那个临时数组或临时表就行了。其实,用不着再去分配一块新的内存来保存这些数据,只要对数据表进行一定的扩充,再对添加类型的数量进行一下约束就行了,要完成上面的列表只需一次检索就行了。下面是扩充后的数据表结构:
类别表_2(Type_table_2)
名称 |
类型 |
约束条件 |
说明 |
type_id |
int |
无重复 |
类别标识,主键 |
type_name |
char(50) |
不允许为空 |
类型名称,不允许重复 |
type_father |
int |
不允许为空 |
该类别的父类别标识,如果是顶节点的话设定为某个唯一值 |
type_layer |
char(6) |
限定3层,初始值为000000 |
类别的先序遍历,主要为减少检索数据库的次数 |
按照这样的表结构,我们来看看上面例子记录在表中的数据是怎样的:
type_id |
type_name |
type_father |
type_layer |
1 |
总类别 |
0 |
000000 |
2 |
类别1 |
1 |
010000 |
3 |
类别1.1 |
2 |
010100 |
4 |
类别1.2 |
2 |
010200 |
5 |
类别2 |
1 |
020000 |
6 |
类别2.1 |
5 |
020100 |
7 |
类别3 |
1 |
030000 |
8 |
类别3.1 |
7 |
030100 |
9 |
类别3.2 |
7 |
030200 |
10 |
类别1.1.1 |
3 |
010101 |
……
现在按type_layer的大小来检索一下:SELECT * FROM Type_table_2 ORDER BY type_layer
列出记录集如下:
type_id |
type_name |
type_father |
type_layer |
1 |
总类别 |
0 |
000000 |
2 |
类别1 |
1 |
010000 |
3 |
类别1.1 |
2 |
010100 |
10 |
类别1.1.1 |
3 |
010101 |
4 |
类别1.2 |
2 |
010200 |
5 |
类别2 |
1 |
020000 |
6 |
类别2.1 |
5 |
020100 |
7 |
类别3 |
1 |
030000 |
8 |
类别3.1 |
7 |
030100 |
9 |
类别3.2 |
7 |
030200 |
……
现在列出的记录顺序正好是先序遍历的结果。在控制显示类别的层次时,只要对type_layer字段中的数值进行判断,每2位一组,如大于0则向右移2个空格。当然,我这个例子中设定的限制条件是最多3层,每层最多可设99个子类别,只要按用户的需求情况修改一下type_layer的长度和位数,即可更改限制层数和子类别数。其实,上面的设计不单单只在类别表中用到,网上某些可按树型列表显示的论坛程序大多采用类似的设计。
或许有人认为,Type_table_2中的type_father字段是冗余数据,可以除去。如果这样,在插入、删除某个类别的时候,就得对type_layer 的内容进行比较繁琐的判定,所以我并没有消去type_father字段,这也正符合数据库设计中适当保留冗余数据的来降低程序复杂度的原则,后面我会举一个故意增加数据冗余的案例。
假设你是一家百货公司电脑部的开发人员,某天老板要求你为公司开发一套网上电子商务平台,该百货公司有数千种商品出售,不过目前仅打算先在网上销售数十种方便运输的商品,当然,以后可能会陆续在该电子商务平台上增加新的商品出售。现在开始进行该平台数据库的商品信息表的设计。每种出售的商品都会有相同的属性,如商品编号,商品名称,商品所属类别,相关信息,供货厂商,内含件数,库存,进货价,销售价,优惠价。你很快就设计出4个表:商品类型表(Wares_type),供货厂商表(Wares_provider),商品信息表(Wares_info):
商品类型表(Wares_type)
名称 |
类型 |
约束条件 |
说明 |
type_id |
int |
无重复 |
类别标识,主键 |
type_name |
char(50) |
不允许为空 |
类型名称,不允许重复 |
type_father |
int |
不允许为空 |
该类别的父类别标识,如果是顶节点的话设定为某个唯一值 |
type_layer |
char(6) |
限定3层,初始值为000000 |
类别的先序遍历,主要为减少检索数据库的次数 |
供货厂商表(Wares_provider)
名称 |
类型 |
约束条件 |
说明 |
provider_id |
int |
无重复 |
供货商标识,主键 |
provider_name |
char(100) |
不允许为空 |
供货商名称 |
商品信息表(Wares_info)
名称 |
类型 |
约束条件 |
说明 |
wares_id |
int |
无重复 |
商品标识,主键 |
wares_name |
char(100) |
不允许为空 |
商品名称 |
wares_type |
int |
不允许为空 |
商品类型标识,和Wares_type.type_id关联 |
wares_info |
char(200) |
允许为空 |
相关信息 |
provider |
int |
不允许为空 |
供货厂商标识,和Wares_provider.provider_id关联 |
setnum |
int |
初始值为1 |
内含件数,默认为1 |
stock |
int |
初始值为0 |
库存,默认为0 |
buy_price |
money |
不允许为空 |
进货价 |
sell_price |
money |
不允许为空 |
销售价 |
discount |
money |
不允许为空 |
优惠价 |
你拿着这3个表给老板检查,老板希望能够再添加一个商品图片的字段,不过只有一部分商品有图片。OK,你在商品信息表(Wares_info)中增加了一个haspic的BOOL型字段,然后再建了一个新表——商品图片表(Wares_pic):
商品图片表(Wares_pic)
名称 |
类型 |
约束条件 |
说明 |
pic_id |
int |
无重复 |
商品图片标识,主键 |
wares_id |
int |
不允许为空 |
所属商品标识,和Wares_info.wares_id关联 |
pic_address |
char(200) |
不允许为空 |
图片存放路径 |
程序开发完成后,完全满足老板目前的要求,于是正式启用。一段时间后,老板打算在这套平台上推出新的商品销售,其中,某类商品全部都需添加“长度”的属性。第一轮折腾来了……当然,你按照添加商品图片表的老方法,在商品信息表(Wares_info)中增加了一个haslength的BOOL型字段,又建了一个新表——商品长度表(Wares_length):
商品长度表(Wares_length)
名称 |
类型 |
约束条件 |
说明 |
length_id |
int |
无重复 |
商品图片标识,主键 |
wares_id |
int |
不允许为空 |
所属商品标识,和Wares_info.wares_id关联 |
length |
char(20) |
不允许为空 |
商品长度说明 |
刚刚改完没多久,老板又打算上一批新的商品,这次某类商品全部需要添加“宽度”的属性。你咬了咬牙,又照方抓药,添加了商品宽度表(Wares_width)。又过了一段时间,老板新上的商品中有一些需要添加“高度”的属性,你是不是开始觉得你所设计的数据库按照这种方式增长下去,很快就能变成一个迷宫呢?那么,有没有什么办法遏制这种不可预见性,但却类似重复的数据库膨胀呢?我在阅读《敏捷软件开发:原则、模式与实践》中发现作者举过类似的例子:7.3 “Copy”程序。其中,我非常赞同敏捷软件开发这个观点:在最初几乎不进行预先设计,但是一旦需求发生变化,此时作为一名追求卓越的程序员,应该从头审查整个架构设计,在此次修改中设计出能够满足日后类似修改的系统架构。下面是我在需要添加“长度”的属性时所提供的修改方案:
去掉商品信息表(Wares_info)中的haspic字段,添加商品额外属性表(Wares_ex_property)和商品额外信息表(Wares_ex_info)2个表来完成添加新属性的功能。
商品额外属性表(Wares_ex_property)
名称 |
类型 |
约束条件 |
说明 |
ex_pid |
int |
无重复 |
商品额外属性标识,主键 |
p_name |
char(20) |
不允许为空 |
额外属性名称 |
商品额外信息表(Wares_ex_info)
名称 |
类型 |
约束条件 |
说明 |
ex_iid |
int |
无重复 |
商品额外信息标识,主键 |
wares_id |
int |
不允许为空 |
所属商品标识,和Wares_info.wares_id关联 |
property_id |
int |
不允许为空 |
商品额外属性标识,和Wares_ex_property.ex_pid关联 |
property_value |
char(200) |
不允许为空 |
商品额外属性值 |
在商品额外属性表(Wares_ex_property)中添加2条记录:
ex_pid |
p_name |
1 |
商品图片 |
2 |
商品长度 |
再在整个电子商务平台的后台管理功能中追加一项商品额外属性管理的功能,以后添加新的商品时出现新的属性,只需利用该功能往商品额外属性表(Wares_ex_property)中添加一条记录即可。不要害怕变化,被第一颗子弹击中并不是坏事,坏的是被相同轨道飞来的第二颗、第三颗子弹击中。第一颗子弹来得越早,所受的伤越重,之后的抵抗力也越强
三、多用户及其权限管理的设计 .
研发数据库管理类的软件,不可能不考虑多用户和用户权限配置的问题。尽管现在市面上的大、中型的后台数据库系统软件都提供了多用户,连同细至某个数据库内某张表的权限配置的功能,我个人建议:一套成熟的数据库管理软件,还是应该自行设计用户管理这块功能,原因有二:
1.那些大、中型后台数据库系统软件所提供的多用户及其权限配置都是针对数据库的共有属性,并不一定能完全满足某些特例的需求;
2.不要过多的依赖后台数据库系统软件的某些特别功能,多种大、中型后台数据库系统软件之间并不完全兼容。否则一旦日后需要转换数据库平台或后台数据库系统软件版本升级,之前的架构设计很可能无法重用。
下面看看如何自行设计一套比较灵活的多用户管理模块,即该数据库管理软件的系统管理员能够自行添加新用户,修改已有用户的权限,删除已有用户。首先,分析用户需求,列出该数据库管理软件任何需要实现的功能;然后,根据一定的联系对这些功能进行分类,即把某类用户需使用的功能归为一类;最后开始建表:
功能表(Function_table)
名称 |
类型 |
约束条件 |
说明 |
f_id |
int |
无重复 |
功能标识,主键 |
f_name |
char(20) |
不允许为空 |
功能名称,不允许重复 |
f_desc |
char(50) |
允许为空 |
功能描述 |
用户组表(User_group)
名称 |
类型 |
约束条件 |
说明 |
group_id |
int |
无重复 |
用户组标识,主键 ! |
group_name |
char(20) |
不允许为空 |
用户组名称 |
group_power |
char(100) |
不允许为空 |
用户组权限表,内容为功能表f_id的集合 |
用户表(User_table)
名称 |
类型 |
约束条件 |
说明 |
user_id |
int |
无重复 |
用户标识,主键 |
user_name |
char(20) |
无重复 |
用户名 |
user_pwd |
char(20) |
不允许为空 |
用户密码 |
user_type |
int |
不允许为空 |
所属用户组标识,和User_group.group_id关联 |
采用这种用户组的架构设计,当需要添加新用户时,只需指定新用户所属的用户组;当以后系统需要添加新功能或对旧有功能权限进行修改时,只用操作功能表和用户组表的记录,原有用户的功能即可相应随之变化。当然,这种架构设计把数据库管理软件的功能判定移到了前台,使得前台研发相对复杂一些。但是,当用户数较大(10人以上),或日后软件升级的概率较大时,这个代价是值得的。
名称 |
类型 |
约束条件 |
说明 |
book_id |
int |
无重复 |
书籍标识,主键 |
book_no |
char(20) |
无重复 |
书籍编号 |
book_name |
char(100) |
不允许为空 |
书籍名称 |
……
借阅用户表(Renter_table)
名称 |
类型 |
约束条件 |
说明 |
renter_id |
int |
无重复 |
用户标识,主键 |
renter_name |
char(20) |
不允许为空 |
用户姓名 |
……
借阅记录表(Rent_log)
名称 |
类型 |
约束条件 |
说明 |
rent_id |
int |
无重复 |
借阅记录标识,主键 |
r_id |
int |
不允许为空 |
用户标识,和Renter_table.renter_id关联 |
b_id |
int |
不允许为空 |
书籍标识,和Book_table.book_id关联 |
rent_date |
datetime |
不允许为空 |
借阅时间 |
……
为了实现按批查询借阅记录,我们能够再建一个表来保存批量借阅的信息,例如:
批量借阅表(Batch_rent)
名称 |
类型 |
约束条件 |
说明 |
batch_id |
int |
无重复 |
批量借阅标识,主键 . |
batch_no |
int |
不允许为空 |
批量借阅编号,同一批借阅的batch_no相同 |
rent_id |
int |
不允许为空 |
借阅记录标识,和Rent_log.rent_id关联 |
batch_date |
datetime |
不允许为空 |
批量借阅时间 |
这样的设计好吗?我们来看看为了列出某个用户某次借阅的任何书籍,需要如何查询?首先检索批量借阅表(Batch_rent),把符合条件的的任何记录的rent_id字段的数据保存起来,再用这些数据作为查询条件带入到借阅记录表(Rent_log)中去查询。那么,有没有什么办法改进呢?下面给出一种简洁的批量设计方案,不需添加新表,只需修改一下借阅记录表(Rent_log)即可。修改后的记录表(Rent_log)如下:
借阅记录表(Rent_log)
名称 |
类型 |
约束条件 |
说明 |
rent_id |
int |
无重复 |
借阅记录标识,主键 |
r_id |
int |
不允许为空 |
用户标识,和Renter_table.renter_id关联 |
b_id |
int |
不允许为空 |
书籍标识,和Book_table.book_id关联 |
batch_no |
int |
不允许为空 |
量借阅编号,同一批借阅的batch_no相同 |
rent_date |
datetime |
不允许为空 |
借阅时间 |
...
……
其中,同一次借阅的batch_no和该批第一条入库的rent_id相同。举例:假设当前最大rent_id是64,接着某用户一次借阅了3本书,则批量插入的3条借阅记录的batch_no都是65。之后另外一个用户租了一套碟,再插入出租记录的rent_id是68。采用这种设计,查询批量借阅的信息时,只需使用一条标准T_SQL的嵌套查询即可。当然,这种设计不符合3NF,但是和上面标准的3NF设计比起来,哪一种更好呢?答案就不用我说了吧。
五、冗余数据的取舍
上篇的“树型关系的数据表”中保留了一个冗余字段,这里的例子更进一步——添加了一个冗余表。先看看例子:我原先所在的公司为了解决员工的工作餐,和附近的一家小餐馆联系,每天吃饭记账,费用按人数平摊,月底由公司现金结算,每个人每个月的工作餐费从工资中扣除。当然,每天吃饭的人员和人数都不是固定的,而且,由于每顿工作餐的所点的菜色不同,每顿的花费也不相同。例如,星期一中餐5人花费40元,晚餐2人花费20,星期二中餐6人花费36元,晚餐3人花费18元。为了方便计算每个人每个月的工作餐费,我写了一个简陋的就餐记账管理程式,数据库里有3个表:
员工表(Clerk_table)
名称 |
类型 |
约束条件 |
说明 |
clerk_id |
int |
无重复 |
员工标识,主键 |
clerk_name |
char(10) |
不允许为空 |
员工姓名 |
每餐总表(Eatdata1)
名称 |
类型 |
约束条件 |
说明 |
totle_id |
int |
无重复 |
每餐总表标识,主键 |
persons |
char(100) |
不允许为空 |
就餐员工的员工标识集合 |
eat_date |
datetime |
不允许为空 |
就餐日期 |
eat_type |
char(1) |
不允许为空 |
就餐类型,用来区分中、晚餐 |
totle_price |
money |
不允许为空 |
每餐总花费 |
persons_num |
int |
不允许为空 |
就餐人数 |
就餐计费细表(Eatdata2)
名称 |
类型 |
约束条件 |
说明 |
id |
int |
无重复 |
就餐计费细表标识,主键 |
t_id |
int |
不允许为空 |
每餐总表标识,和Eatdata1.totle_id关联 |
c_id |
int |
不允许为空 |
员工标识标识,和Clerk_table.clerk_id关联 |
price |
money |
不允许为空 |
每人每餐花费 |
其中,就餐计费细表(Eatdata2)的记录就是把每餐总表(Eatdata1)的一条记录按就餐员工平摊拆开,是个不折不扣的冗余表。当然,也能够把每餐总表(Eatdata1)的部分字段合并到就餐计费细表(Eatdata2)中,这样每餐总表(Eatdata1)就成了冗余表,但是这样所设计出来的就餐计费细表重复数据更多,相比来说还是上面的方案好些。但是,就是就餐计费细表(Eatdata2)这个冗余表,在做每月每人餐费统计的时候,大大简化了编程的复杂度,只用类似这么一条查询语句即可统计出每人每月的寄餐次数和餐费总帐:
SELECT clerk_name AS personname,COUNT(c_id) as eattimes,SUM(price) AS ptprice
FROM Eatdata2 JOIN Clerk_tabsle ON (c_id=clerk_id) JOIN eatdata1 ON (totleid=tid)
WHERE eat_date>=CONVERT(datetime,'"&the_date&"')
AND
eat_date<DATEADD(month,1,CONVERT(datetime,'"&the_date&"'))
GROUP BY c_id
想象一下,假如不用这个冗余表,每次统计每人每月的餐费总帐时会多麻烦,程式效率也够呛。那么,到底什么时候能够增加一定的冗余数据呢?我认为有2个原则:
1、用户的整体需求。当用户更多的关注于,对数据库的规范记录按一定的算法进行处理后,再列出的数据。假如该算法能够直接利用后台数据库系统的内嵌函数来完成,此时能够适当的增加冗余字段,甚至冗余表来保存这些经过算法处理后的数据。要知道,对于大批量数据的查询,修改或删除,后台数据库系统的效率远远高于我们自己编写的代码。 对真正的成功者来说,不论他的生存条件如何,都不会自我磨灭
2、简化研发的复杂度。现代软件研发,实现同样的功能,方法有很多。尽管不必需要程式员精通绝大部分的研发工具和平台,但是还是需要了解哪种方法搭配哪种研发工具的程式更简洁,效率更高一些。冗余数据的本质就是用空间换时间,尤其是现在硬件的发展远远高于软件,所以适当的冗余是能够接受的。但是我还是在最后再强调一下:不要过多的依赖平台和研发工具的特性来简化研发,这个度要是没把握好的话,后期维护升级会栽大跟头的。
对真正的成功者来说,不论他的生存条件如何,都不会自我磨灭