65.9K
CodeProject 正在变化。 阅读更多。
Home

Palm 数据库 - 如何在 Palm 手持设备上持久化数据

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.17/5 (8投票s)

2002 年 11 月 7 日

CPOL

12分钟阅读

viewsIcon

135931

downloadIcon

170

在我们的系列文章中,我们将讨论如何创建数据库,以及创建、修改和删除记录

Sample Image - PalmDB.jpg

引言

欢迎阅读我关于 Palm 开发的第三篇文章。既然我们已经掌握了基础知识,并且能够创建一个带有 GUI 组件的应用程序,那么接下来重要的事情就是能够持久化数据。Palm 应用程序的理念很简单。当一个应用程序启动时,它应该处于上次离开时的状态,从而产生多线程系统的错觉,实际上一次只有一个线程可以运行。为了实现这一点,以及为了编写任何有用的应用程序,我们需要存储数据。由于 Palm 没有外部存储介质,数据存储在内存中,称为数据库的区域。我很抱歉地报告,这在某种程度上是一个误称,它并不意味着任何 SQL 或关系表之类的功能。取而代之的是,你得到的是一个与你的创建者 ID 和名称关联的内存块,你可以通过其数组样式索引或一个在该设备上下文中保证唯一的 ID 来选择数据,而这个 ID 与同一个应用程序在不同设备上运行时返回的 ID 没有任何关系。

创建数据库

第一步当然是创建一个数据库。等于数据库句柄的变量是 DmOpenRef。你应该在主头文件中为每个需要保持打开状态的数据库声明一个这样的全局变量。创建数据库的调用如下所示。
Err err = 0;
err = DmCreateDatabase(0, "BugSquBugsDYTC", 'DYTC', 'DATA', false);
DYTC 是我唯一的创建者 ID,它代表 Dytech,也就是我雇主的名字。您应该事先在 www.PalmOS.com 网站上注册您自己的 ID,并在第三个参数中替换它。第一个参数代表要创建数据库的卡号,几乎总是 0。第二个参数是数据库的名称,您会注意到我在名称中使用了我的创建者 ID,Palm 推荐这样做以确保您的数据库名称是唯一的。第四个参数是数据库的类型,可以是任何四字符常量。您可能没有注意到,但实际上创建者 ID 和类型都是 32 位数字,由我们提供的四个 8 位字符值生成。理论上,一个应用程序可以为同一个名称创建多个数据库,但类型不同。然而,根据我的经验,我发现除非类型是 DATA,否则您的数据库将作为一个独立于应用程序的文件出现在 Palm 中,我认为这是不希望的。最后一个参数是一个布尔值,指定它是否为资源数据库。出于我们的目的,我们将只创建记录数据库,因此该参数将始终为 false。

打开数据库

此时,我们仍然没有打开数据库,也没有使用我们的 DmOpenRef。现在我们将纠正这两个异常。有两种方法可以打开数据库。第一种如下
DmOpenRef myRef;
DmOpenDatabaseByTypeCreator('DATA' , 'DYTC', dmModeReadWrite);
参数是数据库类型、创建者 ID 以及打开数据库的模式。这似乎是官方 Palm 文档和第三方书籍中几乎普遍偏好的方式。为什么会这样对我来说是个谜,因为它不使用我们精心制作的、对于特定设备来说是唯一的名称,而是要求我们为每个要创建的数据库注册一个创建者 ID,或者创建具有我们自己发明的任意类型的数据库,结果(至少根据我的经验)是拥有我们的用户可以独立于应用程序删除的数据库。我更喜欢的方法稍微冗长一些,但我认为它仍然更优越。看起来大致是这样
Err err = 0;
LocalID dbID = DmFindDatabase(0,"BugSquBugsDYTC");

gDB = NULL;

