医学信息学/计算机化病历案例研究
在澳大利亚,广泛引入基于 PC 的计算机病历系统(主要基于 Windows 操作系统)之前,普通科医生诊所的病历记录使用的是 RACGP 标准病历表格,用于患者登记和摘要,以及病程记录。
使用计算机化病历系统带来了许多优势,例如提高了处方可读性,从而理论上减少了由于 GP 手写处方到药剂师之间沟通不畅导致的用药错误,也消除了药剂师需要打电话给 GP 确认难以辨认处方的业务效率低下问题。随着集成地址簿和文字处理功能的加入,医生可以更快地转诊至专家。医学上的理由是,专家也可以自动获取患者病症和处方药物的摘要列表,并将其包含在打印的转诊信中,从而改善了患者交接过程,但这种方法很少被依赖,除了可能由外科医生使用。
澳大利亚的病历系统还可以下载病理学和放射学报告,通常通过病理学服务提供商的外部专有下载客户端进行,该客户端将结果文件放置在文件目录中,供 GP 病历系统解析,使用一些简单的行定向格式,例如 PIT。这发生在互联网接入从拨号接入发展到 ADSL/有线接入,实现持续接入,并需要使用加密和身份验证互联网标准开发安全互联网传输的背景下。
在澳大利亚背景下,早期采用某家供应商的解决方案意味着,用于存储小型企业记录的数据库技术在十多年内保持相当稳定,实际上,DBase/Foxpro 的 DBF 文件格式成为许多年的大多数普通科医生电子病历的基础。
由于普通科医生诊所规模大多是小规模的,因此没有必要改变更简单但更优雅的 DBASE 格式数据库技术,Windows 网络文件系统的机会性文件锁定足以满足系统使用需求,即使在有多名医生的诊所中,十多个用户同时访问也是如此。基于 B 树的 DBASE 索引文件提供了可扩展性,因为记录会累积多年的患者病程记录、操作、疫苗接种、转诊、扫描的回复信件以及下载的文本结果。但 DBASE 索引文件的结构没有公开文档,并且在没有详细了解 DBASE IDX/MDX 文件的情况下,快速查找给定患者的所有相关记录的能力是 GP 基于计算机的病历功能定制的主要障碍。
在一段时间内,这意味着任何普通科医生都能够在知识储备范围内成为自己病历系统的掌控者,因为 DBF 文件格式众所周知,它是一种逻辑堆文件格式,记录只是简单地追加到存储特定表信息的文件末尾。但是,缺乏容易获得/理解的有关 DBASE *索引*文件结构的信息构成了一个主要障碍。
本案例研究将尝试说明 DBASE 的简单、逻辑、透明的组织如何与对一种特定计算机算法(即线性哈希)的基本理解相交,并提供扩展功能的手段。随着向供应商特定实现的 SQL 数据库系统的迁移,这个时代令人遗憾地结束了,未来的防护和免受供应商市场撤出影响变得更加困难。
现代数据库系统通常使用 SQL 声明式编程接口作为前端,因为 SQL 对非程序员来说相对容易学习,但需要大量的基础设施来支持它,并且在 SQL 语法和用于检索的底层文件格式和算法之间存在很大距离。大多数数据库系统仍然依赖索引来指示给定记录的键值存储在何处,索引中使用的算法要么基于 B 树的变体,要么基于哈希。B 树索引的主要优势在于索引键按排序方式存储,因此可以根据较低的键值或较高的键值检索一系列记录。例如,所有患者姓氏以 Smi 开头的记录都可以通过使用 Smia 到 Smiz 作为上下限值来检索。
哈希算法主要在索引中使用,因为它们速度相对较快。简单来说,猴子爬树枝找到香蕉所需的时间比猴子阅读指向香蕉的指示牌所需的时间更长。因此,哈希索引在实现 *JOIN* 操作时通常更快。例如,患者的病历可能包含人口统计表中的条目,通过 ID 字段引用,该字段称为主键。患者的药物可能存储在药物表中,该表也有一个 DEMOGRAPHIC_ID 字段,称为外键字段,因此,通过对人口统计表和药物表进行联接,我们可以通过识别具有 ID 值的患者来识别患者正在服用的药物。
什么是哈希?哈希是将一些数据值转换为整数的过程。通常,一个好的哈希函数会为要转换的数据值域生成一个大致随机的整数。需要一个好的哈希函数的原因是,生成的哈希值用作索引到存储位置,在这个上下文中,就是索引哈希文件中的文件位置。因此,为了有效地使用空间,从输入数据集中派生的所有可能的哈希值应该在存储地址范围内大致均匀分布。
另一个有用的概念是冲突,即两个不同的值被哈希到同一个哈希整数。为了处理冲突,可以在每个位置分配额外的空间,这样就有了固定数量的额外位置,这可以称为一个桶(槽)。如果哈希到一个位置的键数量超过了桶中的槽数,该怎么办?那么可以由该桶引用另一个桶,这称为溢出桶/块/空间。
由于普通科医生数据库的规模较小,因此可以认为标准哈希算法可以用于实现外部索引文件,使用非常大的索引文件空间,每个可能的哈希值对应一个桶,如果需要,可以将溢出桶追加到索引文件的末尾。
更优雅的算法是线性哈希算法,它在几乎所有优秀的数据库基础教程中都有提及,与可扩展哈希并列,被认为是数据库系统中使用的大型基于文件索引需求的最佳哈希算法。
基于文件的索引的主要限制是文件本身的性质:它们很容易创建,也很容易追加,但在文件中间插入数据块很昂贵,因为在插入点,插入点之后的文件的剩余部分必须复制到一个临时文件,原始文件在插入点截断,新的块追加到截断的文件,然后将临时文件的内容追加到该文件。B 树一次扩展一个节点/块,新的节点可以追加到文件末尾。可扩展/线性哈希也可以一次扩展一个块,尽管可扩展哈希可能需要在索引文件开头预留一个目录结构的最大尺寸估计,或者使用单独的目录文件,因为目录结构会定期加倍。
B 树索引使用一种算法,其中键与指向文件地址的指针相关联,该地址标记表示 B 树中其他节点的其他文件数据块的开始。B 树是自平衡的,因为条目的溢出意味着一个节点被拆分,并且一个额外的键被传递到父节点,从而减缓了树高度的增长;最终导致根节点被拆分。新的节点只是简单地追加到索引文件的末尾,并且文件偏移值(节点“指针”)允许随机文件访问从根节点向下遍历块。
可扩展哈希和线性哈希类似于基数搜索,因为地址空间通过增加哈希值中使用的位数来增加,每增加一位就会使地址空间加倍。维基百科的百科全书参考资料对可扩展哈希和线性哈希进行了充分的描述。
然而,由于下面的代码中使用了线性哈希,因此需要简要回顾一下:线性哈希通过使用一个条件哈希函数来实现,该函数依赖于一个迭代变量 split。在 split 值的第一次迭代中,该变量的范围从哈希值 0 到 N-1,即第一次迭代的初始桶数。条件哈希函数的工作原理如下。
h' = h mod M where M = N x 2 ^ level, if h' < split, then h' = h mod M' where M = N x 2 ^(level +1).
每当任何一个桶溢出时,都需要更多空间,这可以通过临时创建一个附加到溢出桶的溢出桶来处理。但它也通过将一个桶附加到当前的桶列表中来处理,因此当 split 变量为零时,附加的桶在 N 中。完成此操作后,split 变量将递增:此后,条件哈希函数将使用 mod 2N 而不是 N 来计算任何最初哈希为零的值,以便记录将被哈希到 0 或 N。例如,如果 y = x mod R 且 y' = x mod 2R,则对于任何非负整数 x,y' = y 或 y' = y + R。这方便地将 split 遍历的值哈希到原始编号的桶 n 或另一个编号的桶,该桶为 n + N 乘以 2 ^ level。这就是线性哈希,因为桶的数量呈线性增长。每当 split 变量达到 N * 2 ^ level(或 N 左移 level 次)时,level 将递增,而 split 将重置为 0,以便条件哈希函数可以访问下一级添加的桶,同时在所有可用桶上进行循环访问,在它们积累太多记录之前将它们拆分,例如:对于 0 到 N-1,N 到 2N-1 是可访问的;对于 0 到 2N-1,2N 到 4N-1 是可访问的,等等。
案例研究账户:过敏症检查器,以及基于操作系统目录的咨询文件日志
[edit | edit source]在本案例研究中,最初的“痒到抓挠”是一个用户需求,要求对用户输入的过敏症信息进行监控,因为虽然医生在开药前几乎会自动询问是否存在过敏症,但澳大利亚的一般做法是要求在所有患者身上记录过敏症,无论每次就诊时是否检查过敏症列表的有效性。这导致实现了一个用 java 编写的外部程序来检查与过敏症相关的 DBF 表格,最初发现内存中的 Map 类(HashMap、TreeMap)足以在程序启动时索引这些表格,并维护一个临时的内存索引以方便查找。通过轮询操作系统“注册表”中与程序相关联的属性来找到一种方法,以便在打开患者记录时检测到,以便触发对过敏症表格的查找,如果尚未记录过敏症,则会弹出一个提醒窗口,并且此检查将使用 java Timer 类定期执行。
下一个用户需求是记录咨询统计信息,以帮助审查之前的咨询,以便临床医生可以根据医疗记录程序未提供的其他详细信息,在他需要的情况下审查他的实践并轻松找到他最近见过的患者。这需要在一个大小超过内存中的 Map java 类能力的表格中进行查找,因此建议使用线性哈希实现来可扩展地索引此类表格。
不幸的是,最初的实现将块管理作为文件实体混合在一起,无法正常工作,而且难以调试,因此第二个实现集中于首先证明算法在内存中是有效的。
一旦错误被消除(主要是由于 java 语言固有的无符号整数表示导致动态哈希函数出现错误),内存中的线性哈希实现被用作基于文件的块管理器的缓存结构。块基本上被保存为一个文件,文件名与块号相同。
由于 java 的内置 LinkedList 类型能够在给定索引位置存储空值,因此空值被用作缓存未命中标志,要求文件块管理器加载一个块。
每当加载一个块时,也会通过比较运行时的空闲内存与块列表大小乘以平均块大小来进行检查,如果它低于此值的某个倍数,则通过遍历列表并将其写入磁盘来选择一些非空块。一旦块被写入,块中的数据就会通过清除对它们的任何引用(例如,调用 map.clear())并随后调用垃圾收集器(System.gc())来使垃圾收集器可以使用。这将导致可用于数据对象分配的可用 java 内存增加。
由于块的加载和保存已经适用于缓存,因此发现它们可以用于加载先前创建的线性哈希结构,或保存新构建的结构。这也需要因为从大量记录构建一个持久索引结构需要超过 10 分钟的时间,对于这样一个监控程序来说,这是不可接受的启动时间。
下面是 Map 接口插件,以及一个带有用于保存和加载索引的静态公共方法的文件管理器类。它说明了从对算法的基本理解出发,这可以用来提供基于计算机的医疗记录系统的透明性和可扩展性。这不会提供 DBF 文件访问逻辑,但这可以通过使用容易获得的 DBF 文件结构和基本类型的描述来实现,这些类可以作为 java 关联数组 Map 类的直接插入,作为数据库索引的程序表示,这些索引可以通过遍历存储在 DBF 表格文件中的记录列表来构建。一旦索引被构建并保存到磁盘,它可以在将来启动监控程序时重新加载。在使用任何索引之前,可以将索引中的条目数量与 DBF 表格文件头中记录的当前记录数量进行比较,如果更少,则可以从位置 L(最后一个记录数量)读取记录到 N(当前记录数量),并将它们添加到索引对象中。因此,一旦索引被构建,它就会定期更新,通过检查文件中的先前记录数量,该数量在每次将索引保存到磁盘时存储为磁盘上的索引头的一部分。使用一个关闭钩子来确保在正常关闭时更新索引。
线性哈希映射实现的具体用途是创建以患者身份键为中心的进度条目索引。每个与给定患者相关的进度记录列表都作为与患者 ID 作为键的链接列表对象存储。然后将患者 ID 通过动态哈希函数传递,并将患者 ID 到链接列表进度 ID 的键值对存储为有限大小的块对象的条目。鉴于此表示,读者可以推断出桶的大小不是可变的,或者使用了大量浪费的空间来适应最大值列表大小。由于块的大小可变,因此决定每个块都是一个文件,并且整个文件系统目录将用于保存线性映射的数据。这将在处理块时增加打开/关闭文件的内核开销,因此,由于将块表示为单个文件的简化决策,而不是将固定大小的块附加到单个索引文件中,因此实现可能速度更慢。然而,当使用完全构建的索引检索特定患者的记录以记录咨询审查信息时,性能足够快。
这种使用线性哈希映射构建索引以连接/关联表格的用户 API 将 SQL 连接查询的隐藏复杂性与表格模式的显式审查相交换,以换取学习 java 语言的 arguably 相同的复杂性,以及使用 java 语言的灵活性来输出自定义报告。似乎没有障碍可以简单地将用于不同语言的方法翻译过来,例如 python。
这种简单的数据库索引实现缺少什么? ACID 是一个购物清单首字母缩略词,它会让人想起;原子性、隔离性、一致性和持久性,在面对并发访问和操作过程中的随机系统崩溃时。到目前为止,对于作为本地过敏症检查器和咨询统计记录器在单用户机器上的使用,在 3 个月的使用时间内似乎没有问题,但台式机从未在索引文件映像更新过程中出现故障,这很少见,但由于没有特别考虑,因此可能需要删除索引文件,并且在下次操作系统启动时需要长时间的重新初始化索引(程序通过正在使用的 windows 操作系统的启动菜单运行)。
处理并发性的最简单方法是使用 java 的 Collections.syncrhonizedMap 包装器包装 Map 接口,而处理可恢复性的标准方法是使用检查点的预写日志。一些 DBMS 使用写时复制,并保留先前写入的数据值的版本,但什么才是最小的足以满足需求?主要的写入操作是对现有键值对的值进行覆盖,插入新的键值对,以及拆分块。一种恢复方案称为影子页面,其中在检查点保留页面目录/块列表的副本,并为写入存储的块赋予版本号,因此块 x 将具有名称“x.v”。这可以通过添加一个可序列化的小型映射属性来实现,该属性将块号映射到块。版本文件名。如果在上次检查点之后发生崩溃,则影子块列表和块名称映射将成为当前块列表和名称映射。如果需要,可以通过删除任何名称包含比当前影子块名称映射早的版本号的文件来定期执行垃圾回收。
使用线性哈希算法实现的基本持久数据库索引的 java 实现
[edit | edit source]package linearhashmap;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
/**
*
* @author S.Tan
*
* @param <K>
* key type , must implement equals() and hashCode()
* @param <V>
* value type
*
*
*/
public class LHMap2<K, V> implements Map<K, V>, Serializable {
/**
*
*/
private static final long serialVersionUID = 3095071852466632996L;
/**
*
*/
public static interface BlockListener<K, V> {
public void blockRequested(int block, LHMap2<K, V> map);
}
List<BlockListener<K, V>> listeners = new ArrayList<BlockListener<K, V>>();
// int savedBlocks;
int N;
int level = 0;
int split = 0;
int blockSize;
long totalWrites = 0;
List<Block<K, V>> blockList = new ArrayList<Block<K, V>>();
public void addBlockListener(BlockListener<K, V> listener) {
listeners.add(listener);
}
void notifyBlockRequested(int block) {
for (BlockListener<K, V> l : listeners) {
l.blockRequested(block, this);
}
}
public LHMap2(int blockSize, int nStartingBlocks) {
this.blockSize = blockSize;
this.N = nStartingBlocks;
for (int i = 0; i < nStartingBlocks; ++i) {
addBlock();
}
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
showStructure();
}
}));
}
public static class Block<K, V> implements Externalizable {
/**
*
*/
int j = 0;
Block<K, V> overflow = null;
LinkedList<K> keyList = new LinkedList<K>();
LinkedList<V> valueList = new LinkedList<V>();
transient private LHMap2<K, V> owner;
transient private Map<K, V> shadow = new TreeMap<K, V>();
private boolean changed = false;
private int size = 0;
public LHMap2<K, V> getOwner() {
return owner;
}
public void setOwner(LHMap2<K, V> owner) {
this.owner = owner;
Block<K, V> ov = overflow;
while (ov != null) {
overflow.setOwner(owner);
ov = ov.overflow;
}
}
public Block() {
}
public Block(LHMap2<K, V> map) {
setOwner(map);
}
public V put(K k, V v) {
setChanged(true);
V v2 = replace(k, v);
if (v2 == null) {
++size;
if (keyList.size() == getOwner().blockSize) {
if (overflow == null) {
getOwner().blockOverflowed(this, k, v);
} else {
overflow.put(k, v);
}
} else {
keyList.addFirst(k);
valueList.addFirst(v);
}
}
return v2;
}
void setChanged(boolean b) {
changed = b;
}
public Map<K, V> drainToMap(Map<K, V> map) {
while (!keyList.isEmpty()) {
K k = keyList.removeLast();
V v = valueList.removeLast();
map.put(k, v);
}
if (overflow != null)
map = overflow.drainToMap(map);
garbageCollectionOverflow();
return map;
}
public void updateMap(Map<K, V> map) {
Iterator<K> ik = keyList.descendingIterator();
Iterator<V> iv = valueList.descendingIterator();
while (ik.hasNext() && iv.hasNext()) {
map.put(ik.next(), iv.next());
}
if (overflow != null)
overflow.updateMap(map);
}
private void garbageCollectionOverflow() {
if (overflow != null) {
overflow.garbageCollectionOverflow();
overflow = null;
}
}
public void addOverflowBucket() {
// assert overflow is needed
if (keyList.size() < getOwner().blockSize)
return;
if (overflow == null) {
overflow = new Block<K, V>(getOwner());
} else {
overflow.addOverflowBucket();
}
}
public V replace(Object key, V v2) {
if (overflow != null) {
V v = overflow.replace(key, v2);
if (v != null)
return v;
}
int j = 0;
for (K k : keyList) {
if (key.equals(k)) {
V v = valueList.get(j);
if (v2 != null) {
valueList.set(j, v2);
}
return v;
}
++j;
}
return null;
}
public boolean isChanged() {
return changed;
}
@Override
public void readExternal(ObjectInput arg0) throws IOException,
ClassNotFoundException {
int sz = arg0.readInt();
for (int i = 0; i < sz; ++i) {
K k = (K) arg0.readObject();
V v = (V) arg0.readObject();
shadow.put(k, v);
}
}
public void loadFromShadow(LHMap2<K, V> owner) {
setOwner(owner);
Block<K, V> b = this;
for (Map.Entry<K, V> entry : shadow.entrySet()) {
if (b.keyList.size() == owner.blockSize) {
Block<K, V> overflow = new Block<K, V>(owner);
b.overflow = overflow;
b = overflow;
}
b.keyList.add(entry.getKey());
b.valueList.add(entry.getValue());
}
shadow.clear();
}
@Override
public void writeExternal(ObjectOutput arg0) throws IOException {
if (!changed)
return;
Map<K, V> map = new TreeMap<K, V>();
// this is destructive of the block
updateMap(map);
int sz = map.size();
arg0.writeInt(sz);
for (Map.Entry<K, V> entry : map.entrySet()) {
arg0.writeObject(entry.getKey());
arg0.writeObject(entry.getValue());
}
setChanged(false);
}
}
void init() {
for (int i = 0; i < N; ++i) {
addBlock();
}
}
/**
* @param hashValue
* @return a bucket number.
*/
int getDynamicHash(int hashValue) {
long unsignedHash = (long) hashValue & 0xffffffffL;
int h = (int) (unsignedHash % (N << level));
// System.err.println("h = " + h);
if (h < split) {
h = (int) (unsignedHash % (N << (level + 1)));
// System.err.println("h < split, new h = " + h);
}
return h;
}
public V put(K k, V v) {
++totalWrites;
int h = getDynamicHash(k.hashCode());
Block<K, V> b = getBlock(h);
b.put(k, v);
return v;
}
public long getTotalWrites() {
return totalWrites;
}
private Block<K, V> getBlock(int h) {
notifyBlockRequested(h);
return blockList.get(h);
}
void blockOverflowed(Block<K, V> b, K k, V v) {
splitNextBucket();
b.addOverflowBucket();
b.put(k, v);
}
private void splitNextBucket() {
Block<K, V> b = getBlock(split);
TreeMap<K, V> map = new TreeMap<K, V>();
b.drainToMap(map);
addBlock();
System.err.printf("split N LEVEL %d %d %d \n", split, N, level);
if (++split >= (N << level)) {
++level;
split = 0;
}
for (Map.Entry<K, V> entry : map.entrySet()) {
K k = entry.getKey();
V v = entry.getValue();
int h = getDynamicHash(k.hashCode());
System.err.println(h + " ");
Block<K, V> b2 = getBlock(h);
b2.put(k, v);
}
}
private Block<K, V> addBlock() {
Block<K, V> b = new Block<K, V>(this);
blockList.add(b);
return b;
}
@Override
public void clear() {
blockList = new ArrayList<Block<K, V>>();
split = 0;
level = 0;
totalWrites = 0;// savedBlocks = 0;
}
@Override
public boolean containsKey(Object key) {
return get(key) != null;
}
@Override
public boolean containsValue(Object value) {
return values().contains(value);
}
@Override
public Set<java.util.Map.Entry<K, V>> entrySet() {
Set<Map.Entry<K, V>> set = new TreeSet<Map.Entry<K, V>>();
for (final K k : keySet()) {
set.add(new Entry<K, V>() {
@Override
public K getKey() {
return k;
}
@Override
public V getValue() {
return get(k);
}
@Override
public V setValue(V value) {
return put(k, value);
}
});
}
return Collections.unmodifiableSet(set);
}
@Override
public V get(Object key) {
int h = getDynamicHash(key.hashCode());
Block<K, V> b = getBlock(h);
return b.replace(key, null);
}
@Override
public boolean isEmpty() {
return size() == 0;
}
@Override
public Set<K> keySet() {
Set<K> kk = new TreeSet<K>();
for (int i = 0; i < blockList.size(); ++i) {
Block<K, V> b = getBlock(i);
kk.addAll(b.keyList);
Block<K, V> ov = b.overflow;
while (ov != null) {
kk.addAll(ov.keyList);
ov = ov.overflow;
}
}
return Collections.unmodifiableSet(kk);
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public V remove(Object key) {
return null;
}
@Override
public int size() {
return (int) Math.min(longSize(), (long) Integer.MAX_VALUE);
}
public long longSize() {
long sz = 0;
for (Block<K, V> b : blockList) {
Block<K, V> b1 = b;
while (b1 != null) {
sz += b1.size;
b1 = b.overflow;
}
}
return sz;
}
@Override
public Collection<V> values() {
List<V> list = new ArrayList<V>();
for (K k : keySet()) {
list.add(get(k));
}
return Collections.unmodifiableList(list);
}
private void showStructure() {
for (int i = 0; i < blockList.size(); ++i) {
Block<K, V> b = getBlock(i);
Block<K, V> ov = b.overflow;
int k = 0;
while (ov != null) {
ov = ov.overflow;
++k;
}
System.out.println("Block " + i + " size " + b.keyList.size()
+ " overflow blocks = " + k);
}
}
}
package linearhashmap;
import java.io.File;
import java.io.Serializable;
import java.util.List;
import java.util.Random;
public class LHMap2BlockFileManagerData implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
public byte[] buf;
public Random r;
public File baseDir;
public File home;
public int maxBlocks;
public int retired;
public double unloadRatio;
public List<Integer> retirees;
public int avgBlockSize;
public long avgCount;
public LHMap2BlockFileManagerData(byte[] buf, Random r, int retired,
List<Integer> retirees, long avgCount) {
this.buf = buf;
this.r = r;
this.retired = retired;
this.retirees = retirees;
this.avgCount = avgCount;
}
}
package linearhashmap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Random;
/**
* This class manages the LHMap2 class block disk swapping, and saves and load
* an instance of the LHMap2 class. It has been used to externally index a
* legacy file based database of 100,000 record master table, and 1,000,000
* record sized child tables, and accounts for heap space available in the java
* virtual machine, so that OutOfMemory errors are avoided when the heap space
* is low by putting blocks back on files, and garbage collecting them. The main
* performance bottleneck appeared when loading a million record table for an
* index , on initial creation of the index.
*
* @author doctor
*
* @param <K>
* @param <V>
*/
public class LHMap2BlockFileManager<K, V> implements
LHMap2.BlockListener<K, V>, Serializable {
/**
*
*/
private static final long serialVersionUID = 2615265603397988894L;
LHMap2BlockFileManagerData data = new LHMap2BlockFileManagerData(
new byte[10000], new Random(), 0, new ArrayList<Integer>(), 0);
public LHMap2BlockFileManager(File baseDir, String name, int maxBlocks,
double unloadRatio) {
data.home = new File(baseDir, name);
if (!data.home.exists())
data.home.mkdir();
this.data.maxBlocks = maxBlocks;
this.data.unloadRatio = unloadRatio;
}
@Override
public void blockRequested(int block, LHMap2<K, V> map) {
LHMap2.Block<K, V> b = map.blockList.get(block);
if (b == null) {
int tries = 3; // for some reason, the File constructor can be
// asynchronous occasionally, so need to try again
// after delay
File f = new File(data.home, Integer.toString(block));
do {
if (f.exists())
break;
if (!f.exists()) {
if (--tries >= 0)
fatal(block);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} while (true);
try {
ByteArrayInputStream bis = new ByteArrayInputStream(data.buf);
FileInputStream fis = new FileInputStream(f);
int sz = fis.read(data.buf);
fis.close();
addByteStats(sz);
ObjectInputStream ois = new ObjectInputStream(bis);
b = new LHMap2.Block<K, V>();
b.readExternal(ois);
ois.close();
b.loadFromShadow(map);
map.blockList.set(block, b);
--data.retired;
} catch (FileNotFoundException e) {
e.printStackTrace();
fatal(block);
} catch (IOException e) {
e.printStackTrace();
fatal(block);
} catch (ClassNotFoundException e) {
e.printStackTrace();
fatal(block);
}
}
int size = map.blockList.size();
try {
long freeMemory = Runtime.getRuntime().freeMemory();
long limitMemory = (long) (data.avgBlockSize * data.unloadRatio * size);
if (block % 30 == 0)
System.err.println("free memory =" + freeMemory + " limit "
+ limitMemory);
if (map.split % 20 == 19) {
// this is just to add statistics before really needing to
// retire
retireActiveBlock(map, block);
++data.retired;
} else if (freeMemory < limitMemory) {
for (int i = 0; i < map.blockList.size() / 4; ++i) {
retireActiveBlock(map, block);
++data.retired;
}
System.runFinalization();
System.gc();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private void addByteStats(int sz) {
++data.avgCount;
data.avgBlockSize = (int) ((data.avgBlockSize * (data.avgCount - 1) + sz) / data.avgCount);
}
public void retireActiveBlock(LHMap2<K, V> map, int notThisOne)
throws FileNotFoundException, IOException {
int pick = 0;
int size = map.blockList.size();
for (pick = 0; pick < size
&& (pick == notThisOne || map.blockList.get(pick) == null); ++pick)
;
if (pick < size)
retireBlock(map, pick);
}
private void retireBlock(LHMap2<K, V> map, int pick) throws IOException,
FileNotFoundException {
LHMap2.Block<K, V> b = map.blockList.get(pick);
if (b == null)
return;
if (b.isChanged()) {
File f = new File(data.home, Integer.toString(pick));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
b.writeExternal(oos);
oos.flush();
oos.close();
FileOutputStream fos = new FileOutputStream(f);
byte[] bb = bos.toByteArray();
fos.write(bb);
fos.flush();
fos.close();
if (bb.length > data.buf.length) {
data.buf = bb;
}
}
map.blockList.set(pick, null);
}
private void fatal(int block) {
Exception e = new Exception();
try {
throw e;
} catch (Exception e2) {
e2.printStackTrace();
}
System.err.println("block " + block
+ " requested and it is not in blocklist and not a file");
for (int i : data.retirees) {
System.err.print(i + " ");
}
System.err.println(" were retired");
System.exit(-2);
}
public static boolean metaExists(File indexDir, String name) {
File home = new File(indexDir, name);
return new File(home, "LinearMap2").exists();
}
public static <K, V> LHMap2<K, V> load(File baseDir, String name)
throws FileNotFoundException, IOException, ClassNotFoundException {
File home = new File(baseDir, name);
File f2 = new File(home, "LinearMap2");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f2));
LHMap2<K, V> map = (LHMap2<K, V>) ois.readObject();
ois.close();
loadBlocks(map);
return map;
}
private static <K, V> void loadBlocks(LHMap2<K, V> map) {
LHMap2BlockFileManager<K, V> mgr = getBlockManagerListener(map);
int size = map.blockList.size();
for (int i = 0; i < size; ++i) {
mgr.blockRequested(i, map);
}
}
public static <K, V> LHMap2BlockFileManager<K, V> getBlockManagerListener(
LHMap2<K, V> map) {
LHMap2BlockFileManager<K, V> mgr = (LHMap2BlockFileManager<K, V>) map.listeners
.get(0);
return mgr;
}
public static void save(File indexDir, String name,
LHMap2<?, ?> offsetMap) throws FileNotFoundException, IOException {
retireAllBlocks(offsetMap);
File home = new File(indexDir, name);
File f2 = new File(home, "LinearMap2");
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(f2));
oos.writeObject(offsetMap);
oos.close();
}
private static <K, V> void retireAllBlocks(LHMap2<K, V> offsetMap)
throws FileNotFoundException, IOException {
LHMap2BlockFileManager<K, V> mgr = getBlockManagerListener(offsetMap);
int sz = offsetMap.blockList.size();
for (int i = 0; i < sz; ++i) {
LHMap2.Block<K, V> b = offsetMap.blockList.get(i);
if (b != null) {
mgr.retireBlock(offsetMap, i);
}
}
}
}
线性哈希是一种散列算法,对于基于键的连接非常有效,其中需要键之间的相等性。但是,需要范围搜索索引来回答某些查询,例如以 SM 开头的名字(以 SMZ 结尾)以查找所有 SMITH,查找所有超过 50 岁的糖尿病患者,查找过去 12 个月内由 Z 医生看过的所有超过 50 岁的患者,他们没有进行血糖测试等等。
该算法通常是 B 树,并且在 1980 年代流行的数据库中也是如此,例如 DBase 系列(foxpro、paradox 等),以及更新的流行数据库,例如 Postgresql、Firebird、DerbyDb 等。B 树是在 1960 年代后期作为一种索引算法引入的,当需要范围可搜索索引时,似乎没有更好的算法(在实施方面如所实践的那样),据说如果需要在哈希和 B 树之间进行选择,那么后者提供了超集功能,但代价是损失 O(0-2) 磁盘访问时间。
线性哈希更容易理解,并且更容易映射到磁盘存储,但 B 树在概念上也同样易于理解,只是调试稍微困难一点。
B 树的重要特征是什么?它基本上就像一棵二叉树,实际上,一棵二叉树实际上是一棵 B 树,其中一个节点只有一个条目,而一个 B 树节点可能包含 n 个条目,其中 n 大约在 10-100 之间,并且通常被引用为 n x(条目大小)= 物理磁盘块大小的大小。这个想法是将一个块加载到内存中并搜索它,如果搜索键没有找到,但它在两个键之间或所有键的左侧或右侧,并且存在指向另一个块的指针在块的左侧或每个键的右侧,然后遵循指针,直到搜索成功或无法遵循任何块。
使用 B 树的原因是,当块在插入键时变得满时,可以通过将其分成三个部分来进行拆分,将其分成一个包含所有左边的条目的块,一个包含中间键的条目以及一个包含所有条目的块在中间键的右边,然后在从插入返回时将包含中间键的条目传递到父块,然后父块将其条目插入到其条目中,如果需要也会进行拆分。如果根节点拆分,则中间条目将成为一个单条目块,该块将成为根节点。在一系列插入过程中,这实际上是一种自平衡策略,并且树在宽度而不是深度上增长,以便以后的搜索主要在加载一个块之后完成,并且在内存中进行宽度遍历快速搜索,而不是更不频繁、更缓慢的垂直搜索从磁盘加载未缓存的子块。
下面是一个内存中的 Java B 树实现,可以像上面线性哈希一样,适用于基于磁盘的块管理,只是需要稍微多一些跟踪工作,例如使用显式引用 ID 来表示块,而不是引用块,并将块存储在 map 中,其中 null 值表示未缓存的块。
[[[1]]] 为什么作者现在放弃了二分查找。
package btreemap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
/** can't work without setting a comparator */
public class BTreeMap<K, V> implements SortedMap<K, V> {
private static final int NODE_SIZE = 100;
BTBlock<K, V> root;
Comparator<? super K> comparator;
@Override
public Comparator<? super K> comparator() {
// TODO Auto-generated method stub
return comparator;
}
/**
*
* @param comparator
* - this is mandatory for the tree to work
*/
public void setComparator(Comparator<? super K> comparator) {
this.comparator = comparator;
root = new BTBlock<K, V>(NODE_SIZE, comparator);
}
/**
*
* @author syan
*
* @param <K>
* @param <V>
* the entry is associated with a right child block.
*
*/
static class BlockEntry<K, V> {
K k;
V v;
BTBlock right;
BlockEntry() {
}
BlockEntry(K k, V v) {
right = null;
this.k = k;
this.v = v;
}
}
/**
*
* @author syan - this represents the result of splitting a full block into
* a left block, and a right block, and a median key, the right
* block and the median key held in a BlockEntry structure as above.
* @param <K>
* @param <V>
* @param <V>g
*/
static class SplitRootEntry<K, V> {
BTBlock<K, V> left;
BlockEntry<K, V> entry;
SplitRootEntry(BTBlock<K, V> left, BlockEntry<K, V> entry) {
this.left = left;
this.entry = entry;
}
SplitRootEntry() {
super();
}
}
/**
* this is used to return a result of a possible split , during recursive
* calling.
*
* @author syan
*
* @param <K>
* @param <V>
*/
static class resultAndSplit<K, V> {
/**
* null , if there is no split.
*/
SplitRootEntry<K, V> splitNode;
V v;
resultAndSplit(V v) {
this.v = v;
}
resultAndSplit(SplitRootEntry<K, V> entry, V v) {
this.v = v;
this.splitNode = entry;
}
}
/**
* used to represent the insertion point after searching a block if compare
* is zero, then a match was found, and pos is the insertion entry if
* compare < 0 and pos == 0 , then the search ended up less than the
* leftmost entry else compare > 0 , and the search will be to the immediate
* right of pos.
*
* @author syan
*
*/
static class PosAndCompare {
int pos = 0;
int compare = 0;
}
static class BTBlock<K, V> {
List<BlockEntry<K, V>> entries;
BTBlock<K, V> leftBlock = null;
private int maxSz = 0;
Comparator<? super K> comparator;
Comparator<? super K> comparator() {
return comparator;
}
public BTBlock(int size, Comparator c) {
entries = new ArrayList<BlockEntry<K, V>>();
maxSz = size;
this.comparator = c;
}
/**
* PosAndCompare usage: if compare is zero, then a match was found, and
* pos is the insertion entry if compare < 0 and pos == 0 , then the
* search ended up less than the leftmost entry else compare > 0 , and
* the search will be to the immediate right of pos.
*
* @author syan
*
*/
private void blockSearch(K k, PosAndCompare pc) {
for (int i = 0; i < entries.size(); ++i) {
pc.compare = comparator.compare(k, entries.get(i).k);
if (pc.compare == 0) {
pc.pos = i;
return;
}
if (pc.compare < 0 && i == 0) {
pc.pos = 0;
return;
}
if (pc.compare < 0) {
pc.pos = i - 1;
pc.compare = 1;
return;
}
}
pc.pos = entries.size() - 1;
pc.compare = 1;
// binary search, it's hard to get it right !
// int left = 0;
// int right = entries.size();
//
// while (left <= right && left < entries.size()) {
// pc.pos = (right - left) / 2 + left;
// pc.compare = comparator().compare(k, entries.get(pc.pos).k);
// if (pc.compare < 0) {
// right = pc.pos-1;
// } else if (pc.compare > 0) {
// left = pc.pos+1;
// } else {
// break;
// }
// }
}
resultAndSplit<K, V> put(K k, V v) {
V v2;
if (entries.size() == 0) {
entries.add(new BlockEntry<K, V>(k, v));
return new resultAndSplit<K, V>(v);
} else {
PosAndCompare pc = new PosAndCompare();
blockSearch(k, pc);
// a match
if (pc.compare == 0) {
v2 = entries.get(pc.pos).v;
entries.get(pc.pos).v = v;
return new resultAndSplit<K, V>(v2);
}
BlockEntry<K, V> entry = new BlockEntry<K, V>(k, v);
// follow leftBlock if search is to left of first entry
if (pc.compare < 0 && pc.pos == 0 && leftBlock != null) {
resultAndSplit<K, V> result = leftBlock.put(k, v);
if (result.splitNode != null) {
leftBlock = result.splitNode.left;
entries.add(0, result.splitNode.entry);
}
} else if (pc.compare > 0 && entries.get(pc.pos).right != null) {
// follow right block if it exists
resultAndSplit<K, V> result = entries.get(pc.pos).right
.put(k, v);
if (result.splitNode != null) {
entries.get(pc.pos).right = result.splitNode.left;
entries.add(pc.pos + 1, result.splitNode.entry);
}
} else if (pc.compare < 0) {
// add to the left
entries.add(pc.pos, entry);
} else if (pc.compare > 0) {
// add to the right
entries.add(pc.pos + 1, entry);
}
// check if overflowed block , split if it has.
if (entries.size() > maxSz) {
int mid = entries.size() / 2;
// copy right half to new entries list.
List<BlockEntry<K, V>> rightEntries = new ArrayList<BlockEntry<K, V>>();
for (int i = mid + 1; i < entries.size(); ++i) {
rightEntries.add(entries.get(i));
}
BlockEntry<K, V> centreEntry = entries.get(mid);
BTBlock<K, V> rightBlock = new BTBlock<K, V>(maxSz,
comparator);
rightBlock.entries = rightEntries;
// the median entry's right block is the new right block's
// leftmost block
rightBlock.leftBlock = centreEntry.right;
// the new right block becomes the right block
centreEntry.right = rightBlock;
// reduce the old block's entries into its left half of
// entries.
for (int i = entries.size() - 1; i >= mid; --i)
entries.remove(i);
// create a return object, with the reduced old block as the
// left block
// and the median entry with the new right block attached
SplitRootEntry<K, V> split = new SplitRootEntry<K, V>(this,
centreEntry);
// the keyed value didn't exist before , so null
return new resultAndSplit<K, V>(split, null);
}
return new resultAndSplit<K, V>(v);
}
}
V get(K k) {
if (entries.size() == 0)
return null;
PosAndCompare pc = new PosAndCompare();
blockSearch(k, pc);
if (pc.compare == 0) {
return entries.get(pc.pos).v;
}
if (pc.compare < 0 && pc.pos == 0 && leftBlock != null) {
return leftBlock.get(k);
} else if (pc.compare < 0 && pc.pos == 0 && leftBlock == null) {
return null;
} else if (pc.compare > 0 && entries.get(pc.pos).right != null) {
return (V) entries.get(pc.pos).right.get(k);
} else
return null;
}
void getRange(SortedMap map, K k1, K k2) {
PosAndCompare pc = new PosAndCompare();
blockSearch(k1, pc);
PosAndCompare pc2 = new PosAndCompare();
blockSearch(k2, pc2);
for (int i = pc.pos; i <= pc2.pos; ++i) {
if (i == 0 && pc.pos == 0 && pc.compare < 0
&& leftBlock != null) {
leftBlock.getRange(map, k1, k2);
// if (pc2.pos == pc.pos)
// break;
}
/*
* add the entry if it exactly matches, or it is in range, but
* not if it is non-inclusive limit i.e. i == pc.pos and compare
* > 0
*/
if ((pc.compare == 0 && pc.pos == i) || i > pc.pos)
map.put(entries.get(i).k, entries.get(i).v);
if (entries.get(i).right != null) {
entries.get(i).right.getRange(map, k1, k2);
}
}
}
K headKey() {
if (leftBlock != null) {
return leftBlock.headKey();
}
return entries.get(0).k;
}
K tailKey() {
int i = entries.size() - 1;
if (entries.get(i).right != null) {
return (K) entries.get(i).right.tailKey();
}
return entries.get(i).k;
}
void show(int n) {
showTabs(n);
for (int i = 0; i < entries.size(); ++i) {
BlockEntry<K, V> e = entries.get(i);
System.err.print("#" + i + ":(" + e.k + ":" + e.v + ") ");
}
System.err.println();
showTabs(n);
if (leftBlock != null) {
System.err.print("Left Block\n");
leftBlock.show(n + 1);
} else {
System.err.println("No Left Block");
}
for (int i = 0; i < entries.size(); ++i) {
BlockEntry<K, V> e = entries.get(i);
showTabs(n);
if (e.right != null) {
System.err.println("block right of #" + i);
e.right.show(n + 1);
} else {
System.err.println("No block right of #" + i);
}
}
showTabs(n);
System.err.println("End of Block Info\n\n");
}
private void showTabs(int n) {
// TODO Auto-generated method stub
for (int i = 0; i < n; ++i) {
System.err.print(" ");
}
}
}
@Override
public SortedMap<K, V> subMap(K fromKey, K toKey) {
TreeMap<K, V> map = new TreeMap<K, V>();
root.getRange(map, fromKey, toKey);
return map;
}
@Override
public SortedMap<K, V> headMap(K toKey) {
// TODO Auto-generated method stub
return subMap(root.headKey(), toKey);
};
@Override
public SortedMap<K, V> tailMap(K fromKey) {
// TODO Auto-generated method stub
return subMap(fromKey, root.tailKey());
}
@Override
public K firstKey() {
// TODO Auto-generated method stub
return root.headKey();
}
@Override
public K lastKey() {
// TODO Auto-generated method stub
return root.tailKey();
}
@Override
public int size() {
// TODO Auto-generated method stub
return 0;
}
@Override
public boolean isEmpty() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean containsKey(Object key) {
// TODO Auto-generated method stub
return get(key) != null;
}
@Override
public boolean containsValue(Object value) {
// TODO Auto-generated method stub
return false;
}
@Override
public V get(Object key) {
// TODO Auto-generated method stub
return root.get((K) key);
}
@Override
public V put(K key, V value) {
resultAndSplit<K, V> b = root.put(key, value);
if (b.splitNode != null) {
root = new BTBlock<K, V>(root.maxSz, root.comparator);
root.leftBlock = b.splitNode.left;
root.entries.add(b.splitNode.entry);
}
return b.v;
}
@Override
public V remove(Object key) {
// TODO Auto-generated method stub
return null;
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
// TODO Auto-generated method stub
}
@Override
public void clear() {
// TODO Auto-generated method stub
}
@Override
public Set<K> keySet() {
// TODO Auto-generated method stub
return null;
}
@Override
public Collection<V> values() {
// TODO Auto-generated method stub
return null;
}
@Override
public Set<java.util.Map.Entry<K, V>> entrySet() {
// TODO Auto-generated method stub
return null;
}
}
使用上述(带有块持久性的线性哈希)数据结构作为持久存储辅助索引 *1,此示例程序已应用于上述临床用途。有趣的是,查询结果识别了将糖尿病列为疾病的患者,以及合并症,以及存在时原子化的病理 HbA1c 数据,但通常在每个执业医师下列出的患者要么是非定期患者,要么存在可疑的糖尿病诊断,而且查询没有检查患者是否正在其他诊所或内分泌科医师处接受管理。
一项练习可能是将查询重新表述为“将患者分组到执业医师下,这些患者有糖尿病相关诊断和现有的 HbA1c 病理原子,但他们的最后一次 HbA1c 超过 6 个月”。这至少会选择那些可能在某个阶段接受咨询执业医师监测的患有糖尿病诊断的患者。
*1 BTreeMap 被用作普通红黑树 TreeMap 类的替代品,作为概念证明,并且在使用的数据集中实际上没有发生内存溢出。线性哈希用于通过 MapperImpl1 类实现表的 ur_no 索引。这是对于具有大量行的表(例如 PathAtom,以及肯定的 Progress)所必需的,因为索引太大而无法容纳在运行在普通 2G Windows 桌面办公电脑上的虚拟 Java 环境中。
package dbfmap.linux.test;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Map.Entry;
import btreemap.BTreeMap;
import dbfmap.structs.DBFMemoPair;
import dbfmap.structs.DBFMemoPair.Iterator;
import dbfmap.use.Mapper;
import dbfmap.use.MapperImpl1;
public class FindDiabeticOverdue {
private static final int MAXINTERVALMONTHS_NOHBA1C = -6;
final static String RESULT = "PATHOL";
final static String RESULT_NAME_FIELD = "TESTNAME";
final static String RESULT_DATE = "REPORTDATE";
static String[] crossRefConditions = new String[] { "DIAB", "HEART", "MYOCARD",
"HYPERT", "HYPERC", "HYERL", "ANGIN", "CORON", "ISCHEM", "VASCUL" , "GOU", "METABO", "OBES" };
private BTreeMap<Date, List<String>> btree;
private Map<String, DBFMemoPair> dbfmap;
final DateFormat dateFormat = SimpleDateFormat
.getDateInstance(SimpleDateFormat.SHORT);
public Map<String, DBFMemoPair> getDbfmap() {
return dbfmap;
}
public FindDiabeticOverdue() throws IOException, ClassNotFoundException {
init();
}
void init() throws IOException, ClassNotFoundException {
Mapper m = MapperImpl1.getInstance();
dbfmap = m.getAccessObjectMap();
DBFMemoPair pairPat = dbfmap.get("PATIENTS");
DBFMemoPair accessResult= dbfmap.get(RESULT);
Iterator iResult = accessResult.iterator();
Map<String, Object> vv;
btree = new BTreeMap<Date, List<String>>();
btree.setComparator(new Comparator<Date>() {
@Override
public int compare(Date arg0, Date arg1) {
// TODO Auto-generated method stub
return arg0.compareTo(arg1);
}
});
Calendar calendarMaxIntervalNoHbA1c = GregorianCalendar.getInstance();
calendarMaxIntervalNoHbA1c.add(Calendar.MONTH, MAXINTERVALMONTHS_NOHBA1C);
// SortedMap before = btree.tailMap(c.getTime());
DBFMemoPair pairHist = dbfmap.get("HISTORY");
Iterator iHistory = pairHist.iterator();
Map<String, Object> vh;
BTreeMap<String, List<String>> bTreeConditionUrNos = new BTreeMap<String, List<String>>();
while ((vh = iHistory.readNextValues()) != null) {
String condition = (String) vh.get("CONDITION");
List<String> l = bTreeConditionUrNos.get(condition);
if (l == null) {
l = new LinkedList<String>();
}
l.add((String) vh.get("UR_NO"));
bTreeConditionUrNos.put(condition, l);
}
SortedMap<String, List<String>> diabetesUrNos = bTreeConditionUrNos.subMap("DIAB", "DIABZ");
SortedSet<String> patientUrNosWithDiabetes = new TreeSet<String>();
for (String s : diabetesUrNos.keySet()) {
System.out.println(s + diabetesUrNos.get(s).size() + " patients");
patientUrNosWithDiabetes.addAll(diabetesUrNos.get(s));
}
Map<String, Map<String, Object>> urNoToLastHbA1c = new HashMap<String, Map<String, Object>>();
for (String urno : patientUrNosWithDiabetes) {
List<Map<String, Object>> resultsOnePatient = accessResult.getRecords("UR_NO", urno);
for (Map<String, Object> result : resultsOnePatient) {
String testName = (String) result.get(RESULT_NAME_FIELD);
if (testName.startsWith("GLYCATED")) {
Map<String, Object> lastResult = urNoToLastHbA1c.get(urno);
if (lastResult == null
|| ((Date) result.get(RESULT_DATE)).after((Date) lastResult
.get(RESULT_DATE))) {
urNoToLastHbA1c.put(urno, result);
}
}
}
}
Map<String, Map<String, Object>> overdue = new TreeMap<String, Map<String, Object>>();
for (String urno : urNoToLastHbA1c.keySet()) {
Map<String, Object> lastResult = urNoToLastHbA1c.get(urno);
Date d = (Date) lastResult.get(RESULT_DATE);
if (d.before(calendarMaxIntervalNoHbA1c.getTime())) {
overdue.put(urno, lastResult);
}
}
List<String> noHbA1c = new ArrayList<String>(patientUrNosWithDiabetes);
noHbA1c.removeAll(urNoToLastHbA1c.keySet());
DateFormat f = SimpleDateFormat.getDateInstance(DateFormat.SHORT);
System.out.println("Those with No HbA1c =" + noHbA1c.size());
TreeMap<String, List<String> > responsibleTable = new TreeMap<String, List<String> > ();
for (String urno : noHbA1c) {
Seen seen = getResponsible(urno);
List<String> drsPatients = responsibleTable.get(seen.whoMost);
if ( drsPatients == null ) {
drsPatients = new ArrayList<String>() ;
responsibleTable.put(seen.whoMost,drsPatients);
}
drsPatients.add(urno);
}
for (String urno : overdue.keySet()) {
Seen seen = getResponsible(urno);
List<String> drsPatients = responsibleTable.get(seen.whoMost);
if ( drsPatients == null ) {
drsPatients = new ArrayList<String>() ;
responsibleTable.put(seen.whoMost,drsPatients);
}
drsPatients.add(urno);
}
for ( String dr : responsibleTable.keySet()) {
System.out.println("*********************** List for " + dr);
for ( String urNo : responsibleTable.get(dr)) {
List<Map<String, Object>> shouldBeOnePatientMap = pairPat.getRecords("UR_NO", urNo);
if (shouldBeOnePatientMap == null || shouldBeOnePatientMap.size() != 1) {
System.err
.println("ERROR with UR_NO for finding patient list : "
+ urNo
+ (shouldBeOnePatientMap == null ? " list " : "sz="
+ (String.valueOf(shouldBeOnePatientMap.size()))));
if (shouldBeOnePatientMap == null || shouldBeOnePatientMap.size() == 0)
continue;
}
Map<String, Object> patient = shouldBeOnePatientMap.get(0);
System.out.println(((String) patient.get("SURNAME")).trim() + ", "
+ ((String) patient.get("FIRSTNAME")).trim() + ", DOB:"
+ f.format(patient.get("DOB")));
DBFMemoPair atoms = dbfmap.get("PATHATOM");
List<Map<String, Object>> pathAtomsForOnePatient = atoms.getRecords("UR_NO", urNo);
boolean printedTest = false;
for (Map<String, Object> at2: pathAtomsForOnePatient ) {
String n = (String)at2.get("TESTNAME");
if ( n.toUpperCase().indexOf("A1C") >= 0 ) {
printedTest = true;
System.out.print( ", " + f.format(at2.get("RESULTDATE")) + " " + at2.get("RESULT").toString().trim() );
}
}
if ( printedTest)
System.out.println();
List<Map<String, Object>> conditions = pairHist.getRecords("UR_NO", urNo);
boolean printedCond = false;
for ( Map<String, Object> condition : conditions ) {
String conditionName = ((String)condition.get("CONDITION")).trim();
for ( String keyPart : crossRefConditions)
if ( conditionName.indexOf(keyPart)>= 0) {
if (!printedCond) System.out.print("Conditions: ");
System.out.print( " " + conditionName +":"+condition.get("YEAR"));
printedCond = true;
break;
}
}
if (printedCond)
System.out.println( );
}
System.out.println("**************** end of list for " + dr);
}
// for (String s : noHbA1c) {
// List<Map<String, Object>> l = pairPat.getRecords("UR_NO", s);
// if (l == null || l.size() != 1) {
// System.err
// .println("ERROR with UR_NO for finding patient list : "
// + s
// + (l == null ? " list " : "sz="
// + (String.valueOf(l.size()))));
// if (l == null || l.size() == 0)
// continue;
// }
// Map<String, Object> patient = l.get(0);
// System.out.println(((String) patient.get("SURNAME")).trim() + ", "
// + ((String) patient.get("FIRSTNAME")).trim() + ", DOB:"
// + f.format(patient.get("DOB")));
// List<Map<String, Object>> patConditions = pairHist.getRecords(
// "UR_NO", s);
// for (Map<String, Object> c2 : patConditions) {
// if (((String) c2.get("CONDITION")).indexOf("DIABE") >= 0) {
// System.out.println("\t" + (String) c2.get("CONDITION")
// + c2.get("YEAR"));
// }
// }
// Seen seen = showResponsible(s);
//
// }
//
// System.out.println("\n\nOVERDUE HbA1c ? = " + overdue.size());
//
// for (String s : overdue.keySet()) {
// Map<String, Object> res2 = overdue.get(s);
// List<Map<String, Object>> l = pairPat.getRecords("UR_NO", s);
// Map<String, Object> x = l.get(0);
// System.out.println(((String) x.get("SURNAME")).trim() + ", "
// + ((String) x.get("FIRSTNAME")).trim() + ", DOB:"
// + f.format(x.get("DOB")));
//
// System.out.println("\t" + f.format((Date)res2.get("REPORTDATE")) + " "+
// (String)res2.get(RESULT_NAME_FIELD) );
// DBFMemoPair atoms = dbfmap.get("PATHATOM");
// l = atoms.getRecords("UR_NO", s);
// for (Map<String, Object> at2: l ) {
// String n = (String)at2.get("TESTNAME");
// if ( n.toUpperCase().indexOf("A1C") >= 0 ) {
// System.out.print( "," + f.format(at2.get("RESULTDATE")) + " " + at2.get("RESULT") );
// }
// }
// System.out.println();
// showResponsible(s);
// }
}
private Seen showResponsible(String s) throws IOException,
ClassNotFoundException {
Seen seen = getResponsible(s);
System.out.println("Dr " + seen.whoMost + " saw " + seen.most
+ " times, out of total " + seen.total + "\n");
return seen;
}
static class Seen implements Comparable<Seen>{
int total;
int most;
String whoMost;
public Seen(int t, int m, String who) {
total = t;
most = m;
whoMost = who;
}
@Override
public int compareTo(Seen o) {
// TODO Auto-generated method stub
return whoMost.compareTo(o.whoMost);
}
}
private Seen getResponsible(String s) throws IOException,
ClassNotFoundException {
// TODO Auto-generated method stub
DBFMemoPair pairPat = dbfmap.get("PROGRESS");
List<Map<String, Object>> l = pairPat.getRecords("UR_NO", s);
TreeMap<String, Integer> countDr = new TreeMap<String, Integer>();
int total = 0;
for (Map<String, Object> m : l) {
String dr = (String) m.get("DR");
if (dr.indexOf("RECEPT") >= 0 || dr.indexOf("PRACTIT") >= 0)
continue;
++total;
Integer c = countDr.get(dr);
if (c == null)
countDr.put(dr, 1);
else
countDr.put(dr, c + 1);
}
TreeMap<Integer, String> ranking = new TreeMap<Integer, String>();
for (String d : countDr.keySet()) {
ranking.put(countDr.get(d), d);
}
Entry<Integer, String> e = ranking.lastEntry();
return new Seen(total, e.getKey(), e.getValue());
}
/**
* @param args
* @throws IOException
* @throws ClassNotFoundException
*/
public static void main(String[] args) throws IOException,
ClassNotFoundException {
// TODO Auto-generated method stub
FindDiabeticOverdue fdo = new FindDiabeticOverdue();
}
}
这个案例研究表明,“权衡”的计算机科学格言也适用于提高用于表示计算机化医疗记录的技术复杂性。人们将企业级 SQL DBMS 的高级技术功能带来的营销效益与供应商锁定、缺乏软件可扩展性以及如果供应商决定关门大吉所需的昂贵数据提取成本的风险进行权衡。可以说,如果医疗记录系统供应商没有决定移除底层数据库管理系统的所有功能,并使数据表和数据表架构的访问变得不透明,这种情况就不会发生。它表明可扩展的数据库管理只需要几页代码、一个好的算法和一个开放的计算机语言系统,从人的角度来说,可以只雇用一名本地计算机科学毕业生来无限期地提供开源服务,以及另一名毕业生作为质量保证,以确保源代码的可读性,如果当前程序员决定不再提供服务。
有人可能会争辩说,维持小型规模和家庭作坊式的普通实践计算机医疗记录状态会破坏医疗信息学标准化的需求以及电子医疗记录在初级保健和医院医疗保健之间的可移植性。但通过保持基于众所周知、易于理解但可扩展的算法的开放、可读和可扩展架构,反驳论点是,合理的简化有助于适应医疗编码的未来需求,因为软件对除少数几个主导供应商以外的所有人来说都是不透明的。