Visual Prolog的外部数据库系统

（以下内容译自Category:Tutorials中的External Database System. ）

下载 本文所用工程例子的源文件：

Visual Prolog 7.1 版.

Visual Prolog 7.2 版.

本教程讨论Visual Prolog的外部数据库系统. 外部数据库（external database） 是由外部链接项集合构成的，这些链可以让我们直接访问并非Prolog程序一部分的那种数据. 外部数据库既可以保存在文件中也可以保存在内存里，它支持B+树（一种可以快速取得数据及排序的数据结构），还通过内部的串行文件访问机制支持多用户访问.


 * Visual Prolog中的外部数据库
 * B+树
 * 外部数据库编程
 * 小结

Visual Prolog中的外部数据库
Visual Prolog中使用asserta、assertz、retract及retractall的内部事实数据库，用起来很简单，适用于很多场合. 但是，它对内存的需求会很快地超出计算机的能力，外部数据库系统部分地就是设计用来解决面临的这个问题. 举例来说，我们可能需要下述一种或多种的实现：

Visual Prolog外部数据库系统支持这些不同类型的应用，并且能够满足数据库系统必须在更新操作时甚至是电源故障时不丢失数据的要求.
 * 有大量记录的股票控制系统；
 * 有大量关系但仅有少量复杂结构记录的专家系统；
 * 在数据库中存储大量文本文件的文件系统；
 * 自己的数据库产品——它或许与关系数据库系统毫不相干——其中的数据相互链接而又没什么规则；
 * 包含上面若干种可能的一个系统；

Visual Prolog外部数据库谓词可以：


 * 高效处理磁盘中的海量数据；
 * 将数据库放在文件中或内存中；
 * 进行多用户访问；
 * 提供比Visual Prolog自动回溯机制的顺序处理更好的数据处理灵活性；
 * 以二进制形式加载或保存外部数据库.

概览：外部数据库里有什么？
Visual Prolog外部数据库是由两部分组成的：存储在链中的数据项——实际上是Prolog项，还有相应的用于快速访问数据项的B+树.

外部数据库把数据项放在链中（而不是单独存放）以使相关项集中在一起. 比如，一个链可能存放的是库存部件号而另一个链中含有客户名单. 简单的数据库操作，如添加新项或替换删除旧项，是用不着B+树的. 当要对数据排序或查找数据库中指定项时，才需要使用它，详细内容后面会介绍.

命名约定
所有涉及数据库管理的标准谓词，其命名有明确规则：

例如，db_delete是删除整个数据库，chain_delete是删除整个链，而term_delete只删除单独的一个项.
 * 名称的第一部分（db_，chain_，term_，等等）指明了需要的输入,
 * 名称的第二部分（flush，btrees，delete，等等）表示动作，或是返回的内容，或是受影响的内容.



图1：一个Visual Prolog外部数据库结构示例

外部数据库的选择
在磁盘上和内存中，可以同时存在多个外部数据库. 利用这种特性，可以把外部数据库放在合适的地方以取得速度与占用内存空间的最佳均衡.

为了区分多个打开的外部数据库，需要使用chainDB对象. 打开或创建数据库时，调用chainDB类中适当的构造器，使用返回的对象进行数据库访问.

Chains（链）
外部数据库是Prolog terms（项）的集合. 项，就是整数、实数、串、符号值以及复合对象，例如：32，-194，3.1417, "Wally"， wages，book("dickens", "Wally goes to the zoo")等等.

在外部数据库内，项是存储在chains（链）中的. 一个链里可以包含任意数量的项，而一个外部数据库又可以包含任意数量的链. 每个链是用名称来区分的，而名称就是个串.

下图是链结构示意图.



图2：链的结构:

数据库关系及数据库表模型的基础是项链. 举例来说，假设有客户、供货商及一个部件数据库，要想把所有数据按三个关系放在一个数据库里. 这就可以把客户放在一个链里，叫它customers；把供货商放在一个链里，叫suppliers；再把部件放在一个名为parts的链中.

要在外部数据库中插入一个项，必须把它插入到命名了的链中. 但另一方面，要取出项则不必点名其所在的链. 插入和取出两种情况下，都必须要指明项所属的域. 在实际使用中，最好是链中所有项都是相同域的，但数据库中项的混合实际上是没有限制的. 保证取出的项域就是插入时的项域是编程人员的事.

外部数据库的域
外部数据库使用以下的域：

域及用途

bt_selector： 由bt_open返回的B+树选择符值

place： 数据库的位置，可以在内存中，也可以在文件中

accessmode： 确定如何使用文件

denymode： 确定别的用户可以怎么用文件

