构建一个简易 JavaScript 数据访问层

使用 PHP、jQuery 和 JavaScript 轻松安全地访问数据库

本文逐步描述完成以下任务的全过程:在 PHP 中构建一个针对后端的数据库访问包装器并将其连接到用户界面层中的一个 JavaScript 访问包装器。

Jack D Herrington, 高级软件工程师, Fortify Software, Inc.

Jack Herrington 的照片Jack Herrington 是一位生活和工作在海湾地区的工程师、作家和主持人。您可以通过 http://jackherrington.com 来关注他的工作和作品。



2011 年 5 月 03 日

简单 JavaScript 数据访问介绍

常用缩略词

  • Ajax:异步 JavaScript + XML
  • HTML:超文本标记语言
  • JSON:JavaScript 对象符号
  • PDO:PHP 数据对象
  • REST:具象状态传输
  • SQL:结构化查询语言
  • URL:统一资源定位符
  • W3C:万维网联盟
  • XML:可扩展标记语言

您想快速构建一个的确很酷的 Web 2.0 JavaScript 应用程序原型,但又不想将您的所有时间都耗费在编写连接代码以访问数据库吗?以前,为了获取从数据库到前端的所有传输方法的数据,要使用所有创建、读取、更新和删除(CRUD)方法为数据库中的每个表编写一个类。然后,需要在那些类之上放置一些编组代码,以便为前端提供一个访问层。最后,将一些 JavaScript 库放置到那个访问层之上,以便访问后端。这是多么痛苦的一件事啊!

本文提出一个替代方法:使用单个数据库类包转多个数据库表。单个驱动程序脚本连接前端和后端,前端上的另一个包装类向您提供对所有所需表的访问权。图 1 显示了完整的堆栈。

图 1. JavaScript 数据访问层架构
JavaScript 数据访问层架构图示

在样例应用程序中,一个拥有添加新条目能力的清单页是用户界面的基础。这个页使用一个 JavaScript 类通过后端访问数据库。反过来,该 JavaScript 类使用 jQuery 进行 Ajax 调用。

Ajax 请求受到一个 PHP 驱动程序页的保护,该页先确保请求有效,然后将其传递到一个处理该请求的数据库包装类。包装器返回一些结果,驱动程序将它们格式化为 JSON 并返回浏览器。

这种方法有其局限性。首先,跨多个相关表工作不太好,因为每个请求都是基于每个表而创建的。要修复这个问题,可以添加新的自定义方法跨多个表进行相关请求。第二个问题是数据安全性,没有任何角色或用户访问检查来确保适当的数据访问。这个问题也可以修复, 方法是添加一些代码来处理用户和角色。

这个代码提供的是一种快速、轻松和安全的方法,将数据从数据库传递到用户界面中,从而快速构建应用程序原型。此外,它还是一个不错的学习工具,可用于学习数据库访问、PHP、Ajax、jQuery 和 JavaScript 等技能。


构建数据库

一切都从数据库开始。在本例中,您使用一个作者及其相关图书数据库。清单 1 显示了数据库的 MySQL 的初始化代码。

清单 1. books.sql
DROP TABLE IF EXISTS authors;
CREATE TABLE authors (
  id BIGINT NOT NULL PRIMARY KEY auto_increment,
  name VARCHAR(255) NOT NULL
);

DROP TABLE IF EXISTS books;
CREATE TABLE books (
  id BIGINT NOT NULL PRIMARY KEY auto_increment,
  author_id INT NOT NULL,
  name VARCHAR(255) NOT NULL
);

INSERT INTO authors VALUES ( 0, 'Jack Herrington' );
INSERT INTO authors VALUES ( 0, 'Dave Smith' );
INSERT INTO authors VALUES ( 0, 'Harold Rollins' );

INSERT INTO books VALUES ( 0, 1, 'Code Generation In Action' );
INSERT INTO books VALUES ( 0, 1, 'Podcasting Hacks' );
INSERT INTO books VALUES ( 0, 2, 'Welcome To The Universe' );

