常常有人问 Oracle Berkeley DB “我如何在 Berkeley DB 中进行 ?"因此,我们此处将介绍如何在 Oracle Berkeley DB 中实施众多您喜爱的 SQL 功能。不是所有的 SQL 应用程序都应该在 Oracle Berkeley DB 实施( Oracle Berkeley DB 是一个开放源的嵌入式数据库引擎,提供了快速、可靠、本地的持久性,无需管理),但如果您有一系列相对固定的查询且很关心性能,Berkeley DB 将是很好的选择。
让我们从头开始 (很好的起步点)。我们从 ABC 开始学阅读,在 Berkeley DB 中我们从术语开始。
这里为坚定的 SQL 程序员提供了一个小型“翻译指南”:
SQL 术语 | Oracle Berkeley DB 对应词汇 |
数据库 | 环境 |
表 | 数据库 |
字节组/行 | 键/数据对 |
主索引 | 键 |
次索引 | 次数据库 |
选择一个应用程序域 — 传统员工数据库,经过一定的简化。我们进一步假定您需要所有 Berkeley DB 的全部额外特性:并发、事务、可恢复性等。
创建数据库
在 SQL 中,您可以执行以下命令
CREATE DATABASE personnel
在 Berkeley DB 中, 您想要创建一个放置所有应用程序数据的环境。在代码中,您将通过一个环境句柄来引用环境,该句柄类型为 DB_ENV。您将使用这一句柄来操作此环境。现在,将一些精妙的错误处理过程放在一边,来集中讨论 API。
DB_ENV *dbenv; int ret; /* Create the handle. */ DB_ASSERT(db_env_create(&dbenv, 0) == 0); /* * If you wanted to configure the environment, you would do that here. * Configuraition might include things like setting a cache size, * specifying error handling functions, specifying (different) * directories in which to place your log and/or data files, setting * parameters to describe how many locks you'd need, etc. */ /* Now, open the handle. */ DB_ASSERT(dbenv->open(dbenv, "my_databases/personnel", DB_CREATE | DB_INIT_LOCK | DB_INIT_MPOOL | DB_INIT_TXN | DB_THREAD, 0644); |
您现在创建和打开了一个环境。需要注意几项事情:
· 开始前必须有 my_databases/personnel 目录。
· open 调用的最后一个参数就是作为此环境的一部分为您创建的文件的模式。
· 此处指定的标记将允许您创建环境
(DB_CREATE),使用锁定 (DB_INIT_LOCK);有一个共享的内存缓存池 (DB_INIT_MPOOL);使用事务 (DB_INIT_TXN);并同时在不同的控制线程中使用得到的环境句柄 (DB_THREAD)。
在 SQL 中,查询通常由单独的服务器处理,该服务器由数据库管理员配置以在您的系统上正常(或不正常)工作。因为 Berkeley DB 嵌入到了您的应用程序中,因此该应用程序可以执行许多配置工作。但这确实与数据库调优有关,我们将另文详述。
现在创建了数据库,接下来创建一些表。在 Berkeley DB 中, 表由类型为 DB * 的句柄引用 。对于应用程序中的每个表,通常会打开一个句柄,然后在一或多个线程中使用该句柄。
因此,在 SQL 中可能是
CREATE TABLE employee (primary key empid int(8), last_name varchar(20), first_name varchar(15), salary numeric(10, 2) salary, street varchar (20), city varchar (15), state char(2), zip int(5)) |
在我们讨论实施这一过程的 Berkeley DB 代码前,要记住在 SQL 中,数据库负责实施和解释数据模式,这很重要。在 Berkeley DB 中,这一解释由应用程序完成。在分析数据操作语言 (DML) 时这将变得更加有趣,但现在其很明显,因为在创建员工表时,Berkeley DB 只知道主键,不知道数据库中的不同域。
首先,您需要创建一个数据库句柄来代表创建的的表。(我们再次略过错误处理。)
DB *dbp; DB_ENV *dbenv; /* Let's assume we've used the code from above to set dbenv. */ ASSERT(db_create(&dbp, dbenv, 0) == 0); /* * Like with the environment, tables can also be configured. You * can specify things like comparison functions, page-size, etc. * That would all go here. */ /* Now, we'll actually open/create the primary table. */ ASSERT(dbp->open(dbp, NULL, "employee.db", NULL, DB_BTREE, DB_AUTO_COMMIT | DB_CREATE | DB_THREAD, 0644) == 0). |
这一调用将使用 B-树作为主索引结构创建表。该表将在 my_databases/personnel 目录中物化,使用名称 employee.db。文件将包含一个表并具有由最后的参数 (0644) 指定的文件系统权限。指定的标记在事务中创建表,允许以后的事务操作
(DB_AUTO_COMMIT);允许表不存在时创建表 (DB_CREATE);并指定可由多个控制线程同时使用得到的句柄 (DB_THREAD)。
注意,您尚未指定具体由什么组成主键 (索引) 或数据字段好似存储于该表中。这都将是应用程序的任务,稍后接触到插入、选择和更新部分的内容时这将变得更清楚。
现在,我们来看看如果在 employee id 上有一个主索引同时对姓使用一个次索引时将会出现什么情况。
您应该使用上述 SQL 查询并执行。
CREATE INDEX lname ON employee (last_name)
在 Berkeley DB 中,次索引就像是表。然后您可以联合表,使一个表成为另一个表的次索引。为实施这一功能,需要更深入地了解应用程序要使用的数据表示。
假设应用程序要使用 C 结构以在我们的员工表中包含字节组。您可以按下面所示定义结构:
typedef struct _emp_data { char lname[20]; char fname[15]; float salary; char street[20]; char city[15]; char state[2]; int zip; } emp_data; |
假设员工 ID 为简单的整数:
typedef int emp_key;
在 Berkeley DB 中,操作键或数据项时,您使用 DBT 结构。DBT 包含了不透明的字节串,以指针和长度来表示。指针由 DBT 的数据字段引用,长度存储于 DBT 的大小字段中。如果希望操纵表示一个员工的键/数据对,您需要将一个 DBT 用于 emp_key,另一个用于 emp_data。
DBT key_dbt, data_dbt; emp_key ekey; emp_data edata; memset(&key_dbt, 0, sizeof(key_dbt)); memset(&data_dbt, 0, sizeof(data_dbt)); /* * Now make the key and data DBT's reference the key and data * variables. */ key_dbt.data = &ekey; key_dbt.size = sizeof(ekey); data_dbt.data = &edata; data_dbt.size = sizeof(edata); |
这里我们可以了解到 SQL 中的字节组由键/数据对表示,应用程序负责了解如何解释这些对。
有了这些作为背景后,我们返回到次索引的讨论。因为 Berkeley DB 不了解键/数据对中数据元素的模式和结构,所以需要应用程序的帮助来找到我们用作次索引的字段。应用程序以回调函数方式提供帮助。回调函数接受键/数据对输入并返回引用了用作次键的值的 DBT。
所以要创建 last_name 的次索引,必须编写一个回调函数,接受键/数据对输入并返回引用了数据项的 last_name 字段的 DBT。
int lname_callback(DB *dbp, const DBT *key, const DBT *data, DBT *skey) { emp_data *edata; /* * We know that the opaque byte-string represented by the data DBT * represents one of our emp_data structures, so let's cast it * to one of those so that we can manipulate it. */ edata = data->data; skey->data = edata->lname; skey->size = strlen((edata->lname); return (0); } |
写完回调函数后,可以指定次索引。次索引只是一个表,所以先创建一个表:
DB *sdbp; ASSERT(db_create(&sdbp, dbenv, 0) == 0); /* Configure sdbp. */ ASSERT(sdbp->open(sdbp, NULL, "emp_lname.db", NULL, DB_BTREE, DB_AUTO_COMMIT | DB_CREATE | DB_THREAD, 0644) == 0); |
再次使用 B 树结构对姓进行索引,保留以前使用的所有标记和模式。
最后,您必须将次索引表与主表(员工表)相关联。请记住,dbp 是员工表的句柄, sdbp 是次索引表的句柄。
ASSERT(dbp->associate(dbp, NULL, sdbp, lname_callback, flags) == 0);
注意事项:
· 可以了创建任意多的次索引。重要的问题是次索引降低了插入的速度 (因为您必须为每个次索引创建索引项),但如果使用次键值(如,姓)来查询和选择字节组, 它们将大大改进查询的性能。
· 在任何时候更新主表时,只要打开并关联了次索引,次索引将始终保持最新。但如果忘记打开并关联次索引,然后更改基表,您会发现次索引不是最新的。
应当避免这一情况。
DDL 中的最后两个操作是丢弃命令:丢弃索引、表和数据库。
如同在 SQL 中丢弃索引和删除表一样,您也能在 Berkeley DB 中完成这些操作。在 SQL 中,您可以执行以下命令
DROP TABLE employee
或者
DROP INDEX lname
在 SQL 中丢弃一个表将丢弃与之关联的所有索引,在 Berkeley DB 中,您必须显式完成此任务。幸运的是,在 Berkeley DB 中丢弃表或索引是同样的操作。
移除表前,必须关闭表上的所有数据库句柄。关闭表容易;假设我们要丢弃员工数据库的次索引。先关闭次索引:
sdbp->close(sdbp, 0)
在发出数据库句柄的关闭命令后,不能再使用句柄。
关闭次索引表后,您可以使用 dbenv 句柄的 dbremove 方法将其移除:
DB_ENV *dbenv; ASSERT(dbenv->dbremove(dbenv, NULL, "emp_lname.db", NULL, DB_AUTO_COMMIT) == 0); |
可以使用同一调用顺序 (closing 和 dbremoving) 来丢弃表。
假设不想丢弃表,只想更改其名称。您也可完成这一操作。
与移除一样,首先要关闭表句柄:
dbp->close(dbp, 0);
现在您可以更改表名称:
DB_ENV *dbenv; ASSERT(dbenv->dbrename(dbenv, NULL, "employee.db", NULL, "newemp.db", DB_AUTO_COMMIT) == 0); |
最后,您可能想销毁数据库。在 SQL 中执行
DROP DATABASE personnel
这一命令在 Berkeley DB 中也有对应的命令。
首先,必须关闭环境:
ASSERT(dbenv->close(dbenv, 0) == 0);
与关闭表句柄一样,当关闭环境句柄后,将不能使用该句柄。所以,为了丢弃表,您需要创建一个新句柄,然后使用该句柄移除数据库(环境)。
ASSERT(db_env_create(&dbenv, 0) == 0); ASSERT(dbenv->remove(dbenv, "my_databases/personnel", 0) == 0); |
至此,我们完成了 SQL 的 DDL 到 Berkeley DB 的转换。接下来,我们将讨论如何完成 SQL DML 到 Berkeley DB 的转换。
在 Berkeley DB 中执行 SQL DML 操作
我们已经介绍了 SQL 的 DDL 和其在 Berkeley DB 中的实现,现在要将数据添加到数据库,讨论 SQL 的插入、更新和删除。
在 SQL 中使用插入语句将数据插入表:
INSERT INTO employees VALUES (00010002, "mouse", "mickey", 1000000.00, "Main Street", "Disney Land", "CA", 98765); |
SQL 插入都变成了数据库或游标句柄的 Berkeley DB“put”方法;我们先讨论数据库,然后再探讨游标。
假设您已经打开了一个表,有一个数据库句柄 dbp 引用了员工表。现在,雇佣 Mickey Mouse。
DB *dbp; DBT key_dbt, data_dbt; emp_data edata; emp_key ekey; /* Put the value into the employee key. */ ekey = 00010002; /* Initialize an emp_data structure. */ strcpy(edata.lname, "Mouse"); strcpy(edata.fname, "Mickey"); edata.salary = 1000000.00; strcpy(edata.street, "Main Street"); strcpy(edata.city, "Disney Land"); strcpy(edata.state, "CA"); edata.zip = 98765; /* Initialize DBTs */ memset(&key_dbt, 0, sizeof(key_dbt)); memset(&data_dbt, 0, sizeof(data_dbt)); /* Now, assign key and data values to DBTs. */ key->data = &ekey; key->size = sizeof(ekey); data->data = &edata; data->size = sizeof(edata); /* Finally, put the data into the database. */ ASSERT(dbp->put(dbp, NULL, &key_dbt, &data_dbt, DB_AUTO_COMMIT) == 0); |
请注意,如果已经有将任何次索引与员工表相关联(如在 SQL 中),则在插入时将自动对其进行更新。
现在,假设表中有些数据,您希望对这些数据进行更改。例如,要给 Mickey 涨工资!有多种完成方法。
第一个方法与上面的插入代码一样 — 如果您在一个表上使用 PUT 方法并且该键已存在 (且表不允许单键有相同的数值), PUT 将使用新版本代替旧版本。因此,以下步骤将使用新记录替换 Mickey 的记录,薪水将为 $2,000,000,而不是 $1,000,000。
/* Put the value into the employee key. */ ekey = 00010002; /* Initialize an emp_data structure. */ strcpy(edata.lname, "Mouse"); strcpy(edata.fname, "Mickey"); edata.salary = 2000000.00; strcpy(edata.street, "Main Street"); strcpy(edata.city, "Disney Land"); strcpy(edata.state, "CA"); edata.zip = 98765; /* Initialize DBTs */ memset(&key_dbt, 0, sizeof(key_dbt)); memset(&data_dbt, 0, sizeof(data_dbt)); /* Now, assign key and data values to DBTs. */ key->data = &ekey; key->size = sizeof(ekey); data->data = &edata; data->size = sizeof(edata); /* Finally, put the data into the database. */ ASSERT(dbp->put(dbp, NULL, &key_dbt, &data_dbt, DB_AUTO_COMMIT) == 0); |
请注意,该方法较麻烦,为完成它,您需要了解数据库中所有其他域的值。因此,不同于
UPDATE employees SET salary = 2000000 WHERE empid = 000100002
其中,您只需知晓 employee ID,而现在需要知晓所有信息。难道在 Berkeley DB 没有可用方法吗?答是是有。如果确切地知道要替换的数据项字节,您可以使用与等同于更新命令的方法。
要使用此方法,您需要引入游标概念。游标表示表中的某个位置。它让您遍历表并保留当前项的情况,然后再对其操作。
在 Berkeley DB 中创建游标很简单 — 它是数据库句柄的方法:
DBC *dbc; DB *dbp; ASSERT(dbp->cursor(dbp, NULL, 0) == 0); |
有游标后,我们需要将其定位于 Mickey 的记录,以便能对其进行更新。这等同于 SQL 语句的 WHERE 部分。
DBT key_dbt, data_dbt; emp_data *edata; emp_key ekey; /* We'd like to look up Mickey's key. */ emp_key = 0010002; memset(&key_dbt, 0, sizeof(key_dbt)); key_dbt.data = &emp_key; key_dbt.size = sizeof(emp_key); /* * We want the data returned, so we don't need to initialize the * employee data data structure. */ memset(&data_dbt, 0, sizeof(data_dbt)); /* Now, set the cursor to the record with the key emp_key. */ dbc->c_get(dbc, &key_dbt, &data_dbt, DB_SET); |
接下来我们就可以更改薪水了 (处理子句的“SET salary=2000000”部分)
/* Change the salary. */ edata = data_dbt->data; edata.salary = 2000000; |
最后,应用 SQL 语句的 UPDATE 部分:
dbc->c_put(dbc, &key_dbt, &data_dbt, DB_CURRENT);
在本例中,您事先不知道 Mickey 的记录内容,因此需要检索然后再更新。
或者,甚至无需检索记录。DBT 上的 DB_DBT_PARTIAL 标记值指示您在获取/插入记录的一部分,所以 Berkeley DB 可以忽略除该部分外的所有内容。
再试一次:
emp_data edata; float salary; /* We'd like to look up Mickey's key. */ emp_key = 0010002; memset(&key_dbt, 0, sizeof(key_dbt)); key_dbt.data = &emp_key; key_dbt.size = sizeof(emp_key); |
不检索整个记录,不检索任何东西 — 即执行 PARTIAL 获取,指定您只需要 0 字节的数据项。
/* We don't want the data, we just want to position the cursor. */ memset(&data_dbt, 0, sizeof(data_dbt)); data_dbt->flags = DB_DBT_PARTIAL; data_dbt->dlen = 0; /* Position the cursor on Mickey's record */ dbc->c_get(dbc, &key_dbt, &data_dbt, DB_SET); /* * Now, prepare for a partial put. Note that the DBT has already * been initialized for partial operations. We need to specify * where in the data item we wish to place the new bytes and * how many bytes we'd like to replace. */ salary = 2000000.00; /* The DBT contains just the salary information. */ data_dbt->data = &salary; data_dbt->size = sizeof(salary); /* * dlen and doff tell Berkeley DB where to place this information * in the record. dlen indicates how many bytes we are replacing -- * in this case we're replacing the length of the salary field in * the structure (sizeof(emp_data.salary)). doff indicates where * in the data record we will place these new bytes -- we need to * compute the offset of the salary field. */ data_dbt->dlen = sizeof(emp_data.salary); data_dbt->doff = ((char *)&edata.salary - (char *)&edata); /* Now, put the record back with the new data. */ dbc->c_put(dbc, &key_dbt, &data_dbt, DB_CURRENT); |
数据检索
了解如何向表插入数据后,现在学习如何检索它。让我们从最简单的方法开始:根据其主键查找值。
SELECT * FROM employees WHERE id=0010002
您已经知道如何使用游标来完成此任务:
DBT key_dbt, data_dbt; emp_data *edata; emp_key ekey; /* We'd like to look up Mickey's key. */ emp_key = 0010002; memset(&key_dbt, 0, sizeof(key_dbt)); key_dbt.data = &emp_key; key_dbt.size = sizeof(emp_key); /* * We want the data returned, so we don't need to initialize the * employee data data structure. */ memset(&data_dbt, 0, sizeof(data_dbt)); /* Now, set the cursor to the record with the key emp_key. */ dbc->c_get(dbc, &key_dbt, &data_dbt, DB_SET); |
使用上面的游标操作,因为我们稍后要更新该记录。假如只想检索记录,则甚至不需要游标。所需要的是 dbp 句柄的 get 方法:
DBT key_dbt, data_dbt; emp_data *edata; emp_key ekey; /* We'd like to look up Mickey's key. */ emp_key = 0010002; memset(&key_dbt, 0, sizeof(key_dbt)); key_dbt.data = &emp_key; key_dbt.size = sizeof(emp_key); /* * We want the data returned, so we don't need to initialize the * employee data data structure. */ memset(&data_dbt, 0, sizeof(data_dbt)); /* Now, use the dbp method. */ dbp->get(dbp, NULL, &key_dbt, &data_dbt, 0); |
因此,这与上面的 SELECT 表达式一样。
到目前为止,我们都是根据主键来查找记录。如果不知道主键,该怎么办?下面提供了几种方法:
使用次键值查找记录。
遍历共享同一键的项目。
遍历整个数据库。
下面详述以上方法。
使用次键
与在 SQL 中一样,根据次键检索和使用主键非常类似。
事实上,SQL 查询看起来相同,除了 where 子句:
SELECT * FROM employees WHERE last_name = "Mouse"
Berkeley DB 调用与其对等的主键调用类似。
它使用的不是主键示例中的 dbp,而是使用 sdbp 根据次键来进行查找:
DBT key_dbt, data_dbt; emp_data *edata; /* We'd like to look up by Mickey's last name. */ memset(&key_dbt, 0, sizeof(key_dbt)); key_dbt.data = "Mouse"; key_dbt.size = strlen((char *)key_dbt.data); /* * We want the data returned, so we don't need to initialize the * employee data data structure. */ memset(&data_dbt, 0, sizeof(data_dbt)); /* Now, call the get method. */ sdbp->get(sdbp, NULL, &key_dbt, &data_dbt, 0); |
data_dbt 中返回的内容非常有趣。返回的是主数据库中的数据 — 即在数据 DBT 中返回了同样的东西,不管您是使用主键还是次键查找。
但是,您会发现按次键查找时,得到的结果与按主键检索或 SQL 语句得到的结果有所不同。主键丢失,因为没有位置来返回它。所以,上面的代码实际上实施
SELECT last_name, first_name, salary, street, city, state, zip FROM employees WHERE last_name="Mouse" |
如果您需要主键,该如何做?答案是使用 dbp->pget 或 dbc->pget 方法。这两个方法与 get 方法一样,只是它们专门设计用于您需要返回主键时的次索引查询。因此,在这一情形中,结果中会包括主键、次键和数据元素:
DBT key_dbt, pkey_dbt, data_dbt; emp_data *edata; /* We'd like to look up by Mickey's last name. */ memset(&key_dbt, 0, sizeof(key_dbt)); key_dbt.data = "Mouse"; key_dbt.size = strlen((char *)key_dbt.data); /* Set up the dbt into which to return the primary. */ memset(&pkey_dbt, 0, sizeof(pkey_dbt)); /* * We want the data returned, so we don't need to initialize the * employee data data structure. */ memset(&data_dbt, 0, sizeof(data_dbt)); /* Now, get the record and the primary key. */ sdbp->pget(sdbp, NULL, &key_dbt, &pkey_dbt, &data_dbt, 0); |
该代码等同于 SQL 次选择:
SELECT * FROM employees WHERE last_name="Mouse"
遍历多个记录
到现在为止,我们仅返回了一个记录。SQL 允许您返回多个记录 (换言之,姓为 Mouse 的所有员工)。如何在 Berkeley DB 中实现此目的?
让我们考虑两种情形。第一个情形,按照键来查找项目。第二个情形,搜索数据库,按照不带键的字段查找项目。
假设您希望查找所有姓 Mouse 的员工(假设有多个)。这意味着已经创建了 last_name 次索引,从而允许重复。打开数据库前,需要对它进行配置以支持重复:
sdbp->set_flags(sdbp, DB_DUP); ASSERT(sdbp->open(sdbp, NULL, "emp_lname.db", NULL, DB_BTREE, DB_AUTO_COMMIT | DB_CREATE | DB_THREAD, 0644) == 0); |
现在,按次索引检索时,您可能想使用游标来完成。开始时使用前面使用的代码,可以添加一个循环以遍历共享同一次键的项目:
DBT key_dbt, data_dbt; DBC *sdc; emp_data *edata; /* We'd like to look up by Mickey's last name. */ memset(&key_dbt, 0, sizeof(key_dbt)); key_dbt.data = "Mouse"; key_dbt.size = strlen((char *)key_dbt.data); /* * We want the data and primary key returned, so we need only * initialize the DBTs for them to be returned. */ memset(&data_dbt, 0, sizeof(data_dbt)); memset(&pkey_dbt, 0, sizeof(pkey_dbt)); /* Now, create a cursor. */ sdbp->cursor(sdbp, NULL, &sdbc, 0); /* Now loop over all items with the specified key. */ for (ret = sdbc->pget(sdbc, &key_dbt, &pkey_dbt, &data_dbt, DB_SET); ret == 0: ret = sdbc->pget(sdbc, &key_dbt, &pkey_dbt, &data_dbt, DB_NEXT_DUP) { /* Do per-record processing in here. */ } |
通过使用游标查找带指定键的第一个项目,完成游标的初始化,然后遍历数据库中带同一键的所有项目。
另一个可能的根据键进行遍历的形式为查询形式,如
SELECT * FROM employees WHERE id >= 1000000 AND id < 2000000
再次使用游标来遍历,但这一次您想创建一个起始和结束点。Berkeley DB 使得起始点的建立非常简单,结束点由应用程序来完成。
DBT key_dbt, data_dbt; DBC *dc; emp_key ekey; /* Set the starting point. */ memset(&key_dbt, 0, sizeof(key_dbt)); ekey = 1000000; key_dbt.data = &ekey; key_dbt.size = sizeof(ekey); key_dbt.flags = DB_DBT_USERMEM; key_dbt.ulen = sizeof(ekey); memset(&data_dbt, 0, sizeof(data_dbt)); /* Now, create a cursor. */ dbp->cursor(dbp, NULL, &dbc, 0); /* Now loop over items starting with the low key. */ for (ret = dbc->get(dbc, &key_dbt, &data_dbt, DB_SET_RANGE); ret == 0: ret = dbc->get(dbc, &key_dbt, &data_dbt, DB_NEXT)) { /* Check if we are still in the range. */ if (ekey >= 2000000) break; /* Do per-record processing in here. */ } |
要注意两点:1) 以 DB_SET_RANGE 标记作为循环的开始,这会将游标定位于第一个大于或等于指定键的项目; 2) 应用程序必须检查循环内范围的终点。