最近发现了一个来自伯克利大学的数据库作业,看起来很不错,所以打算认认真真的做完。
homework链接为:https://sites.google.com/site/cs186fall2013/homeworks
构建工具
作业使用ant构建工具进行构建,但我觉得使用Maven工具更好一点。
导入本地jar包
项目中涉及到了一些jar包,既可以采用远程让Maven自己下载安装的方式,也可以使用本地安装的方式。比如对于jar包:Zql,这个包我在Maven的中心仓库中找不到(可能是我没发现),所以我采用本地安装的方式。
使用Maven命令如下:mvn install:install-file -Dfile=/home/chenyu/java_jar/zql.jar -DgroupId=chenyu -DartifactId=Zql -Dpackaging=Zql -Dversion=1.0.0 -Dpackaging=jar
其中 -DgroupId,-DartifactId,-Dpackaging,-Dversion参数为必需输入的参数,可以根据自己的设置进行设定。
-Dile 参数代表着本地的jar包路径,需要注意的是必须要有 -Dpackaging 参数 。
然后在pom.xml中引入包:
开发环境
编写程序我选择了vscode+java的环境,个人觉得idea有点笨重,vscode更为的灵活一点。
project1 中涉及到的知识很少,没有涉及到并发的部分,所以一些基本的java知识足以应对,而且对于自己对面向接口编程的思维的理解大有帮助。
类的理解
Tuple: 元组。每一个元祖包含一行数据,是构成数据库数据的基本单元。Tuple中包含Field,每一个Field代表一个具体的数据,Field是一个接口,项目中StringField和IntegerField实现了该接口,说明数据类型有String和int两种。Tuple可以用HaspMap或者是Array来对Field进行索引。
TupleDesc:元组描述。顾名思义,用于描述元组。这个类对于Tuple中的每一个数据,有Type和FieldName两种描述,分别描述类型和列名。TupleDesc中将两个描述封装成了TDItem内部类。
Page: 页。存储和读取的基本单位。是一个抽象的接口。项目中使用HeapPage这个类实现了该接口并用于具体的实现。每一个Page中都有一个header,是一个字节数组。Page是由一系列的slot组成的(slot个人理解其实就是tuple)。header中的每一位代表某一个slot是否有tuple。比如:如果header是10010,代表第1个slot和第3个slot存储着tuple,但是其他slot没有tuple,只是一个空的slot。所以每一个tuple需要多余的一个bit的来存储。
所以一个页能存储的tuple数量为:
tupsPerPage = floor((BufferPool.PAGE_SIZE * 8) / (tuple size * 8 + 1))
计算出tuplesPerPage之后,我们就知道了需要用多少个字节来存储header,header需要向上取整,因为字节数组要取整数。
headerBytes = ceiling(tupsPerPage/8)
每一个Page都有一个唯一的索引用来索引这个页,这个索引被封装成了一个类:PageId,所以PageId类需要override hashCode函数。
Page需要实现一个Iterator函数,用于依次返回这个Page中的每一个Page,即header对应位为1的slot中存储的tuple。
HeapFile: 文件。实现了DFile接口。封装了涉及文件读取的操作。与文件相关的大部分内容已经由教授们实现。仅需要实现readPage等几个函数。readPage代表从文件中读取一个页(根据PageId)。具体实现如下:
public Page readPage(PageId pid){
byte[] buf = new byte[BufferPool.PAGE_SIZE];
Page wantedPage = null;
try{
InputStream is = new FileInputStream(f);
//skip to get the wanted data according to pageid
/*
is.skip() method can skip the specific bytes from file head,
so, we use skip method to make offset from head.
*/
int offset = pid.pageNumber() * BufferPool.PAGE_SIZE;
if(offset > 0) is.skip(offset);
//read data
is.read(buf);
wantedPage = new HeapPage((HeapPageId)pid,buf);
is.close();
}catch (IOException e){
//throw new IOException("fail read page!");
e.printStackTrace();
}
return wantedPage;
}
同样的,HeapFile也需要实现Iterator函数,将HeapFile中存储的所有页中的每一个tuple的按照顺序进行迭代,读取页需要通过BufferPool的getPage函数,这是是BufferPool的缓存意义所在。读取出每一个页之后,就可以使用page的Iterator方法来进行迭代,具体实现上比较灵活,因为只要返回正确的Tuple顺序即可。
BufferPool: 缓冲区。用来缓存已经从硬盘读取到内存中的页(page)。并且指明了page的大小和缓冲区的大小。BufferPool中需要设置已经缓存的page的数据结构,我使用的是HashMap。BufferPool中需要实现替换算法,即Instruction中提到的eviction policy,思想和操作系统中的页面的换入换出策略思想应该相同(揣测),但在Project1中不需要实现既可以通过测试。
public Page getPage(TransactionId tid, PageId pid, Permissions perm)
throws TransactionAbortedException, DbException {
Page tempPage = pageBuffers.get(pid);
if(tempPage != null){
return tempPage;
}else{
HeapFile file = (HeapFile)Database.getCatalog().getDbFile(pid.getTableId());
Page pageRead = (HeapPage)file.readPage(pid);
if (pageBuffers.size() == MAX_CAPACITY){
//eviction policy
throw new DbException("can't reach");
}else{
pageBuffers.put(pid,pageRead);
}
return pageRead;
}
}
RecordID: 注释中解释的非常清楚,用来索引每一个table中的每一个page中的每一个tuple。注意有一个override hashCode,用于实际的索引,我是通过用page的索引在加上tuple的序号来实现的。
@Override
public int hashCode() {
// some code goes here
//throw new UnsupportedOperationException("implement this");
return pid.hashCode()+tupleno;
}
Catalog: 用于记录数据库中的Table。起到一个记录的作用,Table和HeapFile的关系有点不太理解透彻,我的理解是Table中有一个DFile的成员变量,说明了一个Table包含一个File,如果是一个DFile数组,那关系就是一个Table中的数据存储在了多个File中。
Table需要用两个元素来进行索引,一个是Table的name,另一个是Table的id。这两个索引我都使用HashMap来实现。
SeqScan:用于扫描一个Table的所有数据。即起到select * from table 的效果。SeqScan需要实现DbIterator接口,可以调用DFile的Iterator来实现。同时SeqScan需要返回一个具有可读性的列名,即通过getTupleDesc()函数。
public TupleDesc getTupleDesc() {
// some code goes here
TupleDesc tempDesc = Database.getCatalog().getTupleDesc(tableId);
int numFields = tempDesc.numFields();
Type[] tempTypes = new Type[numFields];
String[] tempNames = new String[numFields];
//这里的目的是将tupledesc的显示变得更加的可读
for(int i = 0; i < numFields; i++){
String prefix = tableAlias == null ? "null." : tableAlias + ".";
String suffix = tempDesc.getFieldName(i) == null ? "null" : tempDesc.getFieldName(i);
tempNames[i] = prefix + suffix;
}
return new TupleDesc(tempTypes, tempNames);
}
结果验证
测试结果
使用mvn test命令进行测试,可以指定测试的类。比如:
mvn test -Dtest=testClass
放一个我的测试结果:
简单查询实现
按照Instruction的指导,新建一个test类,然后复制代码进行一个简单的查询。
首先先转化一个负数据库文件,使用命令如下:
java -classpath .\target\classes simpledb.SimpleDb convert some_data_file.txt 3
注意我使用的是classpath参数指定的执行目录,classpath用于指定一个具体的javaclass执行目录,注意执行命令的路径:
我找到一篇博客是对这个classpath的解释的博客,具体可以看这里:
https://blog.csdn.net/bluishglc/article/details/9972951
执行了命令之后,就会出现一个some_data_file.dat二进制文件,存储的二进制数据。
之后我们执行test类,就会在终端显示出想要的结果了。