要使这段代码进入数据库,首先需要创建一个数据库:

% mysqladmin create books

然后,将一些表和示例数据添加到数据库:

% mysql books < books.sql

当然,如果您需要指定数据库用户名和密码,那么您的命令可能略有不同。


创建一个数据库包装器

要访问数据库,首先要使用 PHP 创建一个 DatabaseWrapper 类。这个类在构造时获取一个数据库连接句柄和一个表名并提供 5 个方法:

  • get— 如果被给予一个整数 ID,get 方法将返回表中的一行。此方法假定表中的 "id" 字段有唯一整数值。
  • getAll— 此方法返回数据库中的所有记录。它接受一些选项。这里实现的唯一选项是限制,该选项限制返回的记录的计数。如果愿意,可以添加更多选项 — 例如,添加一个 "order" 选项返回按一个指定字段排序的记录。
  • deletedelete 方法接受一个整数 ID 并从表中删除该行。
  • insertinsert 方法在表中插入一个新行。
  • updateupdate 方法接受一个整数 ID,以指定要更新的行;或者接受一个字段哈希表,使用指定的值更新字段。

这 5 个函数可向任何客户机提供一个完整的 CRUD 接口。但它们受到限制,只能在一个表上工作,不能通过关系跨多个表工作。

清单 2 显示了这个数据库封装类的代码。

清单 2. DBWrapper.php
<?php
class DatabaseWrapper {
  private $dbh;
  
  public function __construct( $dbh, $table ) {
    $this->dbh = $dbh;
    $this->table = $table;
  }

  public function get( $id ) {
    $sql = 'SELECT * FROM '.$this->table.' WHERE id = :id';
    $sth = $this->dbh->prepare($sql);
    $sth->execute(array(':id' => $id));
    return $sth->fetchObject();
  }

  public function getAll( $options = array() ) {
    $sql = 'SELECT * FROM '.$this->table;
    if ( isset( $options->{'limit'} ) ) {
      $sql .= ' LIMIT '.$this->dbh->quote( $options->{'limit'}, PDO::PARAM_INT );
    }
    $sth = $this->dbh->prepare($sql);
    $sth->execute();
    return $sth->fetchAll( PDO::FETCH_CLASS );
  }

  public function delete( $id ) {
    $sql = 'DELETE FROM '.$this->table.' WHERE id = :id';
    $sth = $this->dbh->prepare($sql);
    $sth->execute(array(':id' => $id));
  }

  public function update( $id, $values ) {
    $binds = array( ':id' => $id );
    $bindnames = array();

    foreach(array_keys($values) as $k) {
      $k = $this->clean( $k );
      $binds[ ":$k" ] = $values[ $k ];
      $bindnames []= "$k=:$k";
    }

    $bindnames = join( $bindnames, ',' );

    $sql = 'UPDATE '.$this->table." SET $bindnames WHERE id=:id";

    $sth = $this->dbh->prepare($sql);

    $sth->execute( $binds );
  }

  public function insert( $values ) {
    $keys = array();
    $binds = array();
    $bindnames = array();

    foreach(array_keys($values) as $k) {
      $k = $this->clean( $k );
      $keys []= $k;
      $binds[ ":$k" ] = $values[ $k ];
      $bindnames []= ":$k";
    }

    $keys = join( $keys, ',' );
    $bindnames = join( $bindnames, ',' );

    $sql = 'INSERT INTO '.$this->table." ( $keys ) VALUES ( $bindnames )";

    $sth = $this->dbh->prepare($sql);

    $sth->execute( $binds );

    return $this->dbh->lastInsertId();
  }

  private function clean( $k ) {
    return preg_replace( '[^A-Za-z0-9_]', '', $k );
  }
}
?>

getgetAll 的代码是最直观的。它使用 PHP 的 PDO 库通过连接手柄对数据库执行 SELECT 请求。delete 方法也相当简单,因为它只是执行一个 DELETE 请求。

