44
第 5 章 信息集成层和云数据库 由于数据源(如:文件系统、关系数据库、LDAP 等)的多样性,企业应用程 序常常涉及多个数据源间的数据存取。为了减少业务层的复杂性,便出现了信息集 成层。信息集成层为业务层提供抽象接口。这一层的主要目的是:业务层无须知道 底层具体的数据源,所有针对某个数据源的具体数据存取都在信息集成层封装并实 现。业务对象并不需要知道在某一业务过程中,如何从数据源上存取数据。通过信 息集成层存取数据有很多好处,比如:可以实现数据的缓存、统一处理事务和存取 异常处理等。信息集成层解决了数据访问问题,增强了系统灵活性和可维护性。 数据库是云计算平台的主要数据源,云服务最终要访问数据库中的数据。一般 而言,在数据库服务器的前面,都有一层。读者可以把这层认作信息集成层(如图 5-1 所示)。信息集成层的作用在于为应用程序提供对企业中全部数据的一致访问, 不受数据的格式、来源或位置的限制。在实现时,往往对数据虚拟化和对象化,这 可能包括数据总线(Data Bus)和实体管理器的开发和使用,企业中的所有应用程 序都通过标准服务或接口从数据总线或实体管理器中请求数据(并以对象形式返 回)。应用程序不知道管理数据的操作系统,数据的位置也是透明的。由于数据管 理是由共同的服务提供的,所以,由访问的服务而不是由应用程序来负责查询数据 (无论是本地的还是远程的),然后按照请求的格式提供数据。 5-1 云计算分层图 一些工具提供了类和数据库表之间的映射。在程序中插入新行、修改几行数据、 删除行数据等功能都可以通过所映射的类完成,而无需任何 SQL 语句,从而简化了 程序的开发和维护。在本章中我们讲解这些工具。另外,我们在本章探讨实体管理 器的概念。开发人员可以自己开发一个实体管理器,或者使用实体管理器组件。 即使在云计算时代,也离不开数据库的设计。一个良好设计的数据库是一个云 计算系统的关键。在本章,我们还介绍了如何设计数据库。 浏览器 Web 层(JSP/ServletWeb 服务器 信息 集成层 业务层 业务层(EJB数据库

第5章 信息集成层和云数据库 - images.china-pub.comimages.china-pub.com/ebook195001-200000/196745/ch05.pdf的数据库产品无关。 基于JDBC 自己开发一个实体管理器来隐藏具体数据库存取(有时叫它为

  • Upload
    others

  • View
    4

  • Download
    0

Embed Size (px)

Citation preview

  • 第 5 章 信息集成层和云数据库

    由于数据源(如:文件系统、关系数据库、LDAP 等)的多样性,企业应用程序常常涉及多个数据源间的数据存取。为了减少业务层的复杂性,便出现了信息集

    成层。信息集成层为业务层提供抽象接口。这一层的主要目的是:业务层无须知道

    底层具体的数据源,所有针对某个数据源的具体数据存取都在信息集成层封装并实

    现。业务对象并不需要知道在某一业务过程中,如何从数据源上存取数据。通过信

    息集成层存取数据有很多好处,比如:可以实现数据的缓存、统一处理事务和存取

    异常处理等。信息集成层解决了数据访问问题,增强了系统灵活性和可维护性。

    数据库是云计算平台的主要数据源,云服务最终要访问数据库中的数据。一般

    而言,在数据库服务器的前面,都有一层。读者可以把这层认作信息集成层(如图

    5-1 所示)。信息集成层的作用在于为应用程序提供对企业中全部数据的一致访问,不受数据的格式、来源或位置的限制。在实现时,往往对数据虚拟化和对象化,这

    可能包括数据总线(Data Bus)和实体管理器的开发和使用,企业中的所有应用程序都通过标准服务或接口从数据总线或实体管理器中请求数据(并以对象形式返

    回)。应用程序不知道管理数据的操作系统,数据的位置也是透明的。由于数据管

    理是由共同的服务提供的,所以,由访问的服务而不是由应用程序来负责查询数据

    (无论是本地的还是远程的),然后按照请求的格式提供数据。

    图 5-1 云计算分层图

    一些工具提供了类和数据库表之间的映射。在程序中插入新行、修改几行数据、

    删除行数据等功能都可以通过所映射的类完成,而无需任何 SQL 语句,从而简化了程序的开发和维护。在本章中我们讲解这些工具。另外,我们在本章探讨实体管理

    器的概念。开发人员可以自己开发一个实体管理器,或者使用实体管理器组件。

    即使在云计算时代,也离不开数据库的设计。一个良好设计的数据库是一个云

    计算系统的关键。在本章,我们还介绍了如何设计数据库。

    浏览器

    Web 层(JSP/Servlet)

    Web 服务器 信息 集成层

    业务层

    业务层(EJB)

    数据库

  • 5.1 信息集成层

    在 Java 领域,实现信息集成层的方法有多种:

    使用 Java 持久化 API 的实体管理器(EntityManager)。该实体管理器同后台的数据库产品无关。

    基于 JDBC 自己开发一个实体管理器来隐藏具体数据库存取(有时叫它为DAO:Database Access Object,数据访问对象):用 JDBC 实现关系数据库的数据存取。其优点是:直接和数据库关联,存取速度快,支持多数据库,

    以及细粒度存取。缺点是不便以后维护,程序开发复杂。在业务层存取一个

    对象的数据时,只要调用这个实体管理器来完成业务对象的 CRUD。方法的实现由具体的实体管理器实现处理,业务层不关心实现细节。这就完全抽象

    了数据源。 使用Hibernate:Hibernate提供了Java对象到数据库表之间的直接映射。它对JDBC进行了轻量级的对象封装,使得Java程序员可以使用面向对象的编程思维来操纵数据库。Hibernate是一个开源轻量级实现的持久框架。

    后两种方法的使用有其历史原因。EJB 3.0 之前的版本中的持久化(persitence)编程相当复杂,另外,老版本的持久化 bean 也很耗资源,属于重量级的编程。因而,一些公司或者程序员会自己用 JDBC 接口开发一个实体管理器(如:新长安公司的云计算平台 1.0 版本),或者使用 EclipseLink 或 Hibernate 产品。自己开发一个实体管理器的好处是可以访问文件系统上的数据,而不仅仅是数据库中的数据。我们下

    面介绍前两种方法。至于第三种方法, EJB 3 或之后的版本都能够很好地替代Hibernate,所以,我们不建议读者使用 Hibernate。另外,Hibernate 所采用的框架也不是标准。

    5.2 Java持久化API(JPA)

    你可以使用 Java 持久化 API(Java Persistence API,简称 JPA)来实现信息集成层。JPA 可以被用于 J2EE 和 JSE 环境中。图 5-2 显示了 EJB、JPA 和数据库之间的关系。

  • JDBC 数据源 JPA 实体

    EJB 一般 Java 类

    实体管理

    器 数据库

    图 5-2 JPA

    JPA 包含了以下内容:

    JPA 实体:是一个常规的 Java 类,不需要实现任何特别的接口或扩展任何类。这个类必须包含一个没有参数的构造方法;使用@Entity 注释。

    对象-关系映射。 查询语言。 实体管理器。

    5.2.1 对象-关系映射和JPA实体

    JPA 使用一种对象-关系映射(Object-Relational Mapping,ORM 方法将面向对象模型和关系数据库联系起来。我们知道,数据库由很多表组成,每个表由很多行

    组成,而每行又有很多列。在 JPA(包括老版本中的实体 EJB)出来之前,开发人员需要自己写 INSERT、UPDATE、DELETE 等 SQL 语句来管理表上的数据。

    在 JPA 中,一个表被映射到一个实体(entity)类,一行就是一个实体类的实例。JPA 使用注释来表示一个实体或两个实体之间的关系。比如:在数据库中,你有一个订单表,那么,你可以定义一个订单类,并注上该类为订单表的实体(@Entity和@Table(name=“订单表名”))。使用@Column 来映射一个属性到一个列。如果表名和实体类名相同,你可以不注上@Table;如果列名和属性名相同,你可以不注上@Column。一个订单类的实例就是订单表中的一行。实体类是一个 Java bean,通过get/set 方法来直接获取或设置该行上的各个列值。一个表具有一个主键,在实体类上,使用@id 来标志。JPA 还可以生成主键值。下面是新长安的企业实体类代码(部分代码):

    package xinCA.entity; import java.io.Serializable; import java.util.Set; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.OneToMany; @Entity public class Company implements Serializable { @Id

  • private int companyid; private String companyname; …… public int getCompanyid() { return this.companyid; } public void setCompanyid(int companyid) { this.companyid = companyid; }

    5.2.2 关系

    表和表之间具有多种关系:一对一、一对多、多对多和多对一。在实体类中,

    你可以注明这个关系,如下所示:

    一对多:@OneToMany。一个实体实例对应着另一个实体的多个实例。 一对一:@OneToOne。一个实体实例对应着另一个实体的单个实例。 多对一:@ManyToOne。一个实体的多个实例对应着另一个实体的单个实例。在这种情况下,需要使用@JoinColumn 来指定外键列(换句话说,JPA 中的JoinColumn 就是数据库中的外键)。

    多对多:@ManyToMany。一个实体的多个实例对应着另一个实体的多个实例。需要使用@JoinTable 来指定两个实体的关系表。在数据库中,多对多的关系会生成三张表(两个实体表,1 个关系表)。比如:产品和客户的关系是销售关系,他们之间是多对多的关系,那么,除了产品表和客户表,还有一个销售表。还要

    使用@JoinColumn 来指定关系表中的外键(有两个外键,分别指向各个实体表)。

    例如:1 个企业可以有多个广告,所以在企业实体类上定义了一个一对多的关系:

    @OneToMany(mappedBy="companyid") private Set adsCollection;

    对于一对多和多对一的关系,我们还可以设置级联删除。

    5.2.3 连接多个表的实体

    因为数据库中的表是有关系的,其关系通过主外键来体现。比如:一个订单表

    和一个订单明细表,对于一个具体的订单,在订单明细表中存在着多行(即:多个

    商品的订购信息)。在数据库中,把订单表称为父表,把订单明细表称为子表。在

    开发数据库程序时,我们经常需要关联父表和子表,以获得订单的详细信息(如:

    订单时间)和订单的明细(如:一个 MP3 商品)。在 JPA 中,除了映射一个实体到一个数据库表,你还可以映射一个实体到多个表的连接(join)。

    5.2.4 JPA类的UML表示

    很多开发工具都提供了使用 UML 来图形化 JPA 实体类的功能。图 5-3 即显示了

  • 企业和广告实体的 UML 表示。

    图 5-3 JPA 类的 UML 表示

    5.2.5 实体管理器

    到现在为此,我们根据数据库表的结构并使用注释(annoation)定义了一些 JPA实体类。在实体类上,也定义了关系。另外,根据所需要的表和表的连接,还定义

    了另外一些实体类。实体本身并不能把自己保存到数据库中。在 JPA 中,总有一个组件是要真正完成数据库的 insert/delete/ update/select 等操作的。这个组件就是实体管理器类(javax.persistence.Entity- Manager)。通过实体管理器,可以创建实体(插入一行)、删除实体(删除一行)和更新实体(更新一行)。另外,实体管理器提

    供了一个 find 方法,从而可根据主键来完成实体查询的功能。表 5-1 映射了 SQL 语句和实体管理器上的方法之间的关系。

    表 5-1 Java 持久化 API 和 SQL 语句的对应关系

    功能 SQL 语句 Java持久化API

    新增 INSERT INTO 表名… EntityManager.persist(实体) 删除 DELETE FROM 表名… EntityManager.remove(实体) 更新 UPDATE 表名… EntityManager.merge(实体) 查询 SELECT … FROM 表名… EntityManager.find(主键) 刷新 SELECT … FROM 表名… EntityManager.refresh

    JPA 提供了两类实体管理器:

    容器管理(Container-managed)的实体管理器,在程序中使用以下注释: @PersistenceContext

  • EntityManager em;

    应用管理(Application-managed)的实体管理器,使用以下注释和代码: @PersistenceUnit EntityManagerFactory emf; EntityManager em = emf.createEntityManager();

    5.2.6 JPA查询语言

    在一个数据库程序中,有大量的查询操作。JPA 提供了 JPQL(Java Persistence Query Language,Java 持久化查询语言)来帮助开发人员使用类似 SQL 语句的方式查询实体。同 SQL 类似,JPQL 同具体的数据库产品无关。在实体类中,开发人员可以使用@NamedQuery 定义一个静态的查询,比如:

    @Entity @Table(schema="XCAADMIN", name = "COMPANY") @NamedQueries({ @NamedQuery(name="getCompanies", query="select c from Company c"), @NamedQuery(name="getCompanyByName",query="SELECT a FROM Company a WHERE a.companyName = :companyName") })

    JPQL 允许使用 createQuery 方法创建动态的 SQL 查询(类似于 JDBC 的PreparedStatement),使用 createNamedQuery方法创建静态的 SQL查询(类似于 JDBC的 Statement)。在上述查询中,同嵌入式 SQL 编程类似,你可以使用“:变量名”在 SELECT 语句中指定变量。在程序中,使用 setParameter 来给变量赋值。类似于JDBC , 查 询 后 的 结 果 可 以 通 过 getResultList 来 获 得 。 JPQL 还 提 供 了createNativeQuery 方法来允许开发人员直接执行一个 SQL 语句(如:一个 delete 或update 语句)。

    5.2.7 persistence.xml

    正如上节阐述的,我们通过实体管理器可以在数据库上进行增、删、改等操作。

    那么,具体的数据库连接呢?在哪里设置?这就是 persistence.xml 所要实现的其中一个功能。该文件包含了数据源的信息和该数据源所涉及的实体类。换句话说,这

    些实体类表示了该数据库上的数据。比如:

    jdbc/xincadb xinCA.entity.Ads

  • xinCA.entity.AdsPK xinCA.entity.Company

    5.2.8 OpenJPA

    从 EJB 3.0 开始,使用 JPA 编程简单(JPA 类同普通的 Java 类相似),并且系统开销也不重。我们推荐读者尽量使用它,而不是传统的 JDBC 编程。JPA 最早是由javax.persistence 包提供,现在有了开源的 JPA 提供者(即:实现了 javax.persistence 所声明的功能),如:Apach OpenJPA。IBM WebSphere 的 JPA 功能也是基于 OpenJPA的。

    5.2.9 新长安JPA类开发实例

    在这一节中,我们以新长安的广告服务为例来说明开发和测试 JPA 代码的步骤。新长安数据库有以下两个表:COMPANY 表存放企业的信息;ADS 表存放企业要发布的广告信息(限制为文本信息,因为新长安开通了手机短信服务,可通过手机短

    信,发送这些广告到潜在客户手上)。

    CREATE TABLE COMPANY ( COMPANY IDINTEGER GENERATED ALWAYS AS IDENTITY (START WITH 1, INCREMENT BY 1, NO CACHE ), COMPANYNAME VARCHAR(64) NOT NULL UNIQUE, COMPANYTYPE SMALLINT WITH DEFAULT 0, ADDRESS VARCHAR(128) NOT NULL, DISTRICTNAME VARCHAR(32), CITYNAME VARCHAR(32) NOT NULL, PROVINCENAME VARCHAR(32) NOT NULL, POSTALCODE VARCHAR(16) NOT NULL, MANAGER VARCHAR(32) NOT NULL, TELEPHONE VARCHAR(32) NOT NULL, EMAILVARCHAR(64), PRIMARY KEY(COMPANYID) ); CREATE TABLE ADS ( COMPANYID INTEGER NOT NULL, RELEASETIME TIMESTAMP NOT NULL, ADTYPE SMALLINT NOT NULL, EXPIRATION TIMESTAMP NOT NULL, MESSAGE VARCHAR(128) NOT NULL, PRIORITY SMALLINT NOT NULL, PRIMARY KEY(COMPANYID,RELEASETIME), FOREIGN KEY (COMPANYID) REFERENCES COMPANY(COMPANYID) ON DELETE CASCADE

  • );

    我们以 RAD V7.5 为例来讲解开发 JPA 代码的步骤。RAD 7.5 同 Eclipse 开发环境类似。所以,读者可以使用任何兼容 Eclipse 的开发工具来开发。

    1.创建一个 JAP 项目。如图 5-4 所示,从向导中选择 JPA 项目。

    图 5-4 创建 JPA

    2.提供项目名称,如:xinCAJPA,如图 5-5 所示。选择相应的运行环境。

    图 5-5 指定 JPA 项目名称

    3. 选择一个数据库连接,比如:我们选择预先创建的 XINCADB 连接,如图5-6 所示。你也可以添加一个新的数据库连接(因为一个数据库表对应着一

  • 个 JPA 实体类,所以要有一个数据库连接)。如果数据库连接尚未建立,单击界面上的“连接”即可。

    图 5-6 指定数据库连接

    单击“完成”,开发工具即生成 persistence.xml 等文件。persistence.xml 文件内容如下:

    在创建了 JPA 项目后,我们可就来创建 JPA 实体。

    4.如图 5-7 所示,右击“xinCAJPA”项目,从菜单中选择“JPA 工具”|“生成实体”命令。

  • 图 5-7 生成实体

    5.确认数据库连接和模式的信息,见图 5-8。在这个窗口上,你也可以添加一个新的数据库连接。

    图 5-8 数据库连接

    6.如图 5-9 所示,给定一个 JPA 包的名称(即:生成的 JPA 程序所在的包),选择“使 persistence.xml 中的类同步”(目的是让开发工具将所生成的实体类添加到 persistence.xml 文件中),并选择想要生成实体的表。你可以选择所有表。这里我们只选择了 ADS 和 COMPANY 两个表来演示 JPA 的使用。

  • 图 5-9 选择表

    7.单击“完成”按钮,开发工具就自动生成了 JPA 实体类(见图 5-10)。

    图 5-10 生成的实体类

    下面我们分析一下开发工具所生成的该企业(Company)实体类代码:

    package xinCA.entity; import java.io.Serializable; import java.util.Set; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.OneToMany; @Entity

  • public class Company implements Serializable { @Id private int companyid; private String companyname; private short companytype; private String address; private String districtname; private String cityname; private String provincename; private String postalcode; private String manager; private String telephone; private String email; @OneToMany(mappedBy="companyid") private Set adsCollection; private static final long serialVersionUID = 1L; public Company() { super(); } public int getCompanyid() { return this.companyid; } public void setCompanyid(int companyid) { this.companyid = companyid; } public String getCompanyname() { return this.companyname; } public void setCompanyname(String companyname) { this.companyname = companyname; } public short getCompanytype() { return this.companytype; } public void setCompanytype(short companytype) { this.companytype = companytype; } public String getAddress() { return this.address; } public void setAddress(String address) { this.address = address; } public String getDistrictname() { return this.districtname; } public void setDistrictname(String districtname) { this.districtname = districtname; } public String getCityname() { return this.cityname;

  • } public void setCityname(String cityname) { this.cityname = cityname; } public String getProvincename() { return this.provincename; } public void setProvincename(String provincename) { this.provincename = provincename; } public String getPostalcode() { return this.postalcode; } public void setPostalcode(String postalcode) { this.postalcode = postalcode; } public String getManager() { return this.manager; } public void setManager(String manager) { this.manager = manager; } public String getTelephone() { return this.telephone; } public void setTelephone(String telephone) { this.telephone = telephone; } public String getEmail() { return this.email; } public void setEmail(String email) { this.email = email; } public Set getAdsCollection() { return this.adsCollection; } public void setAdsCollection(Set adsCollection) { this.adsCollection = adsCollection; } }

    从上面程序可以看出:

    @Entity 定义了该类为一个实体。另外,你可能注意到,程序上并没有@Table的信息,那是因为实体同表的名字一样。你可以在@Entity 的下面一行添加@Table 的信息。如:

    @Table (schema=”XCAADMIN”, NAME=”COMPANY”)

    @Id 定义了 companyid 为主键。 @OneToMany 定义了企业同广告(ADS)表之间的一对多关系。一个企业可能发布多个广告。那个 Set类型的属性包含了多个广告。

    在 Ads 实体类上,你可以看到@ManyToOne:

  • @ManyToOne @JoinColumn(name="COMPANYID") private Company companyid;

    如图 5-11 所示,在 persistence.xml 文件中,这些实体已经被添加进来。

    图 5-11 persistence.xml 文件

    在系统所生成的 JPA 代码中,开发人员可以根据实际需要,调整一些代码:

    在构造函数中设置一些缺省值,比如:在企业的实体类中,设置企业的缺省分类。

    修改一些方法为 private 方法。比如:将 setCompanyid 方法设置为私有,从而不让客户端设置该值(因为该值由服务器自动生成)。

    添加一些新的构造函数。虽然系统自动生成一个不带参数的构造函数,但是开发人员可以添加多个带参数的构造函数。比如:给广告实体类添加下面的构造函数:

    public Ads(short adtype,Timestamp expiration, String message, short priority){

    setAdtype(adtype); setExpiration(expiration); setMessage(message); setPriority(priority); }

    添加一些新的方法来处理业务逻辑(作者注:对于是否应该在 JPA 实体中处理业务逻辑?我们的观点是尽量避免。业务逻辑的处理应该由外部的 EJB 程序完成)。比如:在企业实体类中添加一个 processAds 的方法,该方法创建了一个 Ads 的实体类。

    public Ads processAds(short adtype, Timestamp expiration, String message,short priority){ Ads ads = new Ads(adtype,expiration,message,priority); ads.setCompanyid(this);

  • return ads; }

    添加命名查询(named query),代码如下:

    @Entity @Table(schema="XCAADMIN", name = "COMPANY") @NamedQueries({ @NamedQuery(name="getCompanies",query="select c from Company c"), @NamedQuery(name="getCompanyByName", query="SELECT a FROM Company a WHERE a.companyName = :companyName") }) public class Company implements Serializable { ……

    要注意的是,查询是基于实体属性名,而不是数据库上的表的列名。

    由于在 COMPANY 表上的 COMPANYID 列是一个 IDENTITY 列,所以你需要在 Company 实 体 类 上 加 上 : @GeneratedValue(strategy = GenerationType.IDENTITY)。 比如:

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int companyid;

    5.2.10 新长安JPA类测试实例

    我们不需要 Web 应用服务器就可以测试 JPA 实体。测试人员可以单独测试信息集成层。只要写一个 Java 程序,该程序直接调用 JPA 实体即可。另外,测试人员也可以使用 JUnit 框架来测试 JPA 实体。在第 7 章,我们将讲解如何使用 JSF 来调用JPA 类。

    下面我们使用一个 Java 程序来测试 JPA 实体。

    1.如图 5-12 所示,创建一个 JPA 测试项目(xinCAJPATest)。

    2.单击“下一步”,然后选择“项目”标签,在选项卡中并选择 JPA 项目(见图 5-13)。

  • 图 5-12 新建 Java 项目

    图 5-13 项目设置

    3.单击“完成”。然后,新建一个测试类 EntityTester(见图 5-14)。

  • 图 5-14 新建测试类

    4. 设置测试项目的属性。因为不在 Web 应用服务器上运行测试,所以,你需要使用另一个 OpenJPA 实现。这就需要修改项目中的 Java 构造路径。缺省输出文件夹的设置为 xinCAJPATest/bin(如图 5-15 所示),将它改为:xinCAJPATest/src。这个修改的目的是让程序能够找得到 persistence.xml 文件。另外,在库的页面上(如图 5-16 所示),添加 JPA 相关的多个库(如:com.ibm.ws.jpa.jar)。

    图 5-15 设置输出文件夹

  • 图 5-16 添加 JPA 相关的多个库

    5.在测试项目上,配置一个 persistence.xml 文件。 在 src 文件夹下创建一个新的文件夹:META-INF。然后,拷贝 xinCAJPA下的 persistence.xml 文件到上述新文件夹下,并修改为:

    org.apache.openjpa.persistence. PersistenceProviderImpl xinCA.entity.Ads xinCA.entity.AdsPK xinCA.entity.Company

  • 6.编写测试类代码,代码如下:

    package xinCA.entity.test; import java.sql.Timestamp; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.Persistence; import javax.persistence.Query; import xinCA.entity.Ads; import xinCA.entity.AdsPK; import xinCA.entity.Company; public class EntityTester { static EntityManager em; //声明一个实体管理器 public static void main(String[] args) { System.out.println("Entity Testing"); System.out.println("\nCreating EntityManager"); em = Persistence.createEntityManagerFactory("xinCAJPA") .createEntityManager(); //获取一个实体管理器实例 System.out.println("xinCAJPA EntityManager successfully created\n"); //生成一个企业类 Company aCompany = new Company(); aCompany.setAddress("address info"); aCompany.setCityname("beijing"); aCompany.setCompanyname("XinChangAn"); aCompany.setCompanytype((short)1); aCompany.setDistrictname("ChaoYang"); aCompany.setEmail("[email protected]"); aCompany.setManager("Sam"); aCompany.setPostalcode("100020"); aCompany.setProvincename("Beijing"); aCompany.setTelephone("123456"); try { //开始一个事务。如果是查询,就不需要这个代码 em.getTransaction().begin(); em.persist(aCompany);//持久化一个事务(即:保存一个企业信息) em.getTransaction().commit ();//提交 em.getTransaction().begin(); AdsPK adspk = new AdsPK(); adspk.setCompanyid2(aCompany.getCompanyid()); adspk.setReleasetime(new Timestamp(System.currentTimeMillis())); //生成一个广告实例 Ads ads = aCompany.processAds((short)1,new Timestamp (System.currentTimeMillis()), "just a test", (short)1);

  • ads.setPk(adspk); em.persist(ads); em.getTransaction().commit (); } catch (Exception e) { System.out.println("persistence failed: " + e.getMessage()); } //em.flush(); System.out.println("\nAll companies: "); //通过命名查询来获取所有企业信息,并打印出各个企业信息 Query query1 = em.createNamedQuery("getCompanies"); List compList1 = query1.getResultList(); for (Company comp : compList1) { //这是从 Java 5才开始支持的循环格式 System.out.println(comp.getCompanyname() + " " + comp.getAddress() + " " + comp.getCityname() + " " + comp.getTelephone()); } } }

    7 .运行测试代码。在运行测试代码之前,你需要设置 VM 参数:javaagent:c:/Progra~ 1/IBM/WAS7/plugins/com.ibm.ws.jpa.jar,如图 5-17所示。这些 Java 代理程序在上述测试类的 main 方法执行之前被调用。

    图 5-17 设置 VM 参数

    8.运行测试类。结果显示出测试代码中创建的一个虚拟企业的信息(如图 5-18所示)。

  • 图 5-18 运行测试程序

    5.3 用JDBC自己开发实体管理器

    Java 数据库连接(Java Database Connectivity,JDBC)API 允许开发人员在 Java程序中使用 SQL 语句访问和管理数据库中的数据。下面是 JDBC 的一些常用类:

    java.sql.DriverManager 和 javax.sql.DataSource:用于获取到数据库服务器的连接。

    java.sql.Connection:一个数据库连接。 java.sql.Statement、PreparedStatement 和 CallableStatement:一个可执行语句。可以用来查询和更新数据库。

    java.sql.ResultSet:返回从数据库查询后的结果集。

    在 J2EE 平台上,你可以使用 JDBC API 自己创建访问数据库的类,服务层访问这个类来完成所有数据库的操作。开发人员可以借用 Java Persistance API 的名称和格式,开发一个自己的实体管理器。如图 5-19 所示,新长安云计算平台 1.0 版本就自己开发一个名叫 XCARepository 的类来封装所有数据库的操作。这个类提供了:

  • 图 5-19 新长安自己开发的实体管理器

    persist(java.lang.Object):保存对象到数据库中。这可能牵涉到多个表的操作。 remove(java.lang.Object):从数据库中删除一个对象。这也可能牵涉到多个表的操作。

    merge(java.lang.Object):在数据库中更新一个对象。同样可能涉及到多个表的操作。

    find(Class,String)和 find(Class,int):按照主键值(字符串或 ID)来查找一个对象。

    另外,这个类还提供了事务开始、提交、回滚等操作。

    5.4 数据库连接池和JNDI

    从前的数据库程序的开发模式是:连接到数据库,进行各类操作,然后在退出

    程序之前关闭数据库连接。这种模式有一个问题在于数据库连接这块。当有 2000 个用户同时使用这个程序时,就会有 2000 个数据库连接连接到数据库服务器。如果真是这样,任何数据库服务器都可能瘫痪(假定不在数据库服务器上使用连接池),

    或者拒绝大部分连接。云计算平台上的用户数目巨大,而且访问时间不定。显然,

    我们不能采用传统的做法。一个最佳的解决方法是使用数据库连接池。数据库连接

    池维护着多个数据库连接。当一个程序需要数据库连接时,向连接池申请一个连接,

    在完成一个操作后,立即释放该连接到数据库连接池。如果连接池中的所有连接都

    已经被使用,那么后续的程序就会等待,直到有可用的连接。连接池的一个基本思

    想是,让很多程序(或很多用户)共享有限的连接,用后立即归还。另外,创建一

    个新的数据库连接需要比较长的时间,通过使用连接池中的可用连接,应用程序避

    免了花费时间来创建一个新的数据库连接,从而节省了时间。

  • 在 Web 应用服务器上,管理员可以通过创建数据源(DataSource)来完成数据库连接池的创建。图 5-20 显示了新长安所定义的一个数据源。在数据源中,可定义数据源名称、JNDI 名称等信息。另外,在数据源上提供了“测试连接”按钮,可以用来测试连接池是否创建成功(即:能否连接到数据库)。

    如图 5-21 所示,针对连接池部分,在数据源中指定“组件管理的认证别名”。通过这个别名,设置访问数据库的用户名和密码。另外,连接池属性指定了最小/最大连接数等。

    此外,也可定义数据库服务器的位置(包括同数据库服务器通讯的端口号)、

    数据库名称、JDBC 驱动程序类型等信息(见图 5-22)。

    下面我们阐述具体创建数据源的步骤。在创建数据源之前,你需要首先创建 J2C认证,然后创建 JDBC 提供程序。最后创建数据源。

    图 5-20 数据源信息

  • 图 5-21 组件管理的认证别名

    图 5-22 连接数据库的信息

    5.4.1 创建J2C认证

    如图 5-23 所示,在“安全性”下,选择“全局安全性”,然后,在右边的“Java认证和授权服务”下,选择“J2C 认证数据”。

  • 图 5-23 选择“J2C 认证数据”

    在 J2C 认证数据页上,单击“新建”按钮。输入别名、用户名和密码(见图 5-24)。用户名和密码是用来连接到数据库。保存即可。

    图 5-24 设置用户名和密码

    5.4.2 创建JDBC提供程序

    下面我们创建 JDBC 提供程序。

    1.如图 5-25 示,在资源下,选择“JDBC”,选择“JDBC 提供程序”。从作用域中选择服务器。然后单击“新建”。

  • 图 5-25 JDBC 提供程序

    2.在“创建新的 JDBC 提供程序”页面下(见图 5-26),选择数据库类型。支持的数据库类型有 DB2、Derby、Oracle、Sybase、SQL Server 等。这里选择DB2。然后,选择提供程序类型和实现类型等配置(如图 5-27 所示)。

    图 5-26 选择数据库类型

  • 图 5-27 选择提供程序类型

    3.单击“下一步”,设置类路径信息(见图 5-28)。

    图 5-28 设置类路径

    4.单击“下一步”,然后单击“完成”按钮并保存。现在你已经创建了 JDBC提供程序。

    5.4.3 创建数据源

    1.在 JDBC 下,选择“数据源”,界面如图 5-29 所示。

  • 图 5-29 数据源

    2.单击“新建”按钮,输入数据源名和 JNDI 名称(见图 5-30)。

    图 5-30 输入 JNDI 名称等信息

    3.单击“下一步”,选择刚刚创建的 JDBC 提供程序(见图 5-31)。

  • 图 5-31 选择创建的 JDBC 提供程序

    4.单击“下一步”,输入数据库名、服务器名称等(见图 5-32)。

    图 5-32 设置数据库的名称、服务器名等信息

    5.继续单击“下一步”,在“用于 XA 恢复的认证别名”和“组件管理的认证别名”中,选择刚刚创建的 J2C 认证别名(见图 5-33)。

  • 图 5-33 设置安全信息

    6.单击“下一步”和“完成”按钮,保存到主配置。最后显示如图 5-34 所示。

    图 5-34 创建后的数据源

    7.选择刚刚创建的 xincadb 数据源,单击上面的测试按钮(见图 5-35)来验证

    数据库连接。

  • 图 5-35 测试数据库连接

    5.4.4 JNDI

    JNDI(Java Naming and Directory Interface,即:Java 命名和目录接口)提供了命名和目录服务。JNDI 名称是唯一的,代表了各个资源,从而可帮助开发人员使用JNDI API 来找到各类资源(如:连接数据库的数据源)。EJB 程序可以使用@Resource的方式来注入 JNDI 资源。图 5-35 上即显示了新长安所使用的数据库连接池的 JNDI名称。在下一节中,读者可以看到使用 JNDI 访问数据库连接池的实例。

    5.4.5 获取连接池中的连接

    正如我们在上面提到的,所谓连接池,就是指 Web 应用服务器已经连接了数据

    库,并把这些连接所放在的地方。因此,在程序中无需使用数据库用户名和密码来

    连接数据库。通过下述的程序,非 EJB 程序使用 JNDI 名称即可从 Web 服务器上获得数据源。

    初始化数据源

    privatevoid initDataSource(String dbName) throws XCAException { String jdbcUrl = "jdbc/" + dbName; Properties parms = new java.util.Properties(); try{ javax.naming.Context ctx = new javax.naming.InitialContext(parms); myDS = (javax.sql.DataSource) ctx.lookup(jdbcUrl); ;

  • } catch (NamingException e){ }catch (SQLException sqle){ } }

    申请一个连接

    private void getConnection() throws XCAException{ try{ if (myDS == null) initDataSource(databaseName); if(myDS != null) con = myDS.getConnection(); else }catch (SQLException sqle){ } if (con == null) }

    返回(关闭)一个连接

    private final void closeConnection () { try {if( con != null) con.close(); con=null;} catch (SQLException exc) {} }

    需要数据库连接的程序可以使用上述方法来获得连接,处理业务逻辑并关闭连

    接。这里的关闭连接是将连接返回给连接池。

    5.5 事务管理

    事务是数据库中的一个重要概念。一个经典的例子就是银行转帐。从源帐号中

    减掉转帐金额,在目标帐号上增加转帐金额。这时候,我们需要把这两个操作放在

    一个事务下,从而保证这两个操作全部成功。如果某一个操作失败的话,事务中的

    其他操作要回滚。从而事务确保了数据的完整性。在云计算平台上,如果一个事务

    涉及多个服务,那么,就更要很好地管理事务。

    下面我们以 J2EE 平台为例来看看事务的管理。在 J2EE 平台上,我们有两种选择:

    容器管理事务:就是让 EJB 容器来管理事务(提交、回滚),在 EJB 代码中,并没有任何事务管理的 API 调用。

    Bean 自己管理事务:在 EJB 代码中,使用事务的 API 来界定事务。开发人员可以选择 JDBC 的事务管理 API,也可以选择 JTA(Java Transaction API)来管理事务。

  • 5.5.1 JTA

    J2EE 的 Web 应用服务器提供了 JTS(Java Transaction Service)来实现事务的管理。开发人员使用 JTA 来调用 JTS。JTA 的好处是可以完成分布数据库的更新(即:同时更新多个数据库上的数据)。

    开发人员可以使用 javax.transaction.UserTransaction 的 begin(开始一个事务)、commit(提交)、rollback(回滚)和 setTransactionTimeout(设置事务超时的时间)来完成事务相关的操作。

    5.5.2 JDBC上的事务管理

    在 java.sql.Connection 类上,提供了一个 setAutoCommit 方法。缺省情况下,这个设置为 true。也就是说,JDBC 对待每条 SQL 语句为一个事务。在一个语句执行后,就提交。开发人员在需要管理一个事务时,就可以设置为 false。这就是事务的开始。当需要提交时,就调用 commit()方法;当需要回滚时,就调用 rollback()方法。在新长安云计算平台 1.0 版本上,使用了 JDBC 的 API 来实现事务的管理,代码如下:

    /**

    * 开始一个事务

    * @throws XCAException if a SQLException occurs

    */

    public void beginTransaction() throws XCAException {

    try {

    if (!this.isConnected())

    connect();

    con.setAutoCommit(false);

    }

    catch (SQLException sqle) {

    logger.error("Failed to start a transaction");

    logger.error(sqle.getMessage() + " " +

    sqle.getErrorCode(),sqle);

    throw new XCAException(FAILED_TO_START_TRANSACTION,

    XCAMessageManager.getMessage(

    FAILED_TO_START_TRANSACTION, "" +

    sqle.getErrorCode()));

    }

    }

    /**

    * 关闭一个结果集

    * @param rs the result set to be closed

    * @throws XCAException if a SQLException occurs

    */

    private void close(ResultSet rs) throws XCAException {

  • try {

    if (rs != null)

    rs.close();

    }

    catch (SQLException sqle) {

    logger.error("Failed to close a resultset");

    logger.error(sqle.getMessage() + " " + sqle.getErrorCode(), sqle);

    throw new XCAException(FAILED_TO_CLOSE_RESULTSET,

    XCAMessageManager.getMessage (FAILED_TO_CLOSE_RESULTSET,

    "" + sqle.getErrorCode()));

    }

    }

    /**

    * 关闭一个 statement对象

    * @param stmt the statement to be closed

    * @throws XCAException if a SQLException occurs

    */

    private void close(Statement stmt) throws XCAException {

    try {

    if (stmt != null)

    stmt.close();

    }

    catch (SQLException sqle) {

    logger.error("Failed to close a statement");

    logger.error(sqle.getMessage()+ " " + sqle.getErrorCode(), sqle);

    throw new XCAException(FAILED_TO_CLOSE_STATEMENT,

    XCAMessageManager.getMessage(FAILED_TO_CLOSE_STATEMENT,

    "" + sqle.getErrorCode()));

    }

    }

    //关闭一个连接(如果是使用连接池,则返回连接到连接池)

    private final void closeConnection() {

    if (!useConnPool)

    return;

    try {

    if (con != null)

    con.close();

    con = null;

    logger.debug("connection is closed.");

    }

    catch (SQLException exc) {

    }

    }

    /**

    * 提交一个事务

    *

    * @throws XCAException - If failed to commit or setAutoCommit

  • */

    public void commit() throws XCAException {

    try {

    con.commit();

    con.setAutoCommit(true);

    logger.debug("commited the transaction");

    }

    catch (SQLException sqle) {

    logger.error("Failed to commit the transaction");

    logger.error(sqle.getMessage()+ " " + sqle.getErrorCode(), sqle);

    throw new XCAException(FAILED_TO_COMMIT,

    XCAMessageManager.getMessage(FAILED_TO_COMMIT, ""

    + sqle.getErrorCode()));

    }

    }

    //获取一个数据库连接(如果使用一个连接池,则从连接池中获取一个连接)

    public Connection connect() throws XCAException {

    if (this.isConnected())

    return con;

    if (this.useConnPool) {

    this.getConnection();

    }

    else {

    con = connect(this.databaseName, this.dbUserId,

    this.dbPassword);

    }

    return con;

    }

    /**

    * 使用 JCC驱动程序连接到数据库

    * @param dbName name of database

    * @param userName name of user

    * @param password user password

    * @throws XCAException if it failed to get a db2 connection

    */

    public Connection connect(String dbName, String userName, String password)

    throws XCAException {

    useConnPool = false;

    if (con == null) {

    try {

    //load the universal jdbc drvier into JVM. may throw a

    ClassNotFoundException

    Class.forName("com.ibm.db2.jcc.DB2Driver");

    // get a connection to a given database

    con = DriverManager.getConnection("jdbc:db2:" +

    dbName.trim(), userName, password);

  • databaseName = new String(dbName);

    dbUserId = new String(userName);

    dbPassword = new String(password);

    logger.info("Connected to database successfully. ");

    }

    catch (ClassNotFoundException e) {

    logger.error("Failed to load database driver: " +

    e.getMessage());

    throw new XCAException(FAILED_TO_LOAD_JDBC_DRIVER,

    XCAMessageManager.getMessage(

    FAILED_TO_LOAD_JDBC_DRIVER, " DB2 UDB"));

    }

    catch (SQLException sqle) {

    logger.error("Failed to connect to the database");

    logger.error(sqle.getMessage() + " "

    + sqle.getErrorCode(),sqle);

    throw new XCAException(FAILED_TO_GET_DB_CONNECTION,

    XCAMessageManager.getMessage(

    FAILED_TO_GET_DB_CONNECTION, "" +

    sqle.getErrorCode()));

    }

    }

    else

    logger.info("Already connected. Skip...");

    return con;

    }

    /**

    * 回滚一个事务

    * @throws XCAException if a SQLException occurs

    */

    public void rollback() throws XCAException {

    try {

    if (con.getAutoCommit())

    logger.debug("the auto commit is true");

    else

    logger.debug(" the auto commit is false");

    con.rollback();

    con.setAutoCommit(true);

    logger.debug("rolled back the transaction");

    }

    catch (SQLException sqle) {

    logger.error("Failed to rollback the transaction");

    logger.error(sqle.getMessage() + " " + sqle.getErrorCode(), sqle);

  • throw new XCAException(FAILED_TO_ROLLBACK,

    XCAMessageManager.getMessage(FAILED_TO_ROLLBACK, ""

    + sqle.getErrorCode()));

    }

    }

    5.6 设计云数据库

    数据库设计的第一步是标识要存储在数据库表中的数据的类型。一个数据库包

    括一个机构或企业中的实体以及他们之间的关系的信息。数据库的逻辑设计即是描

    述数据库组织结构,生成数据库模式。数据库模式定义下述内容:存储什么信息、

    数据的组织、需要什么表、列的定义。推荐的方法是采用 ERA 模型。

    5.6.1 ERA模型

    ERA 模型的作用是描述一个组织的概念模型,ERA 模型主要由 3 个组件组成。

    1.实体(entity):客观存在并可相互区分的事物,如:一个零售店,一件商品。我们在ERA模型中一般用长方形表示。在DB逻辑设计中,它被转化为表。

    2.关系(relation):独立的实体相互之间的关系,如:零售店和商品之间是销售关系。我们在ERA模型中一般用菱形表示。在DB的逻辑设计中,关系是通过主键和外键来描述,用于维护参照完整性。实体间的关系虽然复杂,

    但抽象以后,可把它们归结为 3 类:

    一对一联系(1:1):对于实体集 A 中的每一个实体,实体集 B 中至多有一个实体同它联系,反之亦然。我们把这种关系定义为 1:1。如:在一个公司中,一个经理管理一个部门;一个部门只有一个经理。那么,部

    门和经理之间是一对一关系。

    一对多联系(1:N):对于实体集 A 中的每一个实体,实体集 B 中有 n(n>=0)个实体同它联系,反之, 对于实体集 B 中的每一个实体,实体集A 中至多有一个实体同它联系。我们把这种关系定义为 1:N。如:一个商品类别(如:上衣)有多件商品。所以,品类和商品之间是一对多的关

    系。

    多对多联系(M:N):对于实体集 A 中的每一个实体,实体集 B 中有 n(n>=0)个实体同它联系,反之, 对于实体集 B 中的每一个实体,实体集A 中也有 m(m>=0)个实体同它联系。我们把这种关系定义为 M:N。如:一个零售店可以销售多类商品,而每类商品可以在多个零售店中销售。

    那么,零售店和商品之间的销售关系是多对多的关系。

    3.属性(attribute):实体所具有的某一特征。如:零售店的名称、地址、负责人姓名等。在 ERA 模型中一般用椭圆形表示。在 DB 的逻辑设计中,属

  • 性被转化为表中的列或字段。例如,实体“零售店”在 DB 中对应的表字段:零售店编号、零售店名称、地址、负责人姓名等等。

    下面我们来举一个新长安的例子,请见图 5-36。我们通过 ERA 模型描述了两个实体:商品和零售店的关系。商品有四个属性:商品编号、商品名称、商品价格和

    出厂日期。零售店也有四个属性:零售店编号、零售店名称、地址、负责人姓名。

    零售店和商品的关系是销售。销售本身也有两个属性:销售日期和销售数量。

    商品编号 商品名称 价格 出厂日期

    销售

    商品

    零售店

    零售店编号 零售店名称 负责人姓名 地址

    销售时间 销售数量

    图 5-36 新长安系统中的 ERA 模型

    5.6.2 从ERA模型到逻辑数据库的转化

    你可以采用两类方法将 ERA 模型转化为逻辑数据库。一种是采用如 ERWin 这样的工具,另一类是手工的方法将它们转化。下面我们描述手工的转化方法,也是

    工具转化的原理。

    在 ERA 模型中,实体转换为表名,属性转化为列。ERA 模型中的关系是通过主键/外键的参照关系体现的。针对 3 种关系,你应该遵循以下原则:

    表 A和表 B是一对一联系(1:1):生成两张表。另外,你既可以将表 A的主键列添加到表 B中充当外键,也可以将表 B的主键列添加到表 A中充当外键。

    表 A 和表 B 是一对多联系(1:N):生成两张表。你必须把表 A 的主键列添加到表 B 充当外键。如:类别和商品之间是一对多的关系。所以,在商品表中添加了“分类号”列,该列是类别表的主键。

    表 A 和表 B 是多对多联系(M:N):除了生成表 A 和表 B 外,还应该生成一张关系表。这个关系表的列:由表 A 的主键+表 B 的主键+关系自己的属性。例如:零售店和商品是多对多的关系,即:一个商品可以在多个零售店中销

    售,而一个零售店也可以销售多个商品,那么,除了生成零售表和商品表外,

    还应该生成一张关系表——销售表。销售表的列有商品编号(商品表的主键)、零售店编号(零售店表的主键)、销售时间(自己属性)和销售数量

  • (自己属性)。

    现在的应用程序越来越需要保存非结构化数据(如:XML 数据)。在设计中,你可能还要考虑一些特别属性以便支持多媒体对象,如文档、视频、图象和语音。

    另外,在设计中除了标识数据外,还应标识其他类型的信息,如应用于该数据的商

    业规则。

    5.6.3 指定表的主键

    主键是指一列或若干列的有序集合,它的值唯一标识一行。如:零售店表中的

    零售店编号。每一个值都代表了每一个零售店的完整信息。零售店名称不能唯一地

    标志一个店,这是因为店有可能重名。主键实施实体完整性。每个表必有且仅有一

    个主键。每一个主键值必须唯一,而且不允许 NULL 或重复。我们建议读者,尽量不要改变主键值。

    可能有几列入选一个表的主关键字。可将每个入选列视为唯一的。应考虑只将

    其中一列作为主关键字,然后对其他的一列或多列创建唯一约束或唯一索引。

    在某些情况下,使用时间戳作为该关键字的一部分可能有帮助,例如,当一个表没

    有“自然”的唯一关键字时,又或许到达顺序是用于区分唯一行的方法时。

    在一些情况下,需要用两列或更多的列构成主关键字。包含多列的关键字是组合关

    键字。如:上述销售表中,零售店编号、商品编号和销售时间一起组合成一个关键字。

    列值的组合应定义一个唯一的实体。若指定组合关键字不太容易,可以考虑加上一个具

    有唯一值的新列(如:销售流水号)。

    有时,你可能发现有多个入选关键字。如在员工表中,有多个入选关键字。雇

    员编号 、电话号码和身份证号码这三列都唯一地标识该员工。从一组入选关键字中选择一个主关键字的标准应是根据关键字的持久性、唯一性和稳定性。

    持久性表示该行始终存在该主关键字。 唯一性表示每行的每个关键字值始终是不同的。 稳定性表示不应将该主关键字值更改为另一个值。

    在上述三个入选关键字中,只有员工编号满足以上标准。一个员工在进入一家

    公司时,可能没有电话号码。身份证号码也可能改变,比如:公安局出了新身份证,

    添加了三位。尽管它们在某个时候是唯一的,但不能保证始终如此。因此,员工编

    号列是主关键字的较好选项。一个员工只被一次赋予一个唯一的号码,而且只要该

    员工在该公司内供职,通常不会更新该号码。因为每个员工必须有一个号码,因此

    员工编号列是持久的。

    5.6.4 考虑使表规范化

    使表规范化是数据库设计中一个重要的主题。规范化有助于避免数据中出现冗

    余现象和不一致。规范化的中心思想是将表精简为一组列,在这组列中,所有非关

  • 键字列都取决于该表的整个主关键字。若不是这样,则在更新期间该数据可能变得

    不一致。

    本节简要回顾数据库表的第一种、第二种、第三种和第四种规范形式的规则,

    并描述应遵守或不应遵守它们的一些原因。数据库表的第五种规范形式在有关数据

    库设计的许多书籍中都有说明,在此就不再赘述。以下是规范式的简要说明:

    第一种规范形式

    若一个表中的每一行和每一列均有一个值,而不会是一组值,则该表满足第一

    种规范形式的要求。表 5-2 违反了第一种规范形式,因为对于一个具体的商品,仓库列都包含了几个值。

    表 5-2 违反第一种形式的表

    商品编号(主关键字) 仓库名称 P000010 北京仓库,上海仓库,湖州仓库 P000020 杭州仓库、长春仓库

    表 5-3 是经过修改后符合第一种规范形式的表:

    表 5-3 符合第一种规范形式的表

    商品编号(主关键字) 仓库名称(主关键字) 数量 P000010 北京仓库 400 P000010 上海仓库 543 P000010 湖州仓库 329 P000020 杭州仓库 200 P000020 长春仓库 278

    第二种规范形式

    若不在关键字中的每一列只取决于整个关键字,则该表使用的是第二种规范形

    式。这表示不是主关键字一部分的所有数据必须取决于该关键字中的所有列。这减

    少了数据库表之间的重复。当一个非关键字列只与一个组合关键字的子集相关时,

    就违反了第二种规范形式,如下例所示。表 5-4 是一个库存表,记录了存储在某个仓库中的特定商品的数量。

    表 5-4 违反第二种规范形式的表

    商品编号(主关键字) 仓库名称(主关键字) 数量 仓库地址 P000010 北京仓库 400 北三环中路18号 P000010 上海仓库 543 南京东路28号 P000010 湖州仓库 329 人民路38号 P000020 杭州仓库 200 解放路48号 P000020 长春仓库 278 延安路58号

    在该表中,关键字是由商品编号和仓库名称列组成的。因为仓库地址列只取决

    于仓库的值,因此该表违反了第二种规范形式的规则。此设计存在下列问题:

    对于该仓库中存放的一个商品,在每个记录中重复了该仓库地址。 若该仓库的地址变更,则必须更新在该仓库中的每一个商品。

  • 由于存在冗余现象,该数据可能变得不一致,表现为不同的记录对相同的仓库显示不同的地址。

    若某个时候该仓库中没有存储商品,就可能没有哪一行记录了该仓库地址。

    要满足第二种规范形式,应将表 5-4 中显示的信息分成下面两个表(表 5-5 和表5-6)。

    表 5-5 商品库存表

    商品编号(主关键字) 仓库名称(主关键字) 数量 P000010 北京仓库 400 P000010 上海仓库 543 P000010 湖州仓库 329 P000020 杭州仓库 200 P000020 长春仓库 278

    表 5-6 仓库表

    仓库名称(主关键字) 仓库地址 北京仓库 北三环中路18号 上海仓库 南京东路28号 湖州仓库 人民路38号 杭州仓库 解放路48号 长春仓库 延安路58号

    上述两个表符合第二种规范形式。当然,在两个表使用第二种规范形式时,就

    要多出一些性能开销。有时需要将这两个表连接起来以获得部分商品和其所在仓库

    地址的相关信息。

    第三种规范形式

    若每个非关键字列的值与其他非关键字列无关,而只取决于该表的关键字,则

    该表使用的是第三种规范形式。当一个非关键字列的值与另一个非关键字列的值相

    关时,就违反了第三种规范形式。例如,表 5-7 包含员工编号、工作部门、部门经理列。则部门经理列取决于工作部门,而主关键字是员工编号列;因此,该表现在

    违反了第三种规范形式。

    表 5-7 员工表

    员工编号(主关键字) 姓名 工作部门 部门经理

    E000010 曹操 董事会 杨正洪 E000011 黄盖 测试部门 孙权 E000012 周瑜 测试部门 孙权 E000013 张飞 开发部 刘备 E000014 关羽 开发部 刘备

    例如,更改员工张飞的部门就不会更改该部门中其他雇员(如:关羽)的部门

    名。显然,会产生不一致的结果。可新建一个含有工作部门列和部门经理列的新表,

    将该表规范化。在这种情况下,像更改部门名这种更新就更容易——只对新表更新即可。显示部门经理名和员工名的 SQL 查询需要连接上述两个表。此查询执行的时间可能比单个表查询的时间长。此外,整个排列要占用更多的存储空间,因为工

  • 作部门列必须出现在两个表中。表 5-8 和表 5-9 是经规范化后的两个表。

    表 5-8 员工表

    员工编号(主关键字) 姓名 工作部门 E000010 曹操 董事会 E000011 黄盖 测试部门 E000012 周瑜 测试部门 E000013 张飞 开发部 E000014 关羽 开发部

    表 5-9 部门表

    工作部门 部门经理

    董事会 杨正洪 测试部门 孙权 开发部 刘备

    第四种规范形式

    若没有一行包含有关一个实体的两个或更多个独立的多值事实,则该表使用的

    是第四种规范形式。比如:员工技能表中包含了员工编号、编程语言和数据库产品

    三列(见表 5-10)。一个员工熟悉两种编程语言,并精通两个数据库产品。这里有两个关系:一个关系是员工和编程语言之间的关系,另一个关系是员工和数据库产

    品之间的关系。若一个表表示了这两种关系,则它未使用第四种规范形式。

    表 5-10 员工技能表

    员工编号(主关键字) 编程语言 数据库产品

    E000010 JAVA MYSQL E000010 JSP MYSQL E000010 JAVA IBM DB2 E000010 JSP IBM DB2

    应该改用两个表来表示这种关系,如下所示(见表 5-11 和表 5-12)。

    表 5-11 员工编程技术表

    员工编号(主关键字) 编程语言

    E000010 JAVA E000010 JSP

    表 5-12 员工数据库技术表

    员工编号(主关键字) 数据库产品

    E000010 MYSQL E000010 IBM DB2

    设计一个良好数据库的经验是使用第四种规范形式将所有数据安排在表中,然

    后决定该结果是否提供了一个可接受的性能级别。若不是,可退到第三规范形式。

    总之,在设计数据库时,我们应该首先考虑创建规范化的数据库,一般满足 3NF(第三种规范,即:每一个非主属性既不部分依赖于码,也不传递依赖于码)即可。

  • 然后,再根据实际的编程难易程度,考虑非规范化程度。如:你可以添加冗余列,

    目的是减少应用程序中的两个表的连接,方便编程和加快整个程序的执行速度。冗

    余的缺点是,你自己必须保证值的完整性(可通过触发器或其他方式完成)。

    5.6.5 逻辑数据库设计的物理实现

    在完成数据库的逻辑设计后,下一步是数据库的物理设计,或叫数据库的物理

    实现。这一步首先是根据设计的数据库的结构和以后的数据量、查询和更新的频率,

    来决定选用哪个数据库管理系统,如: Microsoft SQL Server、Oracle、IBM DB2、SYBASE 和 MySQL 等。然后,根据具体的 DBMS,来完成数据库的物理实现。比如:在 Microsoft SQL Server 中,需要创建数据库,创建表等其他数据库对象。创建的方法是采用数据定义语言(DDL)或图形化工具来物理实现数据库模式。

    5.6.6 设计和实现数据完整性要求

    在创建了数据库之后,需要考虑数据的完整性、数据的安全性等要求。数据的

    完整性是指数据的正确性、有效性和相容性。系统提供必要的功能,保证数据库中

    的数据在输入、修改过程中符合原来的定义和规定,如:月是 1~12 之间的整数,零售店编号是唯一的,等等。在数据库服务器上,我们通过缺省、规则、约束,编写

    触发器、存储过程来实现数据完整性要求。约束是数据库管理程序实施的规则,具

    体包括:

    唯一约束:确保一个表中一个关键字的值是唯一的。 参照完整性:在插入、更新和删除操作上实现参照约束。所有外键值都是父表的某一个主键才是一个数据库的正确状态。

    检查约束:验证更改后的数据有无违反当创建或更改表时指定的条件(如:月必须是 1 到 12 之间)。

    另外,也可以通过触发器来实现数据完整性要求。触发器定义要执行的一组操

    作,当对指定的表执行更新、删除或插入操作时要调用这组操作。

    值得注意的是,完整性的实现可以在数据库设计任一阶段及数据库的各个级别

    上实现。在创建表时,应该考虑如下表限制:

    表名唯一:由数据库服务器实施 列名唯一:由数据库服务器实施 行唯一:由主键来定义

    而且应该考虑列限制:

    非空(NN):要求列中有输入项。 无重复(ND):要求列中无重复值(可通过创建主键、唯一约束、唯一索引或触发器实现)。

    无改变(NC):要求列中不更改值(可通过使用参照约束、触发器或许可实

  • 现)。

    在实现了数据完整性要求之后,还应采用批处理、脚本、触发器、存储过程等

    方式来进行数据库服务器编程。要考虑日志的格式和数据库备份等。