ref： 项在链中位置的索引号

数据库索引号
每个插入到外部数据库的项，Visual Prolog都会对其赋予一个database reference number（数据库索引号）. 可以使用项的数据库索引号进行取出、移动或替换操作，还可以对链中的前一项或后一项进行类似的操作. 可以把数据库索引号插入到一个B+树中（后面会有介绍），再用这个B+对项进行排序或对项做快速搜索.

数据库索引号与数据库放在什么地方以及对数据库做了些什么操作都没有关系. 一旦它与项关联之后，不管数据库做了什么，都可以用这个号去访问对应的项，一直到这个项被删除为止.

ref域
数据库索引号是很特殊的，可以把它们插入到事实段中，也可以用stdIO::write或stdIO::writef把它们写出来，但却不能从键盘上输入它们. 对处理数据库索引号的谓词，它们作为参数时必须声明为预定义的ref域.

用term_delete删除了一个项之后，系统会在一个新项插入到外部数据库时重用刚删除的那个项的数据库索引号，这是自动进行的. 但如果索引号已经存入事实段或一个B+树中时，保证索引号与对应项的正确关联就是编程人员的责任了.

有一个错误检查选项对此会有帮助，这个选项是用谓词db_reuserefs激活的. db_reuserefs(1)可以激活对释放项的检查，用db_reuserefs(0)可以关闭这个检查. 激活检查的开销不大（每项多四个字节，几乎不增加CPU开销），但增加的四个字节总也不会释放了. 如果经常地创建和删除项，则数据库就会持续地变大. db_reuserefs的主要目的还是帮助程序开发过程中跟踪定位错误.

操控整个外部数据库
创建新的或是打开已有的外部数据库时，可以把数据库放在文件里，也可以放在内存里，这是由调用db_create或db_openon时的参数Place的值决定的. 外部数据库使用完毕后，调用db_close关闭它. chainDB接口谓词的相关信息可以参看Visual Prolog帮助文件中的PFC章节.

当外部数据库放在内存中时，用db_close关闭数据库并不会将其从内存中删除. 如果要释放数据库占用的内存，需要明确地调用db_delete才行. 这样的外部数据库如果只是关闭了但并没有删除，稍后还可以用db_open谓词再重新打开它.

例如，下面是两个不同的db_create调用：

ChainDbObj1 = chaindb::db_create("MYFILE.DBA", in_file), /* 创建磁盘文件MYFILE.DBA */ ChainDbObj2 = chaindb::db_create("SymName2", in_memory), /* 创建内存数据库SymName2 */

而下面的这db_copy调用：

ChainDbObj:db_copy("dd.dat", in_file)

Visual Prolog会把指定的数据库ChainDbObj拷贝到放在硬盘的新数据库文件 "dd.dat".

数据库拷贝时，原来的仍在那里，这时就会有两份相同的数据库，直到明确地删除一个为止.

数据库的移动不会影响对它的任何处理，因为所有外部数据库的索引号仍是有效的. 因此，如果起先一个数据库是放在内存中的，运行时感到内存紧张了又把它拷贝到文件中，这不会影响对这个数据库的任何操作. 对内存数据库建立的索引，即使把数据库拷贝到文件中后，还是一样可用的.

db_copy有多种用途，可以：


 * 从磁盘加载数据库到内存，以后再把它保存为二进制形式，而不是用 file::save 和 file::consult 保存为文本文件.
 * 把合适规模的数据库从磁盘拷贝到内存中以加快访问速度.
 * 压缩数据库：数据库拷贝到另一个文件时，会消去所有末使用的空间.

chainDB::db_openinvalid/3
db_openinvalid可以打开已经标记为无效的数据库. 如果在数据库更新时发生了电源故障，数据库中的数据可能会因为部分缓冲区中的数据还没有写到磁盘而全部丢失. 数据库有一个标志指示更新后是否为无效有状态.

调用任何一个要改变数据库内容的谓词之后，数据库都会被标记为无效. 这些谓词有chain_inserta、chain_insertz、chain_insertafter、term_replace、term_delete、chain_delete、bt_create、key_insert及key_delete. 一旦数据库用db_close关闭，或是调用了db_flush清空缓冲区，则数据库就会被标记为有效.

用db_openinvalid，有时可以对标记为无效的数据库进行操作. 如果没有以前的备份数据可用，这或许可以恢复部分数据. 但是，对用db_openinvalid打开的数据库进行的所有操作，都有可能产生预想不到的结果.

chainDB::db_flush/0
db_flush把缓冲区的内容写到相应的数据库中并清空缓冲区. 数据库被更新时，会被标记为无效状态，直到db_flush或关闭.