需要注意的一个重点是在 SQL 语句中使用以冒号界定的替换操作符。这些替换操作符是安全数据库访问的关键。替换操作符确保恶意脚本不能将 SQL 附加到查询中覆盖现有代码,访问它们不应该看到的数据。这种伎俩称为 “SQL 注入”,是非常常见的攻击方式。

insertupdate 方法要复杂一些,因为它们需要使用用于输入数据的额外字段格式化 SQL 语句。字段名称通过 clean 方法清理,clean 方法返回字段名称中的字符之外的任意字符。这种方法消除了 SQL 注入攻击的可能性。


将一个 web 前端放置到数据库存取器上

要使数据传递到浏览器,数据必须可以通过一个 web 页面访问。为此,我们创建一个名为 driver.php 的页面。清单 3 显示了这个 web 页面的代码。

清单 3. driver.php
<?php
include 'dbwrapper.php';

header( 'Content-Type', 'application/json' );

try {
  $table = null;
  
  if ( $_GET['table'] == 'books' || $_GET['table'] == 'authors' )
    $table = $_GET['table'];
  else
    throw new Exception( 'Invalid table name' );

  $dw = new DatabaseWrapper(
    new PDO('mysql:host=localhost;dbname=books', 'root', ''),
    $table
  );

  $additional = array();
  foreach( array_keys( $_GET ) as $k ) {
    if ( $k != 'table' && $k != 'method' && $k != 'id' ) {
      $additional[$k] = $_GET[$k];
    }
  }

  switch( $_GET['method'] ) {
    case 'get':
      print json_encode( $dw->get( $_GET['id'] ) );
      break;
    case 'getAll':
      print json_encode( $dw->getAll( $additional ) );
      break;
    case 'insert':
      print json_encode( $dw->insert( $additional ) );
      break;
    case 'update':
      $dw->update( $newid, $additional );
      print json_encode( true );
      break;
    case 'delete':
      $dw->delete( $_GET['id'] );
      print json_encode( true );
      break;
    default:
      throw new Exception( 'Unknown method' );
      break;
  }

} catch ( Exception $e ) {
  print json_encode( array( 'error' => $e->getMessage() ) );
}
?>

这个简单的脚本从 web 请求获取 GET 参数并将它们发送到一个 DatabaseWrapper 实例。如果成功,它将结果格式化为 JSON。如果处理过程中出现错误,将抛出一个异常,这个异常也被格式化为 JSON。

为确保这个脚本不能用于访问数据库中的任何表,代码首先检查被请求的表名。在本例中,只有 "authors""books" 可以访问。如果您将这段代码用于您自己的架构,需要更改代码的相应部分。

代码然后检查这个方法参数,确定调用 5 个动作(get、getAll、delete、insert 和 update)中的哪一个。对于插入,update 和 getAll 还在 GET 数组中发送一组额外的键。

要测试代码,打开一个 web 浏览器并使用各种 URL 参数访问 driver.php 页。图 2 在 URL 指定使用 get 方法访问单个记录时显示。

图 2. 对单个记录的 JSON 请求
对单个作者姓名 Jack Herrington 的请求的屏幕截图

图 2 显示被请求的作者行的 JSON 编码的记录。对于作为一个 JavaScript 哈希表编码的记录,JSON 对象包含 "id" 字段和 "name" 字段。

这个请求上的参数是:

driver.php?table=authors&method=get&id=1

要获取所有记录,只需更改这个方法,删除 "id" 属性:

driver.php?table=authors&method=getAll

图 3 显示了这个请求的 JSON 结果:

图 3. 请求所有记录的 JSON 请求
显示多位作者姓名 Jack Herrington、Dave Smith、Harold Rollins、Geraldo Rivera 的屏幕截图

图 3 中有一个返回的 JavaScript 记录的数组,对于数据库中的每一行,数组都包含一个带有 "name""id" 字段的对象。

毫无疑问,服务器代码的基础对于构建一个 web 接口已经足够。我们下面就开始构建前端。


创建 JavaScript 访问类

