使用 Google Web Toolkit、Apache Derby 和 Eclipse 构建 Ajax 应用程序,第 2 部分: 可靠后端

Apache Derby 提供应用程序的开发基础

本文是使用 Google Web Toolkit(GWT)构建 Asynchronous JavaScript + XML(Ajax)应用程序系列文章的第 2 部分,介绍如何为 Web 应用程序构建 Apache Derby 数据库,并使用它驱动 GWT。本系列文章的 第 1 部分 向您介绍了 GWT,并演示了如何使用它来为 Web 应用程序创建富客户机前端。这一次,您将走进幕后,了解如何使用数据库和用于将数据转换为 GWT 可用格式的代码,从而设置后端。阅读完本文后,您将可以使前端和后端相互通信。

Noel Rappin (noelrappin@gmail.com), 高级软件工程师, Motorola, Inc.

Noel Rappin 是佐治亚理工学院 Graphics, Visualization and Usability Center 的哲学博士,也是摩托罗拉公司的一名高级软件工程师。他还是 wxPython in Action and Jython Essentials 的合著者之一。



2007 年 4 月 04 日

在本文中,您将安装并配置数据库 —— Web 应用程序的后端,来创建数据库模式,并了解一些用于向其中填充数据的简单工具。您将要使用的数据库是 Apache Derby,100% 纯 Java™ 关系型数据库,该数据库最初是在 Cloudscape™ 的名下开发的。最后,IBM® 收购了 Cloudscape 代码,继而将其开源版本贡献给了 Apache 项目。Sun Microsystems 的 JavaDB 名下发行了同样的一个项目,但两者没有丝毫混同之处。

查看 Ajax 资源中心,这是您的一站式 Ajax 编程模型信息中心,包括文章、教程、论坛、blog、wiki、事件和新闻。所发生的任何事情都会在这里介绍。

我选择 Derby 并不是因为它有三个名字,而是因为它是轻量级的并且易于配置。与大多数关系型数据库不同,Derby 可以在与 Java 端服务器代码所在的同一 Java 虚拟机(JVM)中运行。(如果您喜欢,也可以在单独的 JVM 上运行它。)这使开发和部署变得更容易,而且 Derby 的速度很快,是中小型 Web 应用程序的理想选择。

开始之前,有几点注意事项:第一,要读懂本文,您应当掌握关系型数据库、JDBC 和结构化查询语言(SQL)的基础知识。第二,为了达到演示目的,本文在代码中提供了一些在生产系统中可能不太理想的内容。我在讲述过程中尝试将那些元素指出来,但是在这里将不讨论性能优化问题。

获得 Derby

Derby 是作为 Apache DB 项目的一部分提供的。撰写本文时,最新版本是 10.1.3.1 版。如果要在 Eclipse 集成开发环境(IDE)中工作,则获取 derby_core_pluginderby_ui_plugin 两个插件就足够了。如果不是,则可选择满足您需求的任何其他发行版。这些发行版中,有的只包含库文件,有的包含库和文档,有的包含带有调试信息的库,还有只有源代码的发行版。Derby 专以 Java 技术为基础,可以在任何 1.3 或更高版本的 JVM 上运行。本文中的代码示例假定您使用的是 Java 1.4。

不使用 Eclipse 设置 Derby

如果不使用 Eclipse,请将下载的发行版解压到您认为方便的任意位置。完成后,请确保文件 lib/derby.jar 和 lib/derbytools.jar 位于 classpath 变量中。您可以在系统级执行此操作,这样做可能有助于将环境变量 DERBY_INSTALL 设为 Derby 所在的目录(包括 Derby 目录本身,位于 /opt/bin/db-derby-10.1.3.1-bin)。还可以在 IDE 或启动程序脚本中执行此操作。如果需要以客户机/服务器模式和嵌入模式使用 Derby,则文件 lib/derbyclient.jar 和 lib/derbynet.jar 还必须在 classpath 中。

使用 Eclipse 设置 Derby

如果使用 Eclipse,为开发所做的设置工作会比较轻松一点。要在 Eclipse 中设置 Derby,请完成以下步骤:

  1. 将两个插件文件解压缩。每个插件文件都有一个名为 plugin 的顶级目录。
  2. 将该目录中的内容复制到 Eclipse 插件目录中。
  3. 在 Eclipse 中打开您的项目。
  4. 单击 Project > Add Apache Derby Nature 进入 Derby 梦幻世界。这样做将把四个库文件添加到项目 classpath 中并为您提供对 ij 命令行提示符的访问权。

