第一范式(1NF):在关系模式R中的每一个具体关系r中,如果每个属性值 都是不可再分的最小数据单位,则称R是第一范式的关系。例:如职工号,姓名,电话号码组成一个表(一个人可能有一个办公室电话 和一个家里电话号码) 规范成为1NF有三种方法:
一是重复存储职工号和姓名。这样,关键字只能是电话号码。
二是职工号为关键字,电话号码分为单位电话和住宅电话两个属性
三是职工号为关键字,但强制每条记录只能有一个电话号码。
以上三个方法,第一种方法最不可取,按实际情况选取后两种情况。
第二范式(2NF):如果关系模式R(U,F)中的所有非主属性都完全依赖于任意一个候选关键字,则称关系R 是属于第二范式的。
例:选课关系 SCI(SNO,CNO,GRADE,CREDIT)其中SNO为学号, CNO为课程号,GRADEGE 为成绩,CREDIT 为学分。 由以上条件,关键字为组合关键字(SNO,CNO)
在应用中使用以上关系模式有以下问题:
a.数据冗余,假设同一门课由40个学生选修,学分就 重复40次。
b.更新异常,若调整了某课程的学分,相应的元组CREDIT值都要更新,有可能会出现同一门课学分不同。
c.插入异常,如计划开新课,由于没人选修,没有学号关键字,只能等有人选修才能把课程和学分存入。
d.删除异常,若学生已经结业,从当前数据库删除选修记录。某些门课程新生尚未选修,则此门课程及学分记录无法保存。
原因:非关键字属性CREDIT仅函数依赖于CNO,也就是CREDIT部分依赖组合关键字(SNO,CNO)而不是完全依赖。
解决方法:分成两个关系模式 SC1(SNO,CNO,GRADE),C2(CNO,CREDIT)。新关系包括两个关系模式,它们之间通过SC1中的外关键字CNO相联系,需要时再进行自然联接,恢复了原来的关系
第三范式(3NF):如果关系模式R(U,F)中的所有非主属性对任何候选关键字都不存在传递信赖,则称关系R是属于第三范式的。
例:如S1(SNO,SNAME,DNO,DNAME,LOCATION) 各属性分别代表学号,
姓名,所在系,系名称,系地址。
关键字SNO决定各个属性。由于是单个关键字,没有部分依赖的问题,肯定是2NF。但这关系肯定有大量的冗余,有关学生所在的几个属性DNO,DNAME,LOCATION将重复存储,插入,删除和修改时也将产生类似以上例的情况。
原因:关系中存在传递依赖造成的。即SNO -> DNO。 而DNO -> SNO却不存在,DNO -> LOCATION, 因此关键辽 SNO 对 LOCATION 函数决定是通过传递依赖 SNO -> LOCATION 实现的。也就是说,SNO不直接决定非主属性LOCATION。
解决目地:每个关系模式中不能留有传递依赖。
解决方法:分为两个关系 S(SNO,SNAME,DNO),D(DNO,DNAME,LOCATION)
注意:关系S中不能没有外关键字DNO。否则两个关系之间失去联系。
BCNF:如果关系模式R(U,F)的所有属性(包括主属性和非主属性)都不传递依赖于R的任何候选关键字,那么称关系R是属于BCNF的。或是关系模式R,如果每个决定因素都包含关键字(而不是被关键字所包含),则RCNF的关系模式。
例:配件管理关系模式 WPE(WNO,PNO,ENO,QNT)分别表仓库号,配件号,职工号,数量。有以下条件
a.一个仓库有多个职工。
b.一个职工仅在一个仓库工作。
c.每个仓库里一种型号的配件由专人负责,但一个人可以管理几种配件。
d.同一种型号的配件可以分放在几个仓库中。
分析:由以上得 PNO 不能确定QNT,由组合属性(WNO,PNO)来决定,存在函数依赖(WNO,PNO) -> ENO。由于每个仓库里的一种配件由专人负责,而一个人可以管理几种配件,所以有组合属性(WNO,PNO)才能确定负责人,有(WNO,PNO)-> ENO。因为 一个职工仅在一个仓库工作,有ENO -> WNO。由于每个仓库里的一种配件由专人负责,而一个职工仅在一个仓库工作,有 (ENO,PNO)-> QNT。
找一下候选关键字,因为(WNO,PNO) -> QNT,(WNO,PNO)-> ENO ,因此 (WNO,PNO)可以决定整个元组,是一个候选关键字。根据ENO->WNO,(ENO,PNO)->QNT,故(ENO,PNO)也能决定整个元组,为另一个候选关键字。属性ENO,WNO,PNO 均为主属性,只有一个非主属性QNT。它对任何一个候选关键字都是完全函数依赖的,并且是直接依赖,所以该关系模式是3NF。
分析一下主属性。因为ENO->WNO,主属性ENO是WNO的决定因素,但是它本身不是关键字,只是组合关键字的一部分。这就造成主属性WNO对另外一个候选关键字(ENO,PNO)的部 分依赖,因为(ENO,PNO)-> ENO但反过来不成立,而P->WNO,故(ENO,PNO)-> WNO 也是传递依赖。
虽然没有非主属性对候选关键辽的传递依赖,但存在主属性对候选关键字的传递依赖,同样也会带来麻烦。如一个新职工分配到仓库工作,但暂时处于实习阶段,没有独立负责对某些配件的管理任务。由于缺少关键字的一部分PNO而无法插入到该关系中去。又如某个人改成不管配件了去负责安全,则在删除配件的同时该职工也会被删除。
解决办法:分成管理EP(ENO,PNO,QNT),关键字是(ENO,PNO)工作EW(ENO,WNO)其关键字是ENO
缺点:分解后函数依赖的保持性较差。如此例中,由于分解,函数依赖(WNO,PNO)-> ENO 丢失了, 因而对原来的语义有所破坏。没有体现出每个仓库里一种部件由专人负责。有可能出现 一部件由两个人或两个以上的人来同时管理。因此,分解之后的关系模式降低了部分完整性约束。
一个关系分解成多个关系,要使得分解有意义,起码的要求是分解后不丢失原来的信息。这些信息不仅包括数据本身,而且包括由函数依赖所表示的数据之间的相互制约。进行分解的目标是达到更高一级的规范化程度,但是分解的同时必须考虑两个问题:无损联接性和保持函数依赖。有时往往不可能做到既有无损联接性,又完全保持函数依赖。需要根据需要进行权衡。
1NF直到BCNF的四种范式之间有如下关系:
BCNF包含了3NF包含2NF包含1NF
小结:
目地:规范化目的是使结构更合理,消除存储异常,使数据冗余尽量小,便于插入、删除和更新
原则:遵从概念单一化 "一事一地"原则,即一个关系模式描述一个实体或实体间的一种联系。规范的实质就是概念的单一化。
方法:将关系模式投影分解成两个或两个以上的关系模式。
要求:分解后的关系模式集合应当与原关系模式"等价",即经过自然联接可以恢复原关系而不丢失信息,并保持属性间合理的联系。
注意:一个关系模式结这分解可以得到不同关系模式集合,也就是说分解方法不是唯一的。最小冗余的要求必须以分解后的数据库能够表达原来数据库所有信息为前提来实现。其根本目标是节省存储空间,避免数据不一致性,提高对关系的操作效率,同时满足应用需求。实际上,并不一定要求全部模式都达到BCNF不可。有时故意保留部分冗余可能更方便数据查询。尤其对于那些更新频度不高,查询频度极高的数据库系统更是如此。
在关系数据库中,除了函数依赖之外还有多值依赖,联接依赖的问题,从而提出了第四范式,第五范式等更高一级的规范化要求。在此,以后再谈。
各位朋友,你看过后有何感想,其实,任何一本数据库基础理论的书都会讲这些东西,考虑到很多网友是半途出家,来做数据库。特找一本书大抄特抄一把,各位有什么问题,也别问我了,自已去找一本关系数据库理论的书去看吧,说不定,对各位大有帮助。说是说以上是基础理论的东西,请大家想想,你在做数据库设计的时候有没有考虑过遵过以上几个范式呢,有没有在数据库设计做得不好之时,想一想,对比以上所讲,到底是违反了第几个范式呢?
我见过的数据库设计,很少有人做到很符合以上几个范式的,一般说来,第一范式大家都可以遵守,完全遵守第二第三范式的人很少了,遵守的人一定就是设计数据库的高手了,BCNF的范式出现机会较少,而且会破坏完整性,你可以在做设计之时不考虑它,当然在ORACLE中可通过触发器解决其缺点。以后我们共同做设计之时,也希望大家遵守以上几个范式。
关键词:Oracle数据库 分区Partition 表空间Tablespace 数据文件Datafile
伴随着信息高速公路的飞速建设,油田的各项勘探开发数据都做到了及时准确入库,数据库中数据量日益增加。以其下属的某个采油厂为例,数据量已达到2GB,各种数据库表更是多达1千多个。与此同时,又产生了一个新问题,那就是虽然各种生产数据都已入库,但是由于数据量巨大,造成查询速度非常缓慢。
本文以油井日度数据表(dba01)为例进行说明。该表是最基础的开发数据,每天每一口井都有记录进入到数据库中。油田规定,该数据15个月内必须保存在线,15个月下来这个表就有997890条记录。
这接近100万条的记录大大增加系统开销,在用户提交查询后,经常需要等待五六分钟才能得到结果,有时甚至查不出数据,给用户的感觉是仿佛处于“死机”状态。
1 常规解决办法
解决大表查询速度缓慢的问题,最初的对策是在后台创建很多中间表。
例如:要得到采油厂生产日数据汇总情况屯解全厂每天的油井开井数、水井开井数、日产油量、注采比等重要数据,其缺点主要有两个:
(1)中间表的建立会占用大量表空间,即查询速度的提高是以牺牲服务器空间为代价,造成了巨大的资源浪费;
(2)随着各种应用的不断开展,中间表的数量也越来越多,这样人为加大了数据管理和维护的工作量。
因此,要从根本上解决大表存在的查询速度缓慢的问题,必须找到更为有效的方法。
2 采用分区功能解决问题
(1)分区的定义
分区将表分离在若干不同的表空间上,将大的表和索引拆分成小的易于管理的数据片段,分而治之支撑无限膨胀的大表,给大表物理一级的可管理性。将大表分割成较小的分区可以改善表的维护、备份、恢复、事务及查询性能。针对我厂大量的油水井日度数据,可以推荐使用Oracle9的分区功能。
(2)分区的优点
首先,能够成倍提高查询速度:分区管理后,服务器可以进行智能的分区检测。跳过与查询无关的分区访问,并跳过不在线的分区。
其次,增强系统可用性:如果表的一个分区由于系统故障而不能使用,其余好的分区仍然可能使用。
不同分区可以跨表空间存储,降低了磁盘损坏带来的数据不可用性。
3 分区的管理
(1)建立表的各个分区的表空间
下面是建立2004年第一季度表空间的操作语句,指定所建立表空间的名字,所用数据文件的名称、大小和存放目录,并由存储字句指定在该空间中所建立对象的缺省存储参数。
CREATE TABLESPACE ts_dba01_2004q1
DATAFILE '/home2/oracle/test/dba01/2004q1.SIZE 200MB
DEFAULT STORAGE (INITIAL 30m NEXT 30m)
MINEXTENTS 3 PCTINCREASE 0);
(2)建立分区表
下面是建立dba01表的操作语句,指定表名、列名及列的数据类型等。这些都与常规创建表的语句相同。
CREATE TABLE dba01
(jh varchar2(16)not null,
rq date not null,
cyfs varchar2(3),
dwdm varchar2(11),
……
PARTITION BY RANGE(rq)
PARTITION dba01_2003q4
VALUES LESS THAN(TO_DATE('2004-01-01','YYYY-MM-DD')
TABLESDPACE ts_dba01_2003q4,
PARTITION dba01_2004q1
VALUES LESS THAN(TO_DATE('2004-04-01','YYYY-MM-DD')
TABLESPACE ts_dba01_2004q1)
这是创建分区的语句,指定指照日期进行分区,例如:
日期>='2004-01-01'并且<'2004-04-01'(即2004年第一季度)的数据放在dba01_2004q1分区内。其他分区也依此原则建立。
(3)分区表的扩充
在2004年年底,向表中加入2005年的表空间,同样是每季度一个表空间,预计每个分区为200MB。下面是创建2005年第一季度表空间的操作语句,指定表空间名称、数据文件名称及大小等。
CREATE TABLESPACE ts_dba01_2005q1
DATAFILE '/home2/oracle/dba01_2005q1.dat'SIZE
200MB
DEFAULT STORAGE (INITIAL 40m NEXT40m)
MINEXTENTS 3 PCTINCREASE 0)
其他季度表空间也如此建立。
(4)为表添加表空间
操作语句如下:
ALTER TABLE dba01
ADD PARTITION dba01_2005q1
VALUES LESS THAN(TO_DATE('2005-04-01','YYYY-MM-DD')
TABLESPACE ts_dba01_2005q1;
(5)删除不必要的分区
采油厂规定:必须保存15个月的日度数据在线。到2005年,必须把2003年3季度的数据备份,将该分区删除,腾出空间供后续数据循环使用。删除分区ALTER TABLE dba01 DROP PARTION dba01_2003q3;
利用操作系统工具删除这个分区所占用的文件
oracle$ rm /home2/oracle/test/dba01_2003q3.dat
(6)查看分区信息
可通过对数据字典USER_EXTENTS进行查询,操作如下:
SVRMGRL>SELECT *FROM user_extents
WHERE SEGMENT_NAME='dba01';
(7)卸载分区
Oracle9的EXPORT工具可卸载分区并导出数据,例如到2002年,可将2000年的数据按分区卸载。
例如:要卸载2003年3季度的数据,数据如下:
oracle$ exp tycx/***
tables=dba01:dba01_2003d3 file=dba01_2003q3.dmp
在语句中要指定用户名、口令、需要卸出的表名及分区名、卸出文件名称等。
(8)导入分区
Oracle9的IMPORT工具可导入分区并加载数据,例如在2005年,用户要查看2003年的数据,必须导入该年数据。
·建立该表2003年的四个表空间和相应的分区;
·下面是导入2003年3季度分区数据的操作语句:
oracle$ imp tycx/***
file=dba01_2003q3.dmp tables=(dba01:dba01_2003q3)
4 实验效果
(1)能够成倍提高查询速度
分区管理后,服务器可以进行智能的分区检测,跳过与查询无关的分区访问,跳过不在线的分区。
(2)增强系统可用性
如果表的一个分区由于系统故障而不能使用,其余好的分区仍然可以使用。
不同分区可以跨表空间存储,降低了磁盘损坏带来的数据不可用性。
以油井日数据表为例:
不采用分区技术时,若表空间文件受到破坏,会影响到所有数据都无法使用,必须将该表全部记录(多达100万条)重新恢复,工作量很大,恢复期间用户根本无法查询数据,完全不能使用。
而采用分区技术后,由于整个表已按季度拆分为6个分区,因此当某一个表空间文件被破坏,则仅是该分区表空间所对应的季度数据无法使用,其他季度数据仍然可以正常使用,对用户的查询以及其他应用影响不大。
通过合理应用Oracle9的分区功能,可以大大改善系统的性能,降低大表数据管理和维护的工作量,对大表的查询、增加、修改等操作可以分解到表的不同分区并行执行,可使运行速度更快。对促进无纸化办公,辅助生产有积极的推动作用。
在开始做http://133.newsky.cn之前,我已经明白网站的开发与产品开发没有什么不同。不过在2004年离开微软中国研发中心Office组的时候,我对网站开发仍一无所知,这主要是因为我之前没有任何互联网研发的背景。虽然对传统软件产品的研发管理比较有经验,但从未接触过Internet相关的项目。
从零开始与网站开发亲密接触
去年我接手第一个网站项目http://www.okooo.com开发时,并没有做网站的经验,只能试着按照以前我参与做Microsoft Office时的方法来做:
首先是打造一个便于公司内部沟通交流的内部网,其中包含“传统软件”研发需要的三个工具:文档库(存放公司各项目的文档)、CVS(保存项目的各种源代码)、BugFree(记录项目的各种缺陷);
然后,抓住“需求、开发、测试”三个环节:
l 要做好规划、明确需求。为什么要做这个网站、要达到什么目标?特别是需求,要详细到每个页面的每个区域放置什么内容。网站需求应该由对业务最熟悉的人来定义,他负责按照我要求的规范(详细程度)来写出每一部分需求文档,并放入文档库中。每完成一个页面定义,我就召集开发、测试人员来阅读、讨论,这样全部需求写完的时候,项目组成员对整个网站就有了一个清晰的认识。
l 需求明确才进入开发阶段。首先是定义数据库——有多少张表、每张表中有多少个字段。我和开发组长反复讨论,搞清楚这些表定义能否涵盖全部需求,这是最关键的一步,决定着下面编码能否顺利进行。数据库定义后,就是网站后台管理的编码实现,也就是对一张张表进行管理(增、删、改)。当后台管理完成时,项目的大部分就大功告成了。用户看到的前台页面仅仅是内容展示——把一张张表中的数据取出来按照最初的需求放置到页面的各个位置。所有的代码都用CVS管理起来。
l 网站测试和开发同步进行。后台管理每完成若干张表的管理,测试人员立即开始测试。这就像流水线,开发完一部分,立刻测试;同样的,网站前台展示开发时也一样需要测试人员跟进。发现的每一个Bug都用BugFree记录下来跟踪处理过程。
l 数据统计跟上。网站后台各个表的任何改动要准确记录,决不允许出现不知道谁修改了数据库内容的情况。其次,网友访问网站的日志要做好统计,每天结束的时候就能准确的看到当天的用户访问数据。这些数据对网站运营极其重要。
四个月后,我的第一个网站项目顺利上线。所有参与该项目的同事感觉都很新鲜,因为以前他们在做网站时,基本上是一个人“包干”一个频道,简单构思一下就开始写程序、边写边想、相互独立。后来,我跟一位曾在某门户网站工作过的高级工程师朋友介绍了上面的做法,他非常认同和赞赏,得到他的认可我也很兴奋。
随后接触到的很多网站技术人员,让我发觉作坊式做法同样存在于互联网公司,网站在重复多年前传统软件的老路:一个“大虾”很厉害,搞定一个频道或一个网站的方方面面,离开他谁都玩不转;代码中处处留着他的灵感,人走了,网站维护就成了大难题:没有文档、没有统一的编码规范、没有测试记录。
其实无论传统软件、网站、还是游戏等等软件产品/项目,都是程序员用一行行代码敲出来的,只要像微软软件研发那样抓住需求、开发、测试这三个环节,其管理都极其类似。因此当我进入http://133.newsky.cn网站项目的时候,信心十足:我能把它管好!
打造一个网站开发的品牌项目
在我加入金环天朗的时候,这个网站就已经存在了,而最开始的计划也只是对原有的网站进行局部改版。但是等我深入了解后,大吃一惊:
u 规划/需求:原有网站没有经过认真规划就匆忙上马,只有部分的简单示意图,对于每个页面具体区域的功能描述和逻辑过程还是依赖口头沟通。没有独立的后台管理,依赖于WAP业务的后台,内容展示力不从心。
u 页面设计:美工因为还有其它工作所以有一定程度的拖延,没有时间观念,整个设计方案没有经过整体评估,导致后来许多细节没有按照计划实现,页面设计先后由两人分头独立完成,导致部分风格不一致。
u 开发:技术实现一直处在救火的状态,没有规划,没有步骤,没有主次之分,没有时间观念。代码的结构非常散乱,没有可用的文档查询,开发人员走了,给以后接手的人带来极大的麻烦。代码没有规范、没有注释。归结起来就是可读性很差。
u 测试:没有任何测试,开发人员简单试一试就直接上线了!
u 内容:网站内容维护没有专人负责,逐渐处于无人答理的状态。
总之,原来的网站有太多不尽人意之处,和同类网站比起来差距较大,市场人员无法推广,技术人员很难维护,动不动就出错。只能另起炉灶,推倒重做一个全新的网站。
对一家SP公司而言,做网站是打通让用户消费的通道。从常远看,内容为王,但短期内通道为王:就是让用户很容易找到公司提供的内容。因为WAP业务非常依赖于运营商的门户排名,一个业务放在运营商WAP门户上,第一屏和第二屏有着本质的不同,愿意翻到第2屏上的用户可能少一半或更多!所以SP要想尽一切办法来摆脱对门户的唯一依赖,必须能用别的通道让用户很方便的找到你的业务。而网站就是最好的宣传通道,是公司产品最重要的展示平台。网站研发的目标就是尽快打通联通、移动用户的消费通道,把公司生产出来的产品(图、铃、文字)方便地展示给更多的手机用户。
这个http://133.newsky.cn网站是面向中国联通用户的,其设计目标是:
? 1~3年内不需要改动大框架
? 公司业务内容的精美展示、销售平台
? 在同行中有很强的竞争力
? 老板可以拿出来给投资人演示
为了达成这个设计目标,我和项目组花了近一个月的时间来制定完整规划。
| 规划 | 需求 | 美工 | 开发 | 测试 | 运营 |
收到老板Email,项目启动 | | | | | | |
完成规划 | 启动前台展示需求的定义 | | | | | |
| 开始后台管理需求定义 | | | | | |
| 完成需求定义。确定后面的时间进度:6/15正式上线运营! | 开始后台管理页面设计 | 开始网站数据库的设计 | | | |
| | | 完成“后台管理详细设计”的文档 | | | |
| | | 开始后台管理的编码 | | | |
| | 开始前台展示页面设计 | | | | |
| | | 完成后台管理的编码 | | | |
| | | | 引入测试组,开始后台管理的测试 | | |
| | | 两名新人到岗,开始前台展示的编码 | | | |
| | | | | 确定运营组成员及分工 | |
| | | 主要编码结束 | | | |
| | | | 测试完毕 | 开始录入内容 | |
| | | | | 内容全部上线 | |
http://133.newsky.cn正式上线运营,向公司全体同事通报! | ||||||
完成Postmortem(项目总结),为下个移动网站项目做准备 |
明确的研发流程应该是一个开发团队的固定资产,从这点上,我建立了一套项目研发流程,并为其提供工具支撑:
? 认识老网站的现状、确定新网站的设计目标;对新网站的总体设计图纸进行反复讨论,确定网站研发的四个总原则(灵活的后台、以专题为网站细胞、丰富的资讯、翔实的内容);明确人员分工、并预告项目执行的几个关键点。
? 在没有公司内部网的情况下,我先搭建两个工具:用于保存各种文档和源代码的TortoiseCVS(客户端)+CVS(服务器端),用于缺陷管理的BugFree。为每个项目搭建一个CVS模块,其中都有四个子目录:Spec(需求文档)、Design(设计文档)、Code(源代码)、Test(测试文档)。
示意图:网站项目的CVS目录
然后是人力资源,我在规划中提出了非常明确的人力资源需求:
? 前台需求定义:1人(蔡志宏)
? 后台需求定义:2人(刘振飞、朱伟波)
? 美工设计制作:1人
? 开发:3人
? 测试:5人
? 运营:5人
然而,当时的情况却是项目组人员迟迟无法到位:美工只有一个兼职的、时间无法保证;只有一个开发组长;没有测试人员;网站运营人员不能确定。针对这样的情况,我的任务还包括了招聘相关人员及时到位。
在整体上完成上述工作以后,时间已经是
网站需求特别难以确定,为了解决这个问题,我将整个需求定义划分为三个主要的部分:
1.网站前台展示的定义
我首先和负责定义需求的蔡志宏确定了需求Spec文档模板,然后他根据首页、二级、三级页面逐个页面、逐个模块的去定义:展示什么内容,大概的模样(最终样式由美工负责)。这样每个页面都被分解成一块块的“部件”,一个“部件”由一份Spec描述,比如下面是“首页公告栏区域需求定义”Spec的示意图。
示意图:首页公告栏区域的需求定义Spec
每完成若干相关联的Spec,我就召集美工、开发人员开会讨论(本应该也叫上测试和运营人员,但当时还没有人),大家站在不同的角度去看看有无问题,并最终确认下来。
2.联通用户消费流程的定义
用户消费流程涉及到收费问题,必须把每个细节都要搞清楚。这个需求由我负责,先形成一份PPT文档,在大范围内征求大家的意见,然后细化每个细节:从用户访问我们的首页开始,如何登录,如何转向联通网站,如何扣费等每个细节必须想到。
3.网站后台管理的定义
根据网站前台的需求,我和开发组长朱伟波来设计数据库定义,确定多少张表、每张表中有什么字段。然后从运营人员的登录页面开始设计,用PPT把每张页面的示意图以及逻辑关系都展示出来,然后把需求、开发、美工召集起来一起讨论,看看是否符合运营人员的习惯、是否有遗漏的地方。
需求文档要想清楚后再写下来,让别人读得懂。定义好的需求Spec是整个项目开发的“合同”,马虎不得。在需求定义的3周中(其中前台展示的需求用了2周、后台管理的需求用了1周),每写出来若干相关的需求文档,就在项目组内讨论一次,最终明确下来。需求文档一旦成型以后,就必须严格按照需求文档编写设计代码,尽量控制需求的变化。这不但要求我们在最开始的需求分析阶段做好最充分的准备工作,而且还需要作为项目经理的我,顶住一些来自各方意见的压力。幸运的,我们团队还是非常好的坚持下来了:
示意图:上线后的首页公告栏区域——完全根据Spec中的需求定义来实现
然而,另外的一个问题是,需求文档很容易“老旧”、跟不上最新的变化,需求定义人也懒得去更新,因为开始编码后谁都不去注意需求文档了。为了解决这个问题,我就在后台管理的每一张表的维护页面上,增加一个“Spec”按钮,点击后就可以看到相关的需求文档Spec列表了。这样做有两个好处:一方面运营人员可以很方便的看到最初是怎么设计这块功能的;另一方面也把需求定义者的工作暴露在全体同事面前,文档写的好坏是一目了然。
示意图:每个后台表管理页面上都有个Spec按钮,指向对应的需求文档列表
充分的需求定义保证了整个项目能够准时完工,这也是我们这个项目能够取得圆满成功的原因之一。需求确定之后,后面开发、测试的时间就基本明确下来了。
有了完整的需求文档后,接下来就进入开发阶段。如同前面提及的,首先需要完成的是数据库的设计。其实早在需求定义期间,我和朱伟波就已经开始数据库定义,确定多少张表、每张表中有什么字段。我们花费了三天左右的时间来对后台数据库进行详细的设计,并产生出设计文档。
示意图:新天地网站2005版后台数据库定义.doc
然而,光有需求和详细设计文档还不够,开发团队需要保持要一种一致的风格,这一点要求所有的程序员对代码有责任感。因此在这个阶段之前(3/16~4/12),我就让公司所有的Java工程师多次讨论,并最后确定一份“编码规范”,这样网站真正开始写代码的时候,就有一个明确的规范来约束代码的书写。
对于软件项目来说,经常会有一些出乎意料的情况发生。比如,本来计划有两个开发人员做后台管理,结果因为沈阳联通的一个合作项目需求紧急配合,只好临时抽调一个人去支援,毕竟网站是公司内部可以控制的,导致后台开发只有朱伟波一个“光杆司令”,那一段他连续十余天加班到晚上11:30!这样高强度、高压力的工作状态,不是每个程序员都能承受的。经过朱伟波的努力,终于在十天时间内将所有的后台编码全部完成(
紧接下来,从
在这个项目之前,整个公司是没有测试人员的!这不得不让我大为惊讶,一个SP公司没有测试怎么行!所以在这个项目进行的同时我启动测试人员招聘工作,最终成立了一支5人组成的测试组,负责所有业务的测试。
当网站后台管理编码完成后,4/28立即启动测试工作:后台管理中的首页管理、动画、声音、彩图、专题、资讯由专人负责测试,发现一个问题就在BugFree中记录一个Bug。通过BugFree的跟踪和记录,可以让某些问题不必累积到最后才解决。随着网站前台展示开发在5月中旬启动,测试工作也在并行跟进:每个频道、每个页面都有专人负责检查,这样尽可能的把各种潜在的问题揪出来,免除后患。
示意图:用BugFree来管理网站项目中的Bug
很遗憾的是,因为测试组搭建的比较晚、测试任务又比较重,他们需要花费很长时间去熟悉公司的各种业务,所以在这个网站项目中,对测试文档部分(比如测试用例)我并没有要求,只要把问题发现出来上Bug就好了。这就是项目管理中的Trade-Off:抓住主要矛盾、抓大放小。这个项目结束后,测试组已经逐步成熟、磨合好了,我才开始强调测试文档的重要性,每个业务测试时一定要同步完成相关的测试文档(计划、用例、测试结果等),测试时就按照相关的测试文档进行。这样以后复测就能省掉很多时间,换个人测试也很方便上手。
经过一个多月的努力,测试组的同事基本上完成了网站所有频道、页面的检查工作(
研发人员做出来的网站只是一个空空的框架,没有实际的内容填上去,网站就无法上线——打个比方,研发人员把“大楼”盖好了,还需要运营人员把“内部装修”做好。然而面对人员的稀缺和内部调整,一直到
在此期间,整个项目组都进入了最后的冲刺阶段。为了确保
示意图:项目最后突击的日志
值得庆祝的日子到来了。
ü 做出来的网站符合最初的规划和需求定义;
ü 按照需求定义完成的时候(
ü 整个项目执行过程中,规划、需求、开发、测试等环节均按照预定轨道前进,没有出现大的纰漏。
整个项目组成员在网站上线后都非常兴奋,这应该是公司到目前最成功的一个项目管理实践。公司领导对这个项目的研发表示非常满意。现在的情况是,休整2周后,
网站和产品开发没有什么不同!
按我整理的时间表和项目计划,对照微软的流程,你会发现,我完全是按照微软“传统软件”的研发流程去管理这个网站项目,略有不同的地方是,这个网站项目的时间跨度比较小(只有4个月),而且人力资源有限,美工、开发、测试三个环节我只能是并行处理、流水作业,以尽量缩短项目的整体时间。
| 规划和需求阶段 | 开发阶段 | 测试阶段 | 发布阶段 |
主参与人 | Planner与PM驱动 | 开发人员推动 | 测试人员推动 | PM,产品经理,运营管理等执行 |
阶段成果 | 目标描述 (Vision) 详细需求文档 (Spec) 日程进度表 | M1, M2, … Code Complete | 集成测试 Bug-Fix, Check-in Dogfood Beta1, beta2, … (Triage) Zero Bug Release | Show-Stopper bug Release Candidate(RC) Sign-off RTM (Ready To Release) |
我也算是“把微软先进的软件研发理念和中国中小企业的具体情况相结合”吧,其中最难的是把项目研发流程的理念灌输给全组同事以统一认识,并能有效的执行下去。很多时候要靠我不断的去PUSH各个环节,做的比较累,但在完成之后,很有成就感,尤其是针对一个团队不断发展和成熟,所做的努力是显而易见的。(未完待续)
1.软件工程三要素的价值
思考问题的方法可以是由点及面的,也可以是统揽全局的。换成业界最常用的词汇,就是“自上而下”还是“自下而上”的区别。
“牛屎图”中描述的工具、方法与过程也被称为软件工程的“三要素”。在本书中他们被分解开来思考——并不是要孤立这三个层面,它们实际上是相互作用的。例如“过程”问题,既有实施过程的工具,也有相关的过程方法理论。虽然说方法是“基于一种数据结构的编程实践的结果”,但这是一种非常狭义的定义。这个定义在过程的开发环节是有效的(或者说对“开发方法”的定义),然而“需求”、“设计”、“测试”等其它环节也有各自的方法论。即使站在具体环节之外,过程本身也有方法论的问题,这还不包括管理方法等等在内。
由于方法在过程环节以及过程总体层面上具有贯通性,因此保证“方法(或其行为)”实施的“工具”也会出现在过程的各个环节和层面上。这样得到的软件工程模型将不是经典的、层状的“牛屎图”,而可能像太极图一样由阴阳交汇而生万物。为了不使读者认为我已经入了道家理论的歧途,这样的一副图还是交由你们自己去画吧。只不过应该清楚一点,即使画出了太极图的软件工程模型,所见到的仍旧是工程的细部环节,就如同以管窥豹一般——斑是斑,豹是豹。
把每一个“管见”拼合起来,得到的才能是“豹”,而不是“斑”。所以尽管本书割裂了软件工程的各个要素,并从每个孤立的层面来审视。然而实质上,应该回归到软件工程的本体上来思考问题,而不是仅关注于每一个局部的要素。
工程的整体问题仍旧是“实现”。
2.RUP就是“杂物箱”
我也许总是在批评RUP,但是不得不承认它是对前人在软件过程思想方面的高度包容。请注意我用“包容”这个词,而不是按照语言习惯那样用“概括”。因为如果是“高度概括”,那就应该把目光投向瀑布模型,而RUP其实就像一个杂物箱一样“包容”了全部的已知理论。
可以把RUP定制成其它任何模型所表述的过程形态——RUP本身的特质决定了这一点——因而它也如同一个杂物箱一样放满了各种希奇古怪的东西:你可能从这个杂物箱里面拿出了一把剪刀,或一只苍蝇拍,或者是一根钓杆……
面对“软件开发”这样的需求,钓杆能有什么作用呢?在你扔掉它之前,请转换一下思维:钓杆可能带给你的团队以精神上的激励。如果你能意识到这一点,那么它将立即转化为生产力——请把钓杆挂在开发部的墙上。
RUP能不能被用起来,将取决于你刚才那个挑挑捡捡的行为,以及在你拿到“钓杆”后的辨识能力与组织能力。
3.UML与甲骨文之间的异同
在你真的打算用“甲骨文”来写项目文档之前,请先弄明白UML与甲骨文之间的异同。在本书里,它们都被做为沟通的工具。因此,目标是沟通,而不是“选用工具”。更进一步的推论是:即使你因为个人喜好而选择了甲骨文,也不要试图在结绳记事的原始人面前去用它。UML与甲骨文都是符号文字,都具有像形含义。然而,这并不表明UML符号本身能表达多么丰富的含义。如果要像甲骨文一样用几代人、上千册的论著去解释它,那么UML图的价值也就只剩下象征性意义了。
出于沟通的必要,UML语言的象征意义在一个图中应当被表述得足够准确和详细,以致对于不同的阅读者来说都提供了充足的信息。然而,一方面UML的规范中没有提供一个标准来衡量“怎样的UML图是描述充分的”;另一方面,UML作为一个语言,也无法直接在某个硬件平台中被语法检错和调试。所以在工程中使用UML图,应该有相应的文字来描述它。而且,这种描述与图之间的对应关系要持续地维护下去。如果这种关系松散了、断裂了,那么下一个阅读UML图的人所面对的将是无异于甲骨文出土时的困境。好在做UML图的那个工程设计人员(在辞世之前)还有机会为这些古怪符号写下规约。
4.经营者离开发者很远,反之亦然
使我第一次意识到EHM模型反应了角色所关注的不同视角的人,是我的老板。
事实上,他是一个完全不懂软件技术的老板。在EHM模型中,他所处于的位置在最右端,而开发者在最左端,在二者之间没有相同的关注界面(关注点)。EHM真实地反应了“老板不懂技术”的合理性,同样也真实地反应了“开发者转型为老板”的道路将是相当地漫长与艰难。
于是,担任中间角色的项目经理就有了一种使命:协调经营者与开发者之间的沟通。例如招来一名开发高手,对于公司的运作并不会有深入的影响(当然,如果你招来了Anders Hejlsberg就另当别论)。因此,我甚至不需要与BOSS讨论这名高手的来历及作用。同样,与一个技术分析人员讨论一个产品的技术价值、市场价值之间的差异,以及市场运作方式与技术实现手段的无关性是毫无必要的。
你要理解这种根源:角色的关注层面完全不同。
5.矛盾:实现目标与保障质量
在需求阶段我们会面临“目标”的问题,然而在大多数时候,与此相反的是我们会在项目交付和试用时才会碰到客户在质量上的投诉。
需求人员会把所有的责任归咎到开发人员,而开发人员又不停地埋怨需求的不清不楚或者变更的没完没了。如果正巧需求和开发都是由同一个人或者同一小组来做的,那么他们便会开始埋怨客户的苛刻以及工期的紧张。
总之一件事,没有人会跳出来说:我们原本就错了。然而,事实上根本问题可能是:我们把目标定错了。
可以看到,在项目的平衡三角(时间、资源和功能)中讨论的是目标问题,但并不讨论质量问题。也就是说,经典教材中总是关注如何更快的完成项目,并减少资源占用,以及实现更多的功能。但是,即使平衡了这种关系,项目的结果仍可能产生一个天生的残障。因为目标可能在平衡中确立,但质量却要在过程中控制。即使在时间、资源和功能三者中取得了平衡,并且客户、项目组和公司同样满意于这个平衡“目标”,但它仍然有可能是“不能实施”的。
如果原定的目标本身就过大,那么无论如何平衡这三者之间的关系,其结果仍旧是保障不了质量。
问题是:又有谁愿意在最初签订协议的时候,就降低或者放弃协议标准呢?
6.枝节与细节
前面说到目标和质量的问题时,提及“平衡时间、资源和功能三者的关系”。这其实是一个实施过程中的细节。或者说,它是一个具体的方法,而不是目的。
所以我们通常所说的细节,其实是对实施方法的一些有限量的描绘。比如“软件工艺”概念本身的提出,就是考究“细节问题”的。从这个角度上来说,我并不反对“细节决定成败”的观点。但请注意一个前提:这是技术或方法的细部。
我在前文中一再地混用了“细节”与“枝节”这两个词。枝节是事实发展的次要的分枝,它不涉及行为本身,也不是对行为本身的考量。因此我在前面的文字中说到“跳出细节”,本意是“跳出枝节”——细节只有做到何种程度的问题,而并不是关不关注(或做不做)的问题。
大多数情况下,管理人员有责任去审核、评估其它成员的工作成果。这个时候可以讨论“细节决定成败”类似的问题,因为这决定了产品的最终质量,而质量是工程的目标之一。
而在另一些情况下,例如管理人员做事件决策的时候,就必须要学会忽略枝节问题。
混淆这两个名词的使用,其根本原因在于一大部分读者并不能区分“细节”与“枝节”。从惯于“实做”的程序员一路走来的工程人员,很难分清自己什么时候是在“工作”,而什么时候是在“决策”。
因此我只好用最笨的方法提示管理者:别管它是细节还是枝节,只要你感到你的脚趾已经沾上了泥淖,就快点回头。
用脚趾去感觉,有时比用头脑去思维来得有效。
7.灵活的软件工程
并不像现代人想象的那样,古诗词一定是“逐字论平仄”的。变化或者变通,其实是常见之事。因此古词谱中,才常会见到冠以“摊破”、“减字”、“添字”等字的词格。然而古人在词格上的这种变通,是基于“音律”的。通常说的词律是指词格,这与音律是两回事。词律(格)是平仄,音律则是乐器、音调与歌舞。古词中用来吟唱与歌舞的词牌就不能混用,律不同,调不同,如是之。然而古词的音律(亦即是律谱)已经失传了,也就是说,今天的词是用来读的,不是唱,也不是舞,甚至连吟哦也不是。所以今人总是拿普通话中的一、二声作为平声,三、四声为仄声来填词,并以此论平仄,而全然不想词的格律的根基是“词律”与“音律”这两个部分的融合。
我曾经参与过一个讨论,叫“古人是如何说话的”。在我看来,古人做文章和说话是两回事,文章中之乎者也,日常交流中还是市井俚语的。因此评论中会说“以俚语入词”。也可见填词做文章与说话毕竟是不同。再者,说话也存在方言的问题,因此方言之间平仄音调也不尽相同。古代的歌妓是要求会“官话”的,这相当于现在“普通话”的地位,她们歌唱起来,也是用的“官话”。
更进一步的推论是:古代的词律中的平仄是以官话为基础的。然而如今的普通话毕竟不是古时的“官话”。也就是说,即使我们以普通话的四声为基础讨论平仄,在古人看来,也是可笑的,这样做出来的词依旧不可唱,也不可读。因此今人做词的标准是应该重定的了,除了词格(这里仅指字句的格式)和用韵之外,其它的部分是无法遵循的了。在各自的平仄以及句式上,应当以“能通顺”和“能品味”为准,风格上则以古雅为益。
仅此而已。
对于我这样的格律观点,一位网友曾有一句“未蕴而变,自欺也;知律而变,智者之道也”,实为良言。变向不变求。不变者,万变之所源,亦万变之所归。习诗词之法度,若蚕虫之结茧,若无结茧于前,何有破茧于后?故,知律而变,智者之道也。
“知律而变”中的“律”字,若解释为“规律”,便是可以用于软件工程中了。“道”是规律,如果明“道”,而可以变化无穷,这样做软件工程才是活的。就如同今人难于填词一样,不明道,则不明智,不明智则无所以为,因而在软件工程实施中不可避免地盲目与停滞。
“知律”的另一层意思,是在于“知道原理”。明白“为什么要这样”或者“为什么不是那样”。这在软件开发中是常见的问题,大多数人不知究竟地使用着技巧和方法,而一旦出了问题,则归咎于这些技巧和方法的不好。而真正的问题在于,这些人(我们通常叫做Copy&Paster)并不知道这些技巧、技术和方法的原理,因而不知道变通,也不知道回避错误。
死读一本《软件工程》的人不会做真正的软件工程,所以我写了本书,聊做软件工程实践者的思想之著。
不使用递归,直接采用order by按级排序;
支持无限分类;
显示类别时可设置从某类别下开始显示,以及设置是否显示子分类;是否带格式输出;
支持从任何目录的导航输出;
批量移动分类,批量移动文章,改写关联属性;
添加文章内容时,要存放亲缘树序列,目的:当选择某一分类查看时,可设置其子分类的文章是否也显示出来。
程序:无限分类(无递归) + 无限联动 + 树状显示(多显示方式) +导航输出 +批量移动 正式版1.1
作者:欣然随风(QQ:276624915)
时间:2005-10-28
主要功能:
不使用递归,直接采用order by按级排序;
支持无限分类;
显示类别时可设置从某类别下开始显示,以及设置是否显示子分类;是否带格式输出;
支持从任何目录的导航输出;
批量移动分类,批量移动文章,改写关联属性;
添加文章内容时,要存放亲缘树序列,目的:当选择某一分类查看时,可设置其子分类的文章是否也显示出来。
数据表字段参考:
class_id 类别i++号
class_kiss 亲缘树序列(资源内容指向此作为奴属,格式1:1:1.. 包括自己当前序列)
class_base 根分类序列
class_son 子分类序列
class_tier 分类所在层
class_name 分类名称
其它:
填写好数据库信息类便可直接运行。
--------------------------------------------------------------------------------
类文件:sort_class.php
// 数据库信息类
class db
{
const mysql_hdb = "localhost"; // 数据库主机名
const mysql_udb = "root"; // 数据库用户名
const mysql_pdb = "jjfzzzm"; // 数据库密码
const mysql_ddb = "test"; // 数据库名
static $cn; // 数据库连接ID
const table_sort = "class"; // 数据表名
function __construct()
{
self::$cn = @mysql_connect(self::mysql_hdb,self::mysql_udb,self::mysql_pdb)
or die("数据库连接失败,请联系管理员!");
@mysql_select_db(self::mysql_ddb,self::$cn)
or die("数据库选择失败,请联系管理员!");
@mysql_query("Set Names 'gb2312'");
}
}
// 分类信息类
class sort_info
{
/*
方法用途: 取得某分类的信息
参数设置: $post_kiss 分类亲缘树
返回值: 父分类SQL执行号
*/
static function sortinfo($post_kiss)
{
$sql = "select * from `".db::table_sort."`
where `class_kiss`='$post_kiss'
LIMIT 1";
return $fs = @mysql_query($sql,db::$cn);
}
}
// 分类添加类
class sort_add
{
/*
方法用途: 添加根分类
参数设置: $post_name 分类名称
$post_js 是否写入JS,默认0不写入,否则值为路径/文件名
返回值: 添加成功/失败
*/
function sort_add_base($post_name,$post_js=0)
{
//取根分类class_base的最大值
$sql = "select max(`class_base`) as `class_base` from `".db::table_sort."`";
$fs = @mysql_query($sql,db::$cn);
$tmp = @mysql_fetch_array($fs);
$nub = ++$tmp['class_base'];
//插入新记录
$sql = "insert into `".db::table_sort."` values('', '$nub', '$nub', '0', '1', '$post_name')";
if(@mysql_query($sql,db::$cn))
$result = TRUE;
else
$result = FALSE;
//插入JS文件
if($result and $post_js!==0)
{
$js = new sort_show;
$jsdata = $js->sort_js_text();
if(sort_js_write::writejs($post_js,$jsdata))
echo "js写入成功!";
else
echo "js写入失败!";
}
return $result;
}
/*
方法用途: 添加子分类
参数设置: $post_name 分类名称
$post_kiss 上一层分类亲缘树
$post_js 是否写入JS,默认0不写入,否则值为路径/文件名
返回值: 添加成功/失败
*/
function sort_add_son($post_kiss,$post_name,$post_js=0)
{
$fs = sort_info::sortinfo($post_kiss); // 查询父分类信息
$tmp = @mysql_fetch_array($fs);
$class_base = $tmp['class_base']; //取得根分类
$class_tier = ++$tmp['class_tier']; //取得当前分类所处层号
$sql = "select max(`class_son`) as `class_son`
from `".db::table_sort."`
where `class_kiss` LIKE '$post_kiss%' AND `class_tier`='$class_tier'";
$fs = @mysql_query($sql,db::$cn);
$tmp = @mysql_fetch_array($fs);
$class_son = ++$tmp['class_son']; //取得当前分类在子层的排列序号
$post_kiss .=":".$class_son; //取得当前分类的亲缘树序列
//插入新记录
$sql = "insert into `".db::table_sort."`
values('', '$post_kiss', '$class_base', '$class_son', '$class_tier', '$post_name')";
if(@mysql_query($sql,db::$cn))
$result = TRUE;
else
$result = FALSE;
//插入JS文件
if($result and $post_js!==0)
{
$js = new sort_show;
$jsdata = $js->sort_js_text();
if(sort_js_write::writejs($post_js,$jsdata))
echo "js写入成功!";
else
echo "js写入失败!";
}
return $result;
}
}
// 分类编辑类
class sort_ovr
{
/*
方法用途: 更新分类名称/批量移动目录(连带移动子目录)
参数设置: $post_kiss1 提交的该分类亲缘树
$post_kiss2 提交的目标亲缘树(不移动分类则默认FALSE,移动到根值为字符串“base”)
$post_name 提交的分类名(不更改则默认FALSE)
$post_js 更新JS菜单 值为路径/文件名
返回值: 无
*/
public $post_kiss1; // 当前目录的亲缘树
public $post_kiss3; // 移至新位置的亲缘树
public $post_base; // 移至新位置的根
public $post_tier; // 移至新位置层(差)
public $off = 0; // 顺序执行操作开关
public $len; // 当前亲缘树长度
function sortovr($post_kiss1 , $post_kiss2=FALSE , $post_name=FALSE , $post_js)
{
$this->len = strlen($post_kiss1);
$this->post_kiss1 = $post_kiss1;
if($post_name !== FALSE)
{
$sql = "UPDATE ".db::table_sort." SET `class_name`='$post_name' WHERE `class_kiss`='$post_kiss1' LIMIT 1";
if(@mysql_query($sql,db::$cn))
{
$this->off = 1;
echo "分类名编辑成功!";
}
else
echo "分类名编辑失败!";
}
if($post_kiss2 !== FALSE)
{
$fs = sort_info::sortinfo($post_kiss1); // 查询当前分类信息
$tmp = @mysql_fetch_array($fs);
$tier= $tmp['class_tier']; // 当前层号
// 移动到根时的处理
if($post_kiss2 == "base")
{
$sql = "select max(`class_base`) as `class_base` from `".db::table_sort."`";
$fs = @mysql_query($sql,db::$cn);
$tmp = @mysql_fetch_array($fs);
$this->post_base = ++$tmp['class_base']; //最终根分类号
$this->post_kiss3 = $tmp['class_base']; //最终亲缘树
$class_tier = 1; //最终层号
$class_son = 0; //最终子类排序号
}
// 不为根的处理
else
{
$fs = sort_info::sortinfo($post_kiss2); // 查询目标分类信息
$tmp = @mysql_fetch_array($fs);
$this->post_base = $tmp['class_base']; //最终根分类号
$class_tier = ++$tmp['class_tier']; //最终层号
$sql = "select max(`class_son`) as `class_son`
from `".db::table_sort."`
where `class_kiss` LIKE '$post_kiss2%' AND `class_tier`='$class_tier'";
$fs = @mysql_query($sql,db::$cn);
$tmp = @mysql_fetch_array($fs);
$class_son = ++$tmp['class_son']; //最终子类排序号
$this->post_kiss3 = $post_kiss2.":".$class_son; //最终亲缘树
}
$this->post_tier = $class_tier - $tier; //层差值
// 移动当前目录
$sql = "UPDATE ".db::table_sort." SET
`class_kiss`='$this->post_kiss3',
`class_base`='$this->post_base',
`class_son`='$class_son',
`class_tier`='$class_tier'
WHERE `class_kiss` = '$post_kiss1' LIMIT 1";
if(@mysql_query($sql,db::$cn))
{
$this->off = 1;
echo "目录移动成功!";
}
else
echo "目录移动失败!";
// 移动所有受影响子目录
if($this->off == 1)
{
$sql = "UPDATE ".db::table_sort." SET
`class_kiss`=INSERT(`class_kiss`,1,$this->len,'$this->post_kiss3'),
`class_base`='$this->post_base',
`class_tier`=`class_tier`+'$this->post_tier'
WHERE `class_kiss` LIKE '$post_kiss1%'";
if(@mysql_query($sql,db::$cn))
echo "子分类移动成功!";
else
{
$this->off = 0;
echo "子分类移动失败!";
}
}
}
// 更新JS文件
if($this->off == 1)
{
$js = new sort_show;
$jsdata = $js->sort_js_text();
if(sort_js_write::writejs($post_js,$jsdata))
echo "js写入成功!";
else
echo "js写入失败!";
}
}
/*
方法用途: 移动所有关联资源内容的目录指向(如内容表的记录对奴属目录的指向)
参数设置: $name_table 待修改的表名
$name_sort 待修改的存储分类亲缘树的字段名
返回值: 无
*/
function textovr($name_table,$name_sort)
{
if($this->off == 1)
{
$name_table = "`".$name_table."`";
$name_sort = "`".$name_sort."`";
$sql = "UPDATE $name_table SET
$name_sort=INSERT($name_sort,1,$this->len,'$this->post_kiss3')
WHERE $name_sort LIKE '$this->post_kiss1%'";
if(@mysql_query($sql,db::$cn))
echo "资源目录指向移动成功!";
else
echo "资源目录指向移动失败!";
}
}
}
// 分类显示类
class sort_show
{
/*
方法用途: 获取分类信息执行号
参数设置: $post_kiss 设置从某分类下开始显示(传入值应为上层分类的亲缘树序列),默认0为从根开始显示
$post_wise 设置显示方式:1为显示该类的所有子分类,0为只显示当前层次的分类
返回值: sql执行号
*/
public $sqldata;
public $class_tier=1; // 当前分类层号,用于格式输出时减去空格,1为减根
function __construct($post_kiss=0,$post_wise=1)
{
if($post_kiss>0)
{
$fs = sort_info::sortinfo($post_kiss); // 取得父分类信息
$tmp = @mysql_fetch_array($fs);
$this->class_tier = ++$tmp['class_tier']; // 取得该分类的层号
$post_kiss = "`class_kiss` LIKE '$class_kiss%'";
if($post_wise==0)
$post_wise="'$this->class_tier' = `class_tier`";
else
$post_wise=" `class_tier` >= '$this->class_tier'";
}
else
{
$post_kiss=1;
if($post_wise==0) $post_wise="`class_tier` = '1'";
}
$sql = "select * from `".db::table_sort."`
where $post_kiss and $post_wise
order by `class_base` asc, `class_kiss` asc, `class_son` asc";
$this->sqldata = @mysql_query($sql,db::$cn);
}
/*
方法用途: 列表显示分类
参数设置: $type 显示方式(默认0为不带格式输出,要带格式则参数为分级符号,如“└”)
返回值: 无
*/
function show($type)
{
if($type=="0")
{
while($tmp = @mysql_fetch_array($this->sqldata))
{
echo $tmp['class_name']."
";
}
}
else
{
while($tmp = @mysql_fetch_array($this->sqldata))
{
$nub=str_repeat(" ",($tmp['class_tier']-$this->class_tier))."$type"; //使用空格缩进和$type符号产生分级
echo $nub.$tmp['class_name']."
";
}
}
}
/*
方法用途: 生成待写入js文本内容
返回值: 字符串
*/
function sort_js_text()
{
while($tmp = @mysql_fetch_array($this->sqldata))
{
$nub=str_repeat(" ",$tmp['class_tier']); //使用空格缩进
$jsdata .= $nub."[".--$tmp['class_tier'].","".$tmp['class_name']."","".$tmp['class_kiss'].""],".chr(13);
}
$jsdata = substr($jsdata, -strlen($jsdata), -2); //去除最后一个逗号和最后一个换行
return "var arrType=new Array(".chr(13).$jsdata.");";
}
}
// 导航信息类
class sort_boat
{
/*
方法用途: 输出某类别的所有上级目录(导航)
参数设置: $post_kiss 某类亲缘树
返回值: 无
*/
function boat_show($post_kiss)
{
$class_kiss =explode(":",$post_kiss); // 分拆亲缘树序列
$nub = sizeof($class_kiss); // 元素个数
// 取得各层亲缘树序列数组
for($i=0;$i<$nub;$i++)
{
if($i>=1) $kiss_val .= ":";
$kiss_val .= $class_kiss[$i];
$kiss_arr[] = "'".$kiss_val."'=`class_kiss`";
}
// 生成sql查询条件
$kiss_val = implode(" OR ",$kiss_arr);
$sql = "select * from `".db::table_sort."`
where $kiss_val order by `class_tier`";
$fs = @mysql_query($sql,db::$cn);
while($tmp = @mysql_fetch_array($fs))
{
echo " >> ".$tmp['class_name'];
}
}
}
// 写入JS文件类
class sort_js_write
{
/*
方法用途: 写入JS分类数据
参数设置: $fileurl 文件路径/文件名
$filetext 写入内容
返回值: 成功/失败
*/
function writejs($fileurl,$filetext)
{
$no = fopen($fileurl, 'w');
$no = fwrite($no,$filetext);
return ($no)?true:false;
}
}
?>
--------------------------------------------------------------------------------
添加分类演示:add_sort.php
include("sort_class.php");
new db;
// ---------- 添加分类调用 ------------
if(isset($_POST["post"]))
{
if($_POST["name"]!="")
{
$add = new sort_add;
if($_POST["js"]==1) $_POST["js"]="sort_type.js"; else $_POST["js"]=0;
if($_POST["in_dId"] == "")
{
//if($add->sort_add_base("根分类名","是否写入JS/0为不写"))
if($add->sort_add_base($_POST["name"],$_POST["js"]))
echo "根分类添加成功";
}
else
{
//if($add->sort_add_son("父目录亲缘树","子分类名","是否写入JS"))
if($add->sort_add_son($_POST["in_dId"],$_POST["name"],$_POST["js"]))
echo "子分类添加成功";
}
}
else
echo "请输入分类名称!";
}
echo "
---------- 导航显示调用 ------------
";
$boat = new sort_boat;
$boat->boat_show("2:1:1:1"); //("某目录亲缘树,如:1:3:22:44");
echo "
---------- 显示分类调用 ------------
";
$show = new sort_show(0,1); //(某分类的亲缘树/0为根,显示包含子类1/显示当前层分类0)
$show->show("└ "); //(分类前缀的符号,如不使用格式,则设置为0)
?>
--------------------------------------------------------------------------------
编辑分类演示:ovr_sort.php
include("sort_class.php");
new db;
// ---------- 编辑分类调用 ------------
if(isset($_POST["post"]))
{
if($_POST["in_dId"]!="")
{
if($_POST["name"]=="") $_POST["name"]=FALSE;
if($_POST["no"]==1)
{
if($_POST["in_dId1"]=="") $_POST["in_dId1"]="base";
}
else
$_POST["in_dId1"]=FALSE;
$ovr = new sort_ovr;
//参数("待修改分类/不更改值为FALSE","新位置/base表根/不更改值为FALSE","新类名/不更改值为FALSE","更新JS菜单")
$ovr->sortovr($_POST["in_dId"] , $_POST["in_dId1"] , $_POST["name"] , "sort_type.js");
/*
如果对目录进行了批量移动,不要忘了执行下边这句:转移受影响的内容奴属目录指向。
没有把它直接写到目录移动后就自动执行,是因为有些系统可能不止一个表使用目录库,因此在下边手动指定要改哪些表!
*/
// $ovr->textovr("表1","关联目录字段名");
// $ovr->textovr("表2","关联目录字段名");
// .... 有多少表使用了就执行多少次
}
else
echo "未指定待编辑分类!";
}
echo "
---------- 导航显示调用 ------------
";
$boat = new sort_boat;
$boat->boat_show("2:1:1:1"); //("某目录亲缘树,如:1:3:22:44");
echo "
---------- 显示分类调用 ------------
";
$show = new sort_show(0,1); //(某分类的亲缘树/0为根,显示包含子类1/显示当前层分类0)
$show->show("└ "); //(分类前缀的符号,如不使用格式,则设置为0)
?>