要从 web 浏览器访问数据库,需要一个通过 Ajax 利用 driver.php 脚本的类。如果不使用 jQuery 或 Prototype.js 这样的 JavaScript 库,Ajax 请求可能有点麻烦。在这里,我们使用 jQuery,但只需对代码稍作更改,就可以使用您喜欢的任何库。

除 jQuery 外,我们还使用了 Class 扩展,该扩展允许轻松定义一个新的 JavaScript 类 — 本例中为 DbWrapper 类,如 清单 4 所示。

清单 4. dbwrapper.js
var DbWrapper = Class.create({
  table: '',
  get: function( id, callback ) {
    $.getJSON( 'driver.php', { table: this.table, method: 'get', id: id }, callback );
  },
  getAll: function( callback, params ) {
    if ( params == null ) params = {};
    params.table = this.table;
    params.method = 'getAll';
    $.getJSON( 'driver.php', params, callback );
  },
  insertObject: function( params, callback ) {
    params.table = this.table;
    params.method = 'insert';
    $.getJSON( 'driver.php', params, callback );
  },
  updateObject: function( id, params, callback ) {
    params.table = this.table;
    params['id'] = id;
    params.method = 'insert';
    $.getJSON( 'driver.php', params, callback );
  },
  deleteObject: function( id, callback ) {
    $.getJSON( 'driver.php', { table:this.table,
        'id':id, method: 'delete' }, callback );
  }
});

这个简短的类将上述 5 个方法中的每一个封装到后端上。根据所选的方法,每个方法使用一组不同的参数调用 getJSON。另外,每个方法都接受一个处理从请求返回的数据的回调。

insert、update 和 delete 方法的名称分别被更改为 insertObjectupdateObjectdeleteObject,这是因为 delete 是 JavaScript 中的一个保留字。

清单 5 显示了利用这个新 JavaScript 类的页面。

清单 5. Index.html
<html>
<head><title>Database access test</title>
<script src="jquery-1.5.min.js"></script>
<script src="Class-0.0.2.min.js"></script>
<script src="dbwrapper.js"></script>
<script>
var dbw = null;