图 1 显示了添加了 Derby Nature 之后的 Derby 菜单。

图 1. Eclipse Derby 菜单
Eclipse Derby 菜单

即使使用 Eclipse 进行开发,部署应用程序时也必须有相应的 JAR 文件。我会在稍后的一篇文章里详细介绍此主题。

设计您的模式

在开始使用数据库之前,请花点时间来了解一下数据库应当保存哪些内容。我尚未讨论 Slicr 应用程序的需求,因此让我们假定,您希望数据库能够保存基本的客户信息和订单信息。

在产品的早期阶段处理数据库的技巧是使其保持简单并且尽量少使用特定于数据库系统的功能,即使这意味着初始时要在 Java 代码中执行额外的处理。数据库与第三方有很强的依赖关系,因此应避免让数据库决策来驱动应用程序的其余部分。需要使程序与数据库之间的联系点尽可能少,以便在某个点更改系统时,可以顺利进行更改。压力来自在改善数据库性能时所做的的大部分事务都会要求您使用特定的系统,因此尝试将此类优化工作推迟至项目必须进行优化的最后一刻。

数据库设计的起点很简单。客户下订单。订单包括一份或多份比萨(此时,忽略快餐店可能出售其他食物)。比萨有无浇头或多种浇头之分,可按半张或整张比萨放浇头。

创建客户表

现在,您只需关心获得足够的客户信息来交付和确认订单,如清单 1 所示。

清单 1. 客户表
CREATE TABLE customers (
    id int generated always as identity constraint cust_pk primary key,
    first_name varchar(255),
    last_name varchar(255),
    phone varchar(15),
    address_1 varchar(200),
    address_2 varchar(200),
    city varchar(100),
    state varchar(2),
    zip varchar(10)
)

CREATE 语句稍微有一些不符合标准的 SQL 语法。创建一个 ID 列,您需要 Derby 为该列中的每个新行自动加一。指定该行为的子句为:

id int generated always as identity

Identity 列的其他选项为:

generate by default as identity

差别在于 generate by default 允许您将自己的值放入该列中,然而 generate always 不允许这样做。还将 ID 列标识为表的主键。

您总是希望数据库中具有一个和现实中的数字完全没有联系的 ID。最终团队中总是会有某个成员试图说服您使用电话号码之类的数据作为主键,因为它将惟一地标识客户。不要那样做。您绝对不会希望由于有人移动和更改了电话号码而更新整个数据库。

创建订单表

对于订单表(参见清单 2),只需要将其绑定与一位客户和一个日期相关联,并允许折扣。您可以在代码中计算价格的其余部分。

清单 2. 订单表
CREATE TABLE orders ( 
	id int generated always as identity constraint ord_pk primary key,
    customer_id int constraint cust_foreign_key references customers, 
    order_time timestamp,
    discount float
)

除了 id 主键以外,还声明了 customer_id 列作为引用客户表的外键。(如果在声明中不包括外键,Derby 将假定引用的是其他表的主键。)这意味着 Derby 将验证添加到这张表的任何 customer_id 实际上是否与系统中的客户匹配。系统管理员将告诉您应当始终执行此匹配操作。但是,我认为在一些合理的情况下可能不需要数据库一直都执行严格验证。例如,可能需要先输入数据,然后才能知道或验证外键是什么。此外,可能需要删除外键但需要保留表行。例如,在本例中,您可能需要删除一个客户,但出于数据收集目的,需要保留客户的订单。您可以通过一些技巧使 Derby 允许那样做,但是可能无法移植到其他数据库系统。

创建浇头表

最后一个数据库设计问题是比萨和浇头。恩,其实浇头本身并不是什么问题;那十分简单,如清单 3 所示。

清单 3. 浇头表
CREATE TABLE toppings(
    id int generated always as identity constraint top_pk primary key,
    name varchar(100), 
    price float
)