对数据库要采取怎样的安全措施，当然与数据的重要程度有关. 最起码的数据安全措施是在磁盘上保留备份. 中等一些的，可以在每次重要的数据库更新之后调用db_flush. 不过清缓冲区是相当费时的一个操作，如果用得太多数据库系统就会慢慢停下来. 如果外部数据库的内容是非常重要的，还可以用一个特殊的登记文件把所有更动记录下来，或是在不同的磁盘上维护两个一样的数据库.

chainDB::db_close/0
调用db_close会关闭打开的数据库. 如果数据库是放在磁盘上的文件，则文件会关闭. 数据库关闭时不会被删除，就是放在内存的数据库也不会删除，还可以通过调用db_open重新打开它. 可以用db_delete删除在内存中的已经关闭了的数据库.

chainDB::db_delete/2
数据库放在内存中时，db_delete会释放它所占用的空间. 如果数据库是放在文件中的，db_delete会删除这个文件. 如果在指定的地方没有相应的数据库文件，则db_delete会给出一个错误消息.

chainDB::db_garbagecollect/0
db_garbagecollect扫视整个数据库垃圾回收列表的自由空间，并尽可能把若干碎片合成一个大的自由空间. 数据库放在内存中时，这种扫视与合并是自动进行的.

正常情况下，不需要调用这个谓词. 但当插入新项时如果数据库中有太多的回收空间没有得到重新利用，db_garbagecollect有助于得到可利用的空间.

chainDB::db_btrees/1
回溯时，db_btrees会连续将BtreeName约束为指定数据库中各个B+树的名称.

各个名称是依排定的顺序返回的. 关于B+树后面还要介绍.

chainDB::db_chains/1
回溯时，db_chains会连续将ChainName约束为指定数据库中各个链的名称. 各个名称是依排定的顺序返回的.

chainDB::db_statistics/4
db_statistics返回数据库的统计信息. 各参数所表示的意义如下：

参数NoOfTerms表示数据库中的总项数. 参数MemSizeis表示数据库存放于内存中的内表所占字节数. 参数DbaSizeis表示数据库中项与描述符占用的总字节数. 如果数据库是在文件中，而这个值比该文件占用的字节数小很多，这个文件就应该可以用db_copy进行压缩. 参数FreeSize表示空闲内存的值，依数据库当前位置不同这个值也会不同：


 * 数据库中内存中时，FreeSize表示全局栈顶与堆顶之间空闲的字节数（注意：可能还有若干空间的字节数没有包含在这个值中）.
 * 数据库在文件中时，FreeSize表示硬盘文件中未使用的字节数.

操控链
To要把一个项插入到外部数据库的链中，可以使用谓词 chain_inserta、chain_insertz 或 chain_insertafter. 可以用 chain_terms 连续地把它的参数约束为链的项及其索引号，还可以用 chain_delete 删除外部数据库中某个链的所有项.

还有四个标准谓词可以返回数据库的索引号，这四个标准谓词是：chain_first、chain_last、chain_next 和 chain_prev.

操控项
有三个外部数据库管理的标准谓词都与项有关，它们是：term_replace、term_delete 和 ref_term. 无论调用哪个与项有关的外部数据库标准谓词，都需要将项的域作为一个参数给出来. 正因为如此，所以把一个数据库中所有项的域声明为单个域中的可选项会是个好办法. 例如：

domains terms_for_my_stock_control_database = customer(Customer, Name, ZipCode, Address); supplier(SupplierNo, Name, Address); parts(PartNo, Description, Price, SupplierNo). 注意，对外部数据库没有类型（域）上的混杂限制要求. 一个链可以含有文本串而另一个链上的可以是整数，第三个链中还可以是某种复合结构，等等. 但是，外部数据库的数据项并不是按类型描述符存放的，比如一个整数未必就是刚好占用两个字节. 取回数据时恰好就是存放时的那种样子（同样的域），这是编程人员应该保证的. 如果弄混了域，会产生运行时错误.

B+树
B+树是一种数据结构，用于实现快速有效地对大量数据进行分类排序的方法. B+树还有比较高效的搜索算法. 可以认为B+树对数据库提供了一个索引，这也是为什么有时它又被称为“索引”的原因.

在Visual Prolog中，B+树是在外部数据库中的. B+树的每个项有一对值：一个键串和相关联的数据库索引号. 创建数据库时，先在数据库中插入记录并为该记录建立一个键. Visual Prolog B+树谓词接着就可以用来把这个键及该记录相应的数据库索引号插入到一个B+树中.