function updateAuthorsTable() {
  dbw.getAll( function( data ) {
    $('#authors').html('<table id="authors"><tr><td>ID</td>
                        <td>Author</td></tr></table>');
    $(data).each( function( ind, author ) {
      $('#authors tr:last').after('<tr><td>'+author.id+'</td>
                        <td>'+author.name+'</td></tr>');
    } );
  } );
}

$(document).ready(function() {
  dbw = new DbWrapper();
  dbw.table = 'authors';

  updateAuthorsTable();
  
  $('#addbutton').click( function() {
    dbw.insertObject( { name: $('#authorname').val() },
      function( data ) {
        updateAuthorsTable();
      } );
  } );
} );
</script>
</head>
<body>
  <table id="authors">
  </table>
  <h3>Add A New Author</h3>
  Name: <input type="text" id="authorname">
  <input type="submit" value="Add" id="addbutton">
</body>
</html>

这个页面所做的第一件事就是包含 jQuery、Class 扩展以及您的新 DbWrapper.js 文件。然后,当页面载入 ready 函数时,它创建一个 DbWrapper。然后,这个 DbWrapper 实例被 updateAuthorsTable 函数使用,该函数使用这个包装器从数据库请求所有作者。然后,updateAuthorsTable 函数向页面上的一个表添加一些行,显示数据库表的内容。

页面底部有一个作者姓名表单,以及一个 Add 按钮。这个 Add 按钮的脚本调用 DbWrapper 类上的 insertObject 方法,向数据库添加一个新行。那个操作成功后,该脚本调用 updateAuthorsTable 函数,使用这个新行更新显示。

如果一切都设置正确,首次打开这个页面时将看到如 图 4 所示的图像。

图 4. 动态 HTML 列表和添加页面
屏幕截图显示一列作者(Jack Herrington、Dave Smith、Harold Rollins)和 Add A New Author 字段

图 4 显示页面已经连接到服务器端的 driver.php 脚本,请求数据,然后格式化数据以便显示。如果一切正常,这个显示应该显示一列来自数据库的作者、一个输入框、以及一个用于创建新作者记录的 Add 按钮。

下一步是检查添加行的功能是否有效。在本例中,在作者姓名字段中输入 Geraldo Rivera(如 图 5 所示),然后单击 Add

图 5. 添加一条新记录
屏幕截图显示 Add A New Author 字段中的新作者姓名(Geraldo Rivera)

Add 按钮的代码调用 DbWrapper 类上的 insertObject 方法,该方法然后调用服务器上的 insert 方法。如果这个步骤成功,则页面上的 JavaScript 使用页面载入过程中首次调用的函数更新作者表。图 6 显示了更新结果。

图 6. 使用新记录更新的页面
屏幕截图显示新作者(Geraldo Rivera)添加到作者列表(Jack Herrington、Dave Smith、Harold Rollins)

图 6 显示应用程序已将作者 “Geraldo Rivera” 添加到作者列表,该作者出现在显示中。图 6 说明,您已对后端数据库拥有读写访问权。要访问一个包含更多表的不同架构,只需将对新表的支持添加到 driver.php 脚本。


结束语

我在本文开头提到过,这个代码最好用于快速原型构建而非生产,原因有以下两点:

  • 访问非常开放,不受用户、角色和权限限制。
  • 如果您的架构中的表间存在一些关系(这种可能性很大),那么这个代码不是特别有效。

另外,这个示例还存在先天不足:不够 RESTful。在 REST 世界中,应用程序不应该以改变应用程序状态(插入、更新和删除请求时发生)的方式响应 GET 请求。但是,创建一个 PHP 服务来处理 POST、DELE 和 REST 需要的所有 URL 重写工作要困难得多。我在这里使用的简单页面仅仅是用于演示目的。

但是,抛开这些警告,这个示例实际上是一个很不错的例子,展示了如何联合使用 PHP、PDO 库和 jQuery 创建一个可移植的 JavaScript 数据访问层。如果您试用这个示例,一定能从中获得收益,尽管它不适用生产环境。

参考资料

学习

  • jQuery 网站:获取快速 web 开发所需的资源,下载 jQuery 库并找到您需要的所有文档。
  • jQuery Class 扩展:找到这个方便的扩展,它用于 Google 代码站点上托管的 jQuery 的类创建和管理。
  • PHP 网站:访问这个 PHP 最佳参考资料,PHP 是一个应用广泛、目的宽泛的脚本语言,尤其适合 web 开发。
  • W3C:访问这个卓越的标准站点;特别是,XML 标准 与本文相关。
  • PDO 库:找到关于在 PHP 中访问数据库的所有这些用例的优秀文档等资源。
  • 本文作者撰写的更多文章(Jack Herrington,developerWorks,2005 年 3 月至今):阅读关于 Ajax、JSON、PHP、XML 和其他技术的文章。
  • developerWorks XML 专区:在 XML 专区获取提高您的专业技能所需的资源。
  • 访问 developerWorks Open source 专区,获得丰富的 how-to 信息、工具和项目更新,帮助您用开放源码技术进行开发,并与 IBM 产品结合使用,以及我们 最受欢迎的文章和教程
  • IBM XML 认证:了解如何才能成为一名 IBM 认证的 XML 和相关技术的开发人员。
  • XML 技术库:访问 developerWorks XML 专区,获得广泛的技术文章和技巧、教程、标准和 IBM 红皮书,并阅读关于 XML 技巧 的更多信息。
  • developerWorks 技术活动网络广播:随时关注这些活动中的技术。
  • developerWorks 播客:收听面向软件开发人员的有趣访谈和讨论。
  • developerWorks 按需演示:观看面向初学者的产品安装和设置演示,以及为经验 丰富的开发人员提供的高级功能。

获得产品和技术

讨论

条评论

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=XML, Open source
ArticleID=656294
ArticleTitle=构建一个简易 JavaScript 数据访问层
publish-date=05032011