问题在于,如何管理比萨与浇头的关系?一张比萨就等同于一份订单,涉及尺寸和一组浇头。典型的数据库标准会建议需要先创建一张比萨表,然后再创建一张将比萨 ID 与浇头 ID 关联起来的多对多表。这样做有很多很好的属性,其中一个事实是它允许在一个比萨上使用无数种浇头。但是,管理表之间的数据库关系会造成性能损耗。如果不需要如此多的浇头,则可以将若干个浇头字段包括在比萨表中(topping_1topping_2 等等)。从概念上讲,那会更简单点,但会导致难于(比方说)挖掘订单数据来统计最受欢迎的浇头。如果您特别敢于冒险,您可以用一个浇头字段并用一张位图或连接字符串等内容来填充该字段。但我真的不建议那样做。

创建比萨表

经过一番考虑之后,我决定使用完全正规的表单。您希望对一个比萨使用足够多的浇头,将它们全都放在同一张表中会变得十分难看。因此,请使用清单 4 中所示的代码。

清单 4. 比萨表
CREATE TABLE pizzas (
    id int generated always as identity constraint piz_pk primary key,
    order_id int constraint order_foreign_key references orders,
    size int 
)

CREATE TABLE pizza_topping_map (
    id int generated always as identity constraint ptmap_pk primary key,
    pizza_id int constraint pizza_fk references pizzas,
    topping_id int constraint topping_fk references toppings,
    placement int
)

仅为了清晰起见,将用尺寸 1、2、3、4 分别表示小号、中号、大号和超大号。左半个比萨、整个比萨和右半个比萨的浇头布置分别为 -1、0 或 1。并且每个映射都必须要有一个单独的 ID,以便可以,比方说,允许额外添加意大利辣香肠,方法为让意大利辣香肠作为浇头在同一个比萨中出现两次。

注:我是否曾提到过放在 constraint 之后的所有那些名称在整个数据库中必须是惟一的?它们必须是。Derby 实际上将在后台创建索引,并且每个索引都必须有一个惟一名称。

对于您的数据库模式,应当那样做。现在可以将其置入数据库中。

填充数据库

模式已经有了;现在必须设置模式并准备一些初始数据。您将创建一个简短的独立程序来执行此设置过程。但是,那不是惟一的选择。您可以使用 Derby ij 命令行直接输入 SQL 命令,也可以使用一个图形化 SQL 工具。不过,编程方法将给您提供一种可很好控制的方法来查看如何启动 Derby 以及 Derby 与其他 JDBC 数据库的区别。实际上,也可能把 SQL 模式保存在它自己的 SQL 脚本中。

开始时使用一些十分静态的数据 —— 第 1 部分的 Slicr 页面中包括的比萨浇头列表。同样,这里主要使用此方法,因为您要插入静态数据。设置一张浇头表,其中每个浇头都有一个名称和一个底价。清单 5 中所示的代码将设置该数据。目前,假定所有浇头的价格都一样。

清单 5. 在 Derby 中设置浇头表
public class SlicrPopulatr {

    public static final String[] TOPPINGS = new String[] {
        "Anchovy", "Gardineria", "Garlic", 
        "Green Pepper", "Mushrooms", "Olives", 
        "Onions", "Pepperoni", "Pineapple", 
        "Sausage", "Spinach"
    }
    
    public void populateDatabase() throws Exception {
        Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance();
        Connection con = DriverManager.getConnection(
                "jdbc:derby:slicr;create=true");
        con.setAutoCommit(false);
        Statement s = con.createStatement();
        s.execute("DROP TABLE toppings");
        s.execute("CREATE TABLE toppings(" +
                "id int generated always as identity constraint top_pk primary key, " +
                "name varchar(100), " +
                "price float)");
        //        
        // All the other create table statements from above would go here...
        //
        for (int i = 0; i < TOPPINGS.length; i++) {
            s.execute("insert into toppings values (DEFAULT, '" +
                    TOPPINGS[i] + "', 1.25)");
        }
        con.commit();
        con.close();
        try {
            DriverManager.getConnection("jdbc:derby:;shutdown=true");
        } catch (SQLException ignore) {}
    }    
    