if (dbID > 0) gDB = DmOpenDatabase(0, dbID, dmModeReadWrite);
if (!gDB) 
{
	err = DmCreateDatabase(0, "BugSquBugsDYTC", 'DYTC', 'DATA', false);

  	if (err == 0)
	{
		gDB = DmOpenDatabase(0, DmFindDatabase(0,"BugSquBugsDYTC"), dmModeReadWrite);
我们调用的第一个 API 是 DmFindDatabase,它接受卡号和数据库名称,并返回一个 LocalID,我推测它是一个 32 位数字,因为我第一次尝试使用 UInt16 时不起作用。这个 ID 返回后可以传递给 DmOpenDatabase,它再次接受卡号、我们的 LocalID 和所需的打开模式。如果失败,返回值将是 null,因此我们继续创建数据库,既在无法找到的情况下,也无法打开。假设数据库已成功创建,然后我们打开它。请注意,我们需要再次调用 find database,因为第一次返回的值是无效的,否则我们就不会到达这一点。

创建记录

因此,此时我们 Palm 上有一个数据库,以及它的一个句柄。很明显,下一步是创建一些数据记录到数据库中,特别是我们新创建的数据库很可能包含一些默认值。示例应用程序名为 Bug Squasher。它是一个学习练习,理论上是错误跟踪系统的一部分,其中桌面系统更新一个可以报告错误的应用程序列表,而 Palm 创建可以上传到主系统的错误报告。两个数据库中较简单的那个只存储名称列表,这些名称对应于我们可以报告错误的项目的名称。两个默认值是“misc”(用于 Palm 之外的系统错误)和“BugSqu”(以便我们可以报告错误跟踪系统本身的错误)。我们将需要的用于创建记录并用数据填充的函数是 DmNewRecord,它接受三个参数(操作的数据库、要创建的记录的索引以及记录的长度),以及 DmStrCopy,它接受一个 Char*(从我们的记录句柄派生,如下所述)、一个起始位置和另一个 Char*,我们将复制该 Char*。请注意,我将 Char 大写,这是 PalmOS 创建其自有类型的一种常见方式,Char 是 Palm 使用的类型,而不是 char。它们是相同的东西无关紧要,Palm 创建自己的类型是为了确保所有类型在不同平台上大小相同。我们还将使用的其他函数是 MemHandleLock,它返回一个 void*,我们将其应用于我们的新数据库记录,并将结果强制转换为 Char*。最后,MemPtrNew 是 Palm 中 malloc 的等价物,用于创建我们的字符串,StrCopy 是 strcpy 的等价物,用于将值放入我们的字符串,MemPtrFree 用于释放我们创建的字符串。代码如下
UInt16 index = dmMaxRecordIndex;

MemHandle h = DmNewRecord(gDBProj, &index, 5);

if (!h)
    err = DmGetLastErr();
else 
{
    Char * s = (Char *) MemHandleLock(h);
    
    Char * pChar = (Char*)MemPtrNew(5 * sizeof(Char*));
    StrCopy(pChar, "Misc\0");
    DmStrCopy(s, 0, pChar);
MemPtrFree( pChar);
    MemHandleUnlock(h);				
}

index = dmMaxRecordIndex;

h = DmNewRecord(gDBProj, &index, 6);

if (!h)
    err = DmGetLastErr();
else 
{
	Char * s = (Char *) MemHandleLock(h);
    
    Char * pChar = (Char*)MemPtrNew(6 * sizeof(Char*));
    StrCopy(pChar, "BugSq\0");
    DmStrCopy(s, 0, pChar);
MemPtrFree( pChar);
    MemHandleUnlock(h);				
}
dmMaxRecordIndex 是一个 Palm 定义的常量,它会导致我们的记录在数据库末尾创建。注意,我们需要设置两次,因为当我们将其传递给 DmNewRecord 时,它将被设置为创建记录的实际索引。这里没有显示,但应调用 DmReleaseRecord 来在不再需要记录时释放它。

好吧,这很容易,不是吗?但因为数据库只是一个内存区域,除了锁定记录之外,没有提供实际的功能来访问记录中的项,那么当我们想存储更复杂的数据时该怎么办?答案是创建一个定义我们数据类型的结构。例如,如果我们有一个结构如下

typedef struct {
  DateType DateEntered;
  UInt8 nBugSeverity;
  UInt8 nBugType; 
} BugRecord;
那么我们可以简单地使用 sizeof 来获取结构的大小,并使我们的新记录具有该大小,然后使用 DmWrite API 写入它们,该 API 的原型如下 - Err DmWrite (void *recordP, UInt32 offset, const void *srcP, UInt32 bytes),其中 void* 是指向我们记录的指针,offset 指示要开始写入记录的距离,srcP 指向数据,UInt32 表示要写入的字节数。示例如下
Char *s;
UInt16 offset = 0;
s = (Char *) MemHandleLock(DBEntry);
DmWrite(s, 0, br, sizeof(BugRecord));
MemHandleUnlock(DBEntry);
我相信你们很多人都注意到了一个问题。这不是完整的 bug 记录,它不包含字符串。如果您必须存储不涉及字符串的数据,或者字符串的大小是固定的,那么生活就很轻松。然而,正如我经常说的,Palm 上的空间很宝贵,所以如果我们的数据中有字符串,我们必须采取措施确保每条记录占用的空间尽可能小。为了做到这一点,我们需要经历一个通常称为“打包”结构到记录的过程。为了做到这一点,我们创建两个结构,一个包含 Char*,另一个只包含长度为 1 的 Char 数组。这是 Bug Squasher 演示应用程序中的实际结构。
typedef struct {
  DateType DateEntered;
  UInt8 nBugSeverity;
  UInt8 nBugType;   
  char * szBugDesc;
} BugRecord;

typedef struct
{
  DateType DateEntered;
  UInt8 nBugSeverity;
  UInt8 nBugType;   
  char szBugDesc[1];
} PackedBugRecord;
在这种情况下,我们只有一个字符串,但这个例子适用于您想要的任意数量的字符串。因为查找第 n 个字符串值的唯一方法是解包所有 n 个字符串,所以您应该始终将最重要的字符串放在字符串列表的顶部,并将所有非字符串信息放在其前面。存储可变长度记录会使我们需要另一个函数,我们将在一个有用的函数 PackBug 中使用它。API 函数名为 MemHandleResize,它接受一个记录和一个新长度,并且像大多数 Palm 函数一样返回一个 Err,如果没有任何问题,Err 为 0。这是我们的 PackBug 函数。
void PackBug(BugRecord *br, MemHandle DBEntry)
{
	UInt16 length = 0;
	Char *s;
	UInt16 offset = 0;
	length = (UInt16) (sizeof(BugRecord) + StrLen(br->szBugDesc) + 1);

	if (MemHandleResize(DBEntry, length) == errNone)
	{
		s = (Char *) MemHandleLock(DBEntry);
		DmWrite(s, 0, br, sizeof(*br));
		DmStrCopy(s, OffsetOf(PackedBugRecord, szBugDesc), br->szBugDesc);
		MemHandleUnlock(DBEntry);
	}
}
如您所见,我们通过结构的大小和字符串的长度来计算记录的长度,再加上 1 用于空终止符。显然,如果您存储多个字符串,则需要存储足够的空间来空终止所有字符串。还请注意,Palm API 提供了一个名为 OffsetOf 的函数,它允许我们查找字符串在结构中的偏移量。C 标准库包含类似的结构,称为 offsetof,Palm API 再次提供了一个易于记住的替代方案,它只是将标准函数的单词大写。

理所当然,既然我们打包了 bug,我们也需要解包它。这只是一个将正确值分配给结构内变量的问题。显然,如果我们有多个字符串,我们需要使用 StrLength 来计算每个字符串的正确起始位置以便分配它们。代码如下。

void UnpackBug(BugRecord *bug, const PackedBugRecord	*pBug)
{
	bug->DateEntered = pBug->DateEntered;	
	bug->nBugSeverity = pBug->nBugSeverity;
	bug->nBugType = pBug->nBugType;
	bug->szBugDesc = pBug->szBugDesc;
}

读取记录

在此示例中,我使用了一个尚未讨论的 GUI 组件,即列表。该列表已设置为所有者绘制,并设置了一个回调函数,该函数将接收要绘制的项目的索引。这将在以后的文章中详细介绍。我们的策略是使用传递给所有者绘制函数的索引来检索数据库中相应的记录并进行渲染。这样做的好处是无需复制整个数据库,节省了空间和处理器时间。有两种方法可以搜索记录:按索引或按唯一 ID。唯一 ID 并不像听起来那么令人兴奋,搜索仍然是顺序的,因此复杂度呈线性增长。按索引搜索要常见得多,但如果您有 ID,API 是 DmFindRecordByID,它接受一个数据库、一个唯一 ID 和一个返回项目索引的指针。DmQueryRecord 函数接受一个数据库和一个记录编号,并返回一个记录句柄,或者 NULL 表示失败。这是我们在列表回调中用于读取记录的完整代码。
Err err = 0;
MemHandle myRecord = DmQueryRecord(gDB, itemNum);

if (!myRecord)
    err = DmGetLastErr();
else 
{
    PackedBugRecord *prec = (PackedBugRecord *) MemHandleLock(myRecord);

	BugRecord * rec = (BugRecord*) MemPtrNew(sizeof(BugRecord));
	
	UnpackBug(rec, prec);
在使用 WinDrawChars 渲染数据后,我们调用 MemHandleFree 来释放我们的记录。我们不需要释放记录数据,因为它们从未从数据库中复制出来。

删除记录

删除记录很容易,只需使用 DmRemoveRecord(gDB, nIndex),其中 gDB 是您的数据库句柄,nIndex 是要删除的记录。

其他一些事项

记录有 64k 的限制,有一种方法可以解决这个问题,但我对此并不熟悉。每条记录还有一个记录属性区域,大小为一字节,包含已删除位、秘密位、脏位、忙碌位和 4 位类别。已删除的记录不一定从 Palm 中删除。DmRemoveRecord 会完全删除它们,DmDeleteRecord 会删除记录但保留头部并设置位,DmArchiveRecord 不会删除记录,但会设置位。这些差异的原因是同步,这是未来的主题,基本上涉及在 Palm 和桌面应用程序之间移动数据。已删除的记录会被移动到数据库的末尾,并且不可见,即使它们仍然存在。它们通常在下次同步时被完全删除。

数据库也有自己的头部,其中存储了名称、创建者、类型等数据,以及一些应用程序特定的数据。这里可以存储您只想为整个数据库保留一次的数据。此外,数据库可以通过提供的 DmInsertionSort 和 DmQuickSort 函数进行排序,这两个函数都使用函数指针允许程序员指定排序顺序。在保持排序的同时进行插入由 DmFindSortPosition API 提供,插入到现有记录索引会导致该索引以下的记录向下移动。

虽然我无法评论 Palm API 之外的这些函数,但它们似乎很有价值,我们有一个拥有 40,000 条记录的数据库,如果没有某种二分搜索,搜索时间将难以忍受。

结论

我希望本教程能让您掌握 Palm 数据库的基础知识,并了解一些更高级的功能,所有这些功能都已在 Palm 帮助文档中记录。接下来的主题将是列表和表格,之后我们需要研究 conduits 和同步桌面应用程序到 Palm。从表格教程开始,我们将构建一个实际的应用程序,并建立在早期教程中涵盖的技能之上。

© . All rights reserved.