要搜索数据库中的某个记录时，只需要找到该记录的键，而B+树会给出相应的索引号. 用这个索引号，就可以从数据库中得到需要的记录. 当B+树增长变化时，它会保持键的顺序，这也就意味着我们可以很容易地得到排序好的记录列表.

B+树类似于二进制树，只不过在B+树的各个节点上保存了不止一个键串. B+树也是平衡树，这就是说对树中各个叶上的每个键的搜索路径长度是一样的. 由于这个特点，对于在数以百万计的键中搜索某个键是有保证的，就是在最坏的情况下，需要对磁盘进行访问，也只需要不多的几次，这和每个节点上保存有多少键相关.

尽管B+树是放在外部数据库中的，但并非一定要与它所指的项放在同一个数据库中，可以是一个数据库中含有一些链，而另一个数据库放指向这些链中项的B+树.

Pages(页） Order（阶） Key Length（键长）
在B+树中，键是聚合在页里的，每页有相同的大小，所有的页都包含有同样数量的键，这意味着所有为B+树保存的键必须大小相同. 键的大小取决于KeyLen参数，这是在创建B+树时需要指定的一个参数. 如果要在B+树中插入比KeyLen还要长的串，Visual Prolog会把多出来的部分截去. 通常应该尽可能选小一些的KeyLen值以节省空间、提高速度.

创建B+树时还需要指定称为阶的一个参数. 这个参数决定了树的各个节点可以存储多少个键，而通常它是需要反复试验才能取得最佳值. 阶的起始值用4是不错的选择，它允许各节点上保存4～8个键. 阶的值必须要通过实验来选取，因为B+树的搜索速度与键长与阶的值、B+中键的数量以及计算机硬件配置都有关系.

重复键
设置B+树时，需要允许键的重复. 比如，设置一个客户数据库的B+树，用客户的姓做键，键就会有重复出现的可能. 由于这个原因，B+树是可以有重复键的.

要删除数据库中的一个项，需要给出相应的B+树键及数据库索引号（因此键重复也不会有问题）.

多重操作
为了能对B+进行多重操作，每个B+树内部都有多个指针，允许对一个树多次打开. 不过要注意，如果更新了一个B+树的拷贝，而此时又有已经打开了的副本，则副本的指针会重定位到树的顶端.

B+树的标准谓词
Visual Prolog提供了一些标准谓词来处理B+树，这些谓词的工作方式与相应的 db_... 谓词是一致的.

chainDB::bt_create/4 and chainDB::bt_create/5
调用这个谓词可以创建一个新的B+树.

参数BtreeName指定新树的名称，以后可以把这个名称作为参数用于bt_open. B+树的KeyLen参数和Order参数是在树创建时给出的，以后不能再更改. 如果调用bt_create/4或bt_create/5时参数Duplicates设为1，则在这个B+树中允许重复；若是设为0，则不能在该树中插入重复的项.

chainDB::bt_open/2
bt_open打开名为BtreeName的已经创建了的B+树.

打开一个B+树时，这个调用会返回一个用于该B+树的选择器Btree_Sel，它就是该B+树的引用，无论系统的搜索或定位操作都要用到它. B+树的名称与选择器之间的关系就如同一个实际的文件名称与相应的符号文件名之间的关系.

可以多次打开同一个B+树以便同时进行多项操作. 每打开一次该B+树，就分配一个描述符，各个描述符都对应有自己的B+树内部指针.

chainDB::bt_close/1 and chainDB::bt_delete/1
可以用bt_close关闭一个打开的B+树，也可以用bt_delete删除整个B+树.

调用bt_close会释放打开的名为BtreeName的B+树分配的内部缓冲区.

chainDB::bt_copyselector/2
bt_copyselector对已经打开的B+树复制一个新的选择器.

新选择器开始时其指针定位在与老的选择器相同的位置上. 之后，B+树的两个选择器定位就都可以自行确定，互不相干.

chainDB::bt_statistics/7
bt_statistics返回由Btree_Sel指定的B+树的统计信息. 各参数意义如下：

参数Btree_Sel是该B+树的bt_selector；NumKeys是该树中键的总数；NumPages是该树的总页数；Depth是该树的深度；KeyLen是该树键长；Order是该树的阶；PgSize是页大小（以字节计）.

chainDB::key_insert/3 and chainDB::key_delete/3
标准谓词key_insert和key_delete用于更改B+树.

给定Key和Ref时，可以删除有重复项的B+树中的某个确定项.

chainDB::key_first/2, chainDB::key_last/2, and chainDB::key_search/3 每个B+树都维护有指向自身节点的内部指针. key_first和key_last可以分别把指针置于树的第一个键和最后一个键的位置上，而key_search则可以把指针置于指定键的位置上.

如果找到了指定的键，key_search就成功；反之则失败，并将B+的内部指针置于该指定键应该存放的位置之后. 这时可以用key_current返回这个键及其数据库索引号. 如果想要在一个有重复项的B+树中准确定位，也可以把索引号做为一个条件输入.

chainDB::key_next/2 and chainDB::key_prev/2
可以用谓词key_next和key_prev在经过排序树中向前或向后移动B+树指针.

If如果已经到了树的端头，想进一步移动指针会引起失败，但指针本身还是会移出树外.

chainDB::key_current/3
key_current返回B+树当前指针指向的键及其数据库索引号.

调用谓词bt_open、bt_create、key_insert或key_delete之后立即使用key_current谓词会失败，当指针由于key_prev或key_next已经位于第一个键之前或最后一个键之后使用key_current谓词也会失败.

外部数据库编程
本节要介绍使用Visual Prolog外部数据库系统的一些基本方法与原则. 主要内容有：


 * “遍历数据库” 介绍顺序扫遍外部数据库中的链或B+树的方法.
 * “数据库的防护” 说明如何应对没想到的电源故障及其它可能的问题以保护数据库.
 * “使用B+树内部指针” 说明如何在打开的B+树中使用定位指针的一些谓词.

遍历数据库
在使用数据库系统时，头脑中一定要清楚Visual Prolog的存储机制. 每当Visual Prolog用ref_term谓词从外部数据库中取回一个项时，会把这个项放在全局栈中. 这个项占用的空间，系统要一直等到程序失败并回溯到ref_term调用之前的点才会释放. 这意味着，要想连续地遍历外部数据库中某个链，总应该使用下面这样的结构：

/* 连续访问一个链所用的结构 */ scan(ChainObj, Chain, ....) :- ChainObj:chain_first(Chain, Ref), scanloop(ChainObj, Ref). scanloop(ChainObj, Ref) :- hasDomain(myDom, Term), ChainObj:ref_term(Ref, Term), /* ... 进行相应的处理 ... */   fail. scanloop(ChainObj, _) :- ChainObj:chain_next(Ref, NextRef), scanloop(ChainObj, NextRef).

同样，要遍历索引，应该用如下的结构：

/* 连续访问索引所用的结构 */ scan(ChainObj, Bt_selector) :- ChainObj:key_first(Bt_selector, FirstRef), scanloop(ChainObj, Bt_selector, FirstRef). scanloop(ChainObj, Bt_selector, Ref) :- hasDomain(myDom, Term), ChainObj:ref_term(Ref, Term), /* ... 进行相应的处理 ... */   fail. scanloop(ChainObj, Bt_selector, _) :- ChainObj:key_next(Bt_selector, NextRef), scanloop(ChainObj, Bt_selector, NextRef).

也可以用chain_terms来遍历外部数据库中的链，像这样：

/* 遍历链的另一种方法 */ scan(ChainObj, Chain) :- hasDomain(myDom, Term), ChainObj:chain_terms(Chain, Term, Ref), /* ... 进行相应的处理 ... */   fail. scan(_, _).

要遍历B+树，可以通过已经定义和使用过的bt_keys谓词来实现. 回溯时，这个谓词就会逐一返回（指定B+和数据库）各个键及相应的数据库索引号.

predicates bt_keys : (chainDB, bt_selector, string, ref). bt_keysloop : (chainDB, bt_selector, string, ref). clauses bt_keys(ChainObj, Bt_selector, Key, Ref) :- ChainObj:key_first(Bt_selector, _), bt_keysloop(ChainObj, Bt_selector, Key, Ref). bt_keysloop(ChainObj, Bt_selector, Key, Ref) :- ChainObj:key_current(Bt_selector, Key, Ref). bt_keysloop(ChainObj, Bt_selector, Key, Ref) :- ChainObj:key_next(Bt_selector, _), bt_keysloop(ChainObj, Bt_selector, Key, Ref).

数据库的防护
如果在数据库中输入了大量信息，要确保系统掉电时这些信息不会丢失就很重要了. 本节要介绍一种方法：把对数据库的所有变更记录在另一个文件中.

对数据库进行变更，首先更新数据，然后再把缓冲区的内容也写进来. 如果操作成功，系统就记录变更到一个登记文件并更新登记文件本身. 任何一个时刻，只有一个文件是不保险的. 有了登记文件，如果数据库（由于电源失效前没有及时得到更新）不可用时，就可以用备份的数据库和登记文件恢复完整的数据库内容；而如果登记文件出了问题，也可以创建新的登记文件并对备份新的数据库文件.

如果在登记文件中还记录了修改数据的日期时间，则还可以按指定时间恢复数据库原来的状态.

使用B+树的内部指针
每个打开的B+树都有一个指向其节点的指针，打开或更新B+树时，这个指针指向树的起点之前；当指针指向树的最后一个键时，调用key_next会使指针指向树的末端之后的位置. 只要指针超出了树，key_current就都会失败. 如果这样一些安排不能满足特定应用程序的要求，就需要自行构造其它的谓词了.

比如，可以构造像下面这样一些mykey_next、mykey_prev、mykey_search的谓词，让B+树指针总是在B+树的内部（如果树中有一些键的话）.

predicates mykey_next(db_selector, bt_selector, ref). mykey_prev(db_selector, bt_selector, ref). mykey_search(db_selector, bt_selector, string, ref). clauses mykey_prev(Dba, Bt_selector, Ref) :- Dba:key_prev(Bt_selector, Ref), !. mykey_prev(Dba, Bt_selector, Ref) :- Dba:key_next(Bt_selector, Ref), fail. mykey_next(Dba, Bt_selector, Ref) :- Dba:key_next(Bt_selector, Ref), !. mykey_next(Dba, Bt_selector, Ref) :- Dba:key_prev(Bt_selector, Ref), fail. mykey_search(Dba, Bt_selector, Key, Ref) :- Dba:key_search(Bt_selector, Key, Ref), !. mykey_search(Dba, Bt_selector, _, Ref) :- Dba:key_current(Bt_selector, _, Ref), !. mykey_search(Dba, Bt_selector, _, Ref) :- Dba:key_last(Bt_selector, Ref).

还可以使用下面定义的samekey_next和samekey_prev谓词，将索引指针定位到具有重复键的B+树中同一键值的下一个键上：

predicates samekey_next(db_selector, bt_selector, ref). try_next(db_selector, bt_selector, ref, string). samekey_prev(db_selector, bt_selector, ref). try_prev(db_selector, bt_selector, ref, string). clauses samekey_next(Dba, Bt_selector, Ref) :- Dba:key_current(Bt_selector, OldKey, _), try_next(Dba, Bt_selector, Ref, OldKey). try_next(Dba, Bt_selector, Ref, OldKey) :- Dba:key_next(Bt_selector, Ref), Dba:key_current(Bt_selector, NewKey, _), NewKey = OldKey, !. try_next(Dba, Bt_selector, _, _) :- Dba:key_prev(Bt_selector, _), fail. samekey_prev(Dba, Bt_selector, Ref) :- Dba:key_current(Bt_selector, OldKey, _), try_prev(Dba, Bt_selector, Ref, OldKey). try_prev(Dba, Bt_selector, Ref, OldKey) :- Dba:key_prev(Bt_selector, Ref), Dba:key_current(Bt_selector, NewKey, _), NewKey = OldKey, !. try_prev(Dba, Bt_selector, _, _) :- Dba:key_next(Bt_selector, _), fail.

外部数据库与文件共享
Visual Prolog支持文件共享型外部数据库，也就是说一个文件可以被若干用户或是进程同时打开，这对在局域网中或多任务平台上使用外部数据库是很有用处的.

Visual Prolog提供了下面几种文件共享机制：


 * 打开已有文件有两种不同的访问模式及三种不同共享模式，以优化访问速度，
 * 处理过程中会聚合对数据库的访问以确保连贯性，
 * 有检查其他用户是否更新过数据库的谓词.

文件共享域
有两个特殊的域，可供文件共享选择使用：

域accessMode = read或readWrite；域denyMode = denyNone或denyWrite或denyAll.

共享模式下打开数据库
为了使打开的外部数据库可以共享，需要使用四元维版本的db_open打开已有的数据库文件，并指定AccessMode及DenyMode.

如果AccessMode是read，打开的文件就只能读，任何对文件的更新都会导致运行时错误；如果是readwrite，该文件就可读可写. AccessMode在谓词db_begintransaction中也会用到.

如果DenyMode是denyNone，那么所有其他的用户都可以更新和读取该文件；若是denyWrite，则其他用户不能以 AccessMode = readWrite 模式打开该文件，但如果该文件是按 AccessMode = readWrite 模式打开的，还是可以对文件进行更新的. 如果db_open是带 DenyMode = denyAll 的，则任何其他用户都不能访问该文件.

打开文件的第一个用户使用的DenyMode，决定了后续所有打开该文件的条件，如果试图用不相容的模式打开该文件，就会产生运行时错误. 下表归纳了打开文件的结果及后续再打开同一个文件时的各种情况：

Domain（域） 用途 bt_selector bt_open返回的用于选取B+树的唯一值 place 数据库的位置：内存中或是文件中 accessmode 确定文件的使用方式 denymode 确定其他用户可以打开该文件的方式 ref 项在链中位置的索引

Transactions（事务处理）与文件共享
如果一个数据库文件是打开共享的，则所有以任何方式访问数据库文件的数据库谓词必须要集中在事务处理内部，它是通过把对这些谓词的调用包围在db_begintransaction和db_endtransaction之间来实现的.

根据选择的AccessMode和DenyMode不同，共享的文件有可能在事务处理期间会被锁定. 再根据锁定的程度，事务处理时其他用户可能还不能读或更新该文件. 这对于避免读写的混乱当然是必要的，但文件共享要有意义就一定不要有过多的锁定. 这可以通过尽量减小（缩短）事务处理来实现，只把那些访问数据库的谓词包含在事务处理中.

涉及文件共享的事务处理这个概念非常重要，两者之间的需求是矛盾的，即同时要实现数据库的安全和最少的文件锁定.

db_begintransaction是确保数据库一致性的，它实现文件的适当锁定，文件可由若干用户同时读但只允许有一个进程在当下更新数据库. 谓词db_setretry可以用来设置db_begintransaction在返回运行时错误之前需要等待多长时间才可以访问该文件. 当文件AccessMode是按read打开的，而调用db_begintransaction时AccessMode设成了readWrite也会产生运行时错误. 如果已经调用了db_begintransaction，对同一个数据库调用新的db_begintransaction之前必须调用db_endtransaction，否则就会出现运行时错误.

下表归纳了db_begintransaction对不同的AccessMode和DenyMode组合所做的反应：

AccessMode read readWrite DenyMode denyNone WLock\Reload RWLock\Reload denyWrite None RWLock denyAll None NoneActions:

WLock ：不允许写，只允许读. RWLock ：读和写都不允许. Reload ：重新载入文件描述符. 由于重新载入及锁定都是需要时间的，所以要小心选用AccessMode和DenyMode. 比如，若不会有用户更新数据库，将AccessMode设为read而DenyMode设为denyWrite就可以使开销最小化.

文件共享谓词
Visual Prolog中有一些文件共享谓词，它们是db_open、db_begintransaction、db_endtransaction、db_updated、bt_updated和db_setretry.

chainDB::db_begintransaction/1
这个谓词标记事务处理的起点，应该在访问共享的数据库之前调用它，哪怕是以denyAll打开的数据库，也应该调用它. db_begintransaction调用时AccessMode必须为read或readwrite. 进一步的了解可以参看PFC的帮助文件.

chainDB::db_endtransaction/0
db_endtransaction标记一个事务处理的结束并解锁数据库. 必须在每个db_begintransaction调用之后且在下一个db_begintransaction调用之前调用db_endtransaction.

chainDB::db_updated/0
如果有其他用户更新了数据库，db_begintransaction的调用会保持数据库的一致性. 调用谓词db_updated可以检查变更情况：如果在一个事务处理中调用它，而db_begintransaction之后有其他用户更新过数据库，就会成功；如果没有更新，就会失败. 如果在事务处理之外调用这个谓词，就会产生运行时错误.

chainDB::bt_updated/1
这个谓词很像db_updated，只不过它是在指定的B+树更新了才会成功.

chainDB::db_setretry/2
如果由于某个进程锁定了一个文件而导致另一个进程访问该文件被拒绝，这另一个进程可以进入一个等待期，期满后再试着访问该文件. 谓词db_setretry可以改变缺省的SleepPeriod设置，这是以百分之一秒为单位的等待期；这个谓词还可以设置RetryCount，这是重试访问的最大次数. 缺省的设置，RetryCount是100而SleepPeriod是10.

文件共享时的编程
使用文件共享谓词一定要十分小心. 尽管使用恰当时它们能够保证共享数据库低层的一致性，但防止某个程序危害高层的一致性则是应用程序编写人员的责任. 这中间所用的所谓“事务处理（transaction）”是将文件访问集中在一起的一个方法，但一定要清楚它并没有提供什么恢复机制，不论是软件还是硬件失效引起的程序中断，都有可能导致数据库文件的不完整.

若干进程共享一个数据库时，还需要特别注意涉及到的域，它们必须要严格一致，使用时顺序也要一致.

为避免不要地锁定数据库文件，事务处理一定要尽量短小以使文件锁定时间尽可能短. 同时，要把对数据库项的定位与访问的谓词放在一个事务处理中，这也很重要.

..... ChainObj:db_begintransaction(readWrite), ChainObj:key_current(Btree, Key, Ref), ChainObj:ref_term(Ref, Term), ChainObj:db_endtransaction, write(Term), .....

上面的代码中，谓词key_current和ref_term不应该分开放在不同的事务处理中，因为Ref中存储的项可能会在不同事务处理间就被其他用户删除了.

当B+树被别的用户更新并且文件缓冲区重新加载后，B+树的指针会重新定位到第一项之前. 调用谓词bt_updated可以确定是否需要重新对B+树指针定位. 把当前的键暂存在内部数据库中，既可以列表完整的索引号，同时也可以使用事务处理短小.

实现高级锁定
对于一个共享的数据库文件，用户可以进行所有独自享有该文件时相同的操作. 把对文件的访问集中于db_begintransaction和db_endtransaction的内部，可以保证Visual Prolog系统自身描述符表的一致性. 但是，在网络环境中多用户条件下应用程序的各种逻辑约束, 需要在较高层级上由编程人员确保得以实现.

W我们把这称之为高层级的锁定或应用程序级的锁定. 通过使用简单的db_begintransaction和db_endtransaction，可以有多种方法实现高级锁定的方法.

需要使用高级锁定的一个很普通的例子，就是用户想要改变数据库中记录的时候. 确定要改变一个记录时，就需要采取一些步骤使应用程序对该记录锁定，直到更改结束、新的记录写回到磁盘中，然后对该记录解锁.

下面是对应用程序级锁定的一些建议：

或许还需要实现某种超级用户机制，以便特殊的用户可以解除对记录的锁定.
 * 在记录中设置一个专门的字段来表示它是否为锁定状态；
 * 设置一个专门的B+树或链，存放所有被用户锁定的记录的索引；
 * 用一个REF存放所有锁定记录的索引表.

这只是个例子，比如需要实现对表或表组，或知识组等的高级锁定.

注意： 如果要在以共享方式打开的数据库文件中删除一个B+树，必须要使用高级锁定来确保没有其他用户打开这同一个树. Visual Prolog系统中没有对一个B+树名是否因其被删除了而不可用做检查.

一个完整的文件共享实例
在这个完整的例子中，可以看到如何通过使用自己实现的锁定系统来方便地共享文件. 如果自己管理锁，可以避免不必要的文件锁定，其他用户不必因为文件锁定而长时间等待对文件的访问.

该程序允许若干用户对一个共享的文件进行文本的创建、编辑、查看及删除. 创建和编辑文本时，需要进行锁定直到编辑完成. 文本锁定时，其他用户不能删除或编辑该文本，但可以查看它. 用不同的 db_open和db_setretry设置运行这个程序，体验一下吧.

Visual Prolog文件共享的实现方面
Visual Prolog中的文件共享很有效率，很快，因为其它用户对数据库进行了更新之后只是必需的数据库文件描述符部分要重新加载. 前面已经介绍过，只是在特定环境下才需要进行文件缓冲区的重载及文件锁定，而数据库文件内部完善的管理保证了更新后只进行最小必要的磁盘操作.

数据库有一个六字节整数序列号，每次更新它都会增加并被写到磁盘上. db_begintransaction谓词会将当前的序列号副本与磁盘上的进行比较，如果不一样，就重载描述符. 锁使用的是一个可以容纳256个用户的数组. 当一个用户需要访问文件时，就会为其在这个数组中分配一个未使用的位置，并在事务处理期间一直占用这个位置. 这种方法允许文件同时有若干个读者进行访问. 如果db_begintransaction是以AccessMode = readWrite方式调用的，则需要等待当前已有读者都从这个数组中退出并放弃所占用的位置之后，再锁定整个的数组，使其他用户不能再访问该文件.

小结
Visual Prolog外部数据库系统为用户的数据库应用程序添加了强大、快速和高效的功能. 在本文中主要介绍了：


 * 外部数据库的项是存储在链中的，它可以直接用数据库索引号进行访问；索引号属于预定义的ref域.
 * 各数据库可以由数据库选择符来指定，它是属于标准域db_selector的.
 * 外部数据库可以放在两个地方，这与预定义的place域有关：
 * in_file把数据库放在磁盘文件中，
 * in_memory把数据库放在内在中.
 * 如果要在数据库中对项进行排序，会要用到B+树. 与数据库一样，各个B+树是通过B+树选择符来指定的，它属于标准域bt_selector.
 * B+树节点的每个项由一个标识记录的键串（也叫索引）和与记录关联的数据库索引号构成.
 * B+树的键组成页，而存储在一个节点上的键的数量是由树的阶指定的.
 * 文件共享是通过将访问数据库的谓词组合在事务处理中实现的.