    public static void main(String[] args) throws Exception {
        (new SlicrPopulatr()).populateDatabase();
    }

如果您熟悉 JDBC 的话,则不会对这段代码中的大部分代码感到陌生。不过,还有几个特定于 Derby 的特性我必须介绍。开始时需要使用 Class.forName 方法装入驱动类。由于要使用的是嵌入式版本的 Derby,因此驱动程序的类名为 org.apache.derby.jdbc.EmbeddedDriver。接下来,创建连接字符串。Derby URL 的格式为:

jdbc:derby:数据库名称;[attr=value]

数据库名称是引用数据库时使用的名称。您选什么名称都没有关系,只要与在服务器代码中再次打开数据库时所用的名称保持一致即可。

创建连接后,您就处于标准的 JDBC 中了。创建一个 Statement 来执行删除和重新创建表的命令,这允许您在数据库受损时通过此程序重置数据库。(否则,Derby 将在尝试创建一张已经存在的表时抛出异常)。创建表后,需要对浇头数组中的每个条目使用一条 insert 语句。

insert 语句中的 SQL 代码有一个您可能不需要的功能。我使用了关键字 DEFAULT 作为 Identity 列的占位符。如果不在 insert 语句中指定字段列表,则 Derby 希望使用 Identity 列中的关键字。

程序存在之前,需要进行一次特殊调用获取一个与 URL "jdbc:derby:;shutdown=true" 的连接 —— 无需指定数据库。此调用告诉 Derby 系统关闭并释放所有可能活动的连接。

运行这个小程序后,您将在名为 derbyDb 的应用程序顶级目录中看到一个目录。此目录将存储 Derby 存储数据的二进制文件。请不要以任何方式更改那些文件。

准备好数据用于 GWT

数据库模式就绪并装入了静态数据后,现在必须说明如何将数据通信给客户机,反之亦然。最后,您不得不序列化整个客户机-服务器连接上的数据。为了序列化运行,最后的数据类必须在 GWT 能看到和处理的位置,这意味着这些类必须定义在 client 软件包中并且可由 GWT Java-to-JavaScript 编译器编译。

对于将要序列化的客户机类还有一些附加限制。举例来说,该类必须实现接口 com.google.gwt.user.client.rpc.IsSerializable,该接口是一个标记接口,不定义任何方法。此外,该类中的所有数据字段本身必须是序列化的。(与普通的 Java 序列化一样,您可以通过将字段标记为 transient 使其免除被序列化。)

怎样的字段才是可序列化字段?首先,该字段可属于一个实现了 IsSerializable 的类型,或者具有一个实现了 IsSerializable 的超类。或者,该字段可以是基本类型之一,其中包括 Java 原语,所有原语包装类,DateString。序列化类型的数组或集合也是序列化的。但是,如果要将一个 CollectionList 序列化,GWT 希望您用一个指定实际类型的 Javadoc 注释对其评注,以便编译器可以使其最优化。清单 6 显示了样例字段和方法。

清单 6. 序列化字段和方法
/**
 * @gwt.typeArgs <java.lang.Integer>
 */
private List aList; 

/**
 * @gwt.typeArgs <java.lang.Double>
 * @gwt.typeArgs argument <java.lang.String>
 */
public List doSomethingThatReturnsAList(List argument) {
    // Stuff goes here
}

注:方法列表中的参数只能由注释中的名称指定,而返回值则不是。

注意,该表中的序列化对象中缺少处理 java.sql 和 JDBC 所必需的内容。无论您执行什么操作来获取设为数据对象的结果,都必须在服务器端代码中执行。

此刻,您进入了对象关系映射(Object-Relational Mapping,ORM)的世界,或者将数据从关系型数据库结构转换为 Java 程序的面向对象的结构。对于一个复杂的 Java 生产系统,可能需要使用一个预先存在的成熟的 ORM 系统,例如 Hibernate 或 Castor。这两个系统都将自动把数据从数据库装入到所选的 Java 对象中。但是,它们也要求大量配置,然后才能开始。由于本文主要介绍 Derby 和 GWT,我提供了一个在开发开始时提供服务的快速转换程序。最后,可以将其换为功能更强大的工具。

简单的 ORM 转换程序

首先,为所有数据表创建 bean 类。我使用 Topping 类作为示例,因为它简单并且已经包含有数据。对表中的每个列使用普通 bean 命名约定,但将下滑线转换为大小写混合(例如,topping_id 变成 getToppingId)。清单 7 显示了 Topping 类。

清单 7. Topping 类
package com.ibm.examples.client;

import com.google.gwt.user.client.rpc.IsSerializable;

public class Topping implements IsSerializable {

    private Integer id;
    private String name;
    private Double price;

    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Double getPrice() {
        return price;
    }
    public void setPrice(Double price) {
        this.price = price;
    }
}

接下来是简单的 ORM 工具,如清单 8 所示。

清单 8. 简单的 ORM 工具
package com.ibm.examples.server;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

public class ObjectFactory {

    public static String convertPropertyName(String name) {
        String lowerName = name.toLowerCase();
        String[] pieces = lowerName.split("_");
        if (pieces.length == 1) {
            return lowerName;
        }
        StringBuffer result = new StringBuffer(pieces[0]);
        for (int i = 1; i < pieces.length; i++) {
            result.append(Character.toUpperCase(pieces[i].charAt(0)));
            result.append(pieces[i].substring(1));
        }
        return result.toString();
    }

    public static List convertToObjects(ResultSet rs, Class cl) {
        List result = new ArrayList();
        try {
            int colCount = rs.getMetaData().getColumnCount();
            while (rs.next()) {
                Object item = cl.newInstance();
                for (int i = 1; i <= colCount; i += 1 ) {
                    String colName = rs.getMetaData().getColumnName(i);
                    String propertyName = convertPropertyName(colName);
                    Object value = rs.getObject(i);
                    PropertyDescriptor pd = new PropertyDescriptor(propertyName, cl);
                    Method mt = pd.getWriteMethod();
                    mt.invoke(item, new Object[] {value});
                }
                result.add(item);
            } 
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return result;
    }
}

convertToObjects() 方法只简单地在结果集中循环,使用 JavaBean 映射推断 getter 属性,并设置所有值。convertPropertyName() 方法将在 SQL 加下滑线的命名约定与 Java 大小写混合的约定之间切换。

ORM 工具不具有的功能

如果把此工具缺少的所有有用的 ORM 功能汇集在一起,完全可以出一本书了。例如,工具不能:

  • 避免创建同一个对象的多个版本。
  • 允许回写数据库。
  • 快速运行。

代码所做的工作可能超出了您的想象。您可以在任何数据库工具上立即运行它而无需进一步配置。早期开发时,模式可能更改,您无需使映射文件与数据库保持同步。并且在需要时切换到功能更强大的工具也不难。

清单 9 显示了运行的这个工具,读取回先前创建的所有 Topping 实例。

清单 9. 测试 ORM 工具
public class ToppingTestr {

    public static final String DRIVER = "org.apache.derby.jdbc.EmbeddedDriver";

    public static final String PROTOCOL = "jdbc:derby:slicr;";

    public static void main(String[] args) throws Exception {
        try {
            Class.forName(DRIVER).newInstance();
            Connection con = DriverManager.getConnection(PROTOCOL);
            Statement s = con.createStatement();
            ResultSet rs = s.executeQuery("SELECT * FROM toppings");
            List result = ObjectFactory.convertToObjects(rs, Topping.class);
            for (Iterator itr = result.iterator(); itr.hasNext();) {
                Topping t = (Topping) itr.next();
                System.out.println("Topping " + t.getId() + ": " +
                        t.getName() + " is $" + t.getPrice());
            }
        } finally {
            try {
                DriverManager.getConnection("jdbc:derby:;shutdown=true");
            } catch (SQLException ignore) {}
        }
    }

}

此测试程序将创建一个与 Slicr 数据库的 Derby 连接。(您将不再要求协议字符串根据需要创建数据库)。执行一个简单的 SQL 查询,然后将结果传递给工厂。然后可以自由地在结果列表内循环并退出数据库。

下期精彩预告

现在已经安装并配置了数据库。创建了数据库模式并且发现了一些简单的工具可将数据放入数据库中。阅读了本系列文章中的两篇之后,Slicr 项目现在已经具有了简单但是可以工作的前端和后端。下一步是通信。在本系列文章的第三篇文章中,您将了解 GWT 使用框架如何使远程过程调用(Remote Procedure Call,RPC)可以轻松地编码和管理。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source, Information Management, Java technology
ArticleID=206902
ArticleTitle=使用 Google Web Toolkit、Apache Derby 和 Eclipse 构建 Ajax 应用程序,第 2 部分: 可靠后端
publish-date=04042007