實現一個小型資料庫--記一次中級軟體設計實作浮沉歷程
說在前頭:本篇文章主要是記錄這次專案的過程,不全是貼程式碼,具體的程式移步這裡,不喜歡的同學請輕噴。
事件起因:大三狗一枚,專業是軟體應用。這學期的中級軟體設計實作題目是實現一個小型的資料庫,具體的題目要求如下:
建立一個類似Oracle\SQL Server的小型資料庫系統:
- 可建立表,表有欄位、型別
- 表中可通過SQL語句填入資料(只能是特定型別)和查詢資料;
- 對主鍵欄位,應自動建立索引;
- 當進行查詢時,在主鍵上應是基於索引的高效查詢,而不能是字串匹配這樣的原始查詢。
覺得題目似曾相識的同學,應該猜到我是哪所大學的了吧。一番Google、百度下來,最終確定了實現成一個Android的應用,畢竟這學期剛學的Android,這知識還滾燙著呢,而且Android的各種框架已經很成熟了,不用重複造輪子了。
前期準備:既然需求已經如此明確,接下來就是進行介面設計。然而自己並沒有高大上的審美,也沒有美術的基礎,所以並不敢有太高的要求,但是業務邏輯要通順,其他只能說長得不醜就行了。我用的是Mac系統下的一個Sketch的軟體繪製的原型圖,在這裡就不貼出來了,具體的應用介面下面有。
接下來就是技術準備了,實現一個小型資料庫,我總結了以下幾點主要的技術難點:
- 其次也是最難的部分是B+樹,當前的很多資料庫系統都是用B+樹。關於B+樹的總結,可以參看我的另一篇部落格。
- 最後就是Android的相關知識。這個需要長期的積累,遇到不會的我都會Google,或者檢視技術部落格,stackoverflow,github等等也有很多輪子可以直接用。
實際編碼:終於來到真正的打大怪階段了。這一部分內容我直接按介面(Activity)進行劃分。
- 主介面(Main Activity):首先是介面展示
主介面上就是所有已經建好的表的列表,點選列表項則彈出對話方塊進行刪除表、新增資料、查詢資料操作,這三個操作具體實現在後面會詳細說;其次右上角是新建表的按鈕,點選跳轉到對應的介面。主介面邏輯都很簡單,值得一提的是重新整理列表的操作我是放在onResume函式中,所以每次進入主介面就會重新整理列表。程式碼片段如下:
@Override
protected void onResume()
{
super.onResume();
refreshSharedPreferencesList(); // 重新整理列表
}
- 新建表介面(AddTable Activity):介面展示如下:
介面上一個是編輯框輸入欄位名,一個是對應資料型別的下拉列表,填好一個欄位資訊後按右上角的”+”按鈕再生成一對新的欄位名框和型別列表,所有的編輯框和下拉列表儲存在ArrayList中,且預設第一個是主鍵,最後按下面的確定按鈕新建表。
考慮到資料量小,而且是鍵值對應的形式,我將表的資訊儲存SharedPreference中,以使用者輸入的表名命名該檔案。相關程式碼如下:
private List<EditText> editTextList = new ArrayList<>();
private List<Spinner> spinnerList = new ArrayList<>();
// 將欄位名和對應的型別儲存到SharedPreferences中
private void saveInSharedPreferences(String tableName)
{
SharedPreferences.Editor editor = getSharedPreferences(tableName, MODE_PRIVATE).edit();
int index = -1;
for (EditText editText : editTextList) {
index++;
String fieldName = editText.getText().toString();
Spinner spinner = spinnerList.get(index);
String type = spinner.getSelectedItem().toString();
editor.putString(String.valueOf(index), fieldName+"&"+type);
}
editor.commit();
}
因為SharedPreference內部是用HashMap儲存的,所以欄位是無序的,為了取出的時候保持有序我只能將所以的鍵依次設為遞增的整數,值通過’&’符號複合字段名和對應的型別,等取出的時候在解析出來。另外,剛剛主介面有刪除表的操作,現在可以補上具體操作:找出SharedPreference儲存的位置(haredPreferences 檔案都是存放在/data/data/packagename/shared_prefs/目錄下的),根據檔名刪除對應的檔案即可,程式碼如下:
// 刪除表
private void deleteTableAction(String tableName)
{
File prefsdir = new File(getApplicationInfo().dataDir,"shared_prefs");
if(prefsdir.exists() && prefsdir.isDirectory()){
String[] list = prefsdir.list();
for (String fileNameWithEx : list) {
String fileName = getFileNameNoEx(fileNameWithEx);
if (fileName.equals(tableName)) {
File deleteFile = new File(prefsdir, fileName+".xml");
deleteFile.delete();
refreshSharedPreferencesList(); //重新整理列表
}
}
}
}
- 插入介面(ExecSQL Activity):國際慣例展示介面先:
考慮到插入介面和搜尋介面都是輸入SQL語句然後執行操作,所以共用了同一個介面(ExecSQL Activity),下面不再說明。因為輸入插入語句然後按”執行”按鈕然後在當前介面並不能立刻進行查詢操作,同時需要永久儲存插入的資料,所以考慮將所有的插入語句儲存到檔案中,然後在需要執行查詢的時候在取出插入語句插入到B+樹上,然後進行查詢。相關程式碼如下:
// 將插入語句寫進檔案中
private void writeInsertSqlToFile(String fileName)
{
FileOutputStream out = null;
BufferedWriter writer = null;
try {
out = openFileOutput(fileName, Context.MODE_APPEND); // 追加模式
writer = new BufferedWriter(new OutputStreamWriter(out));
String insertSQLStatement = sqlEdit.getText().toString();
String[] lines = insertSQLStatement.split(System.getProperty("line.separator")); // 按行擷取
int length = lines.length;
// 將所有的插入語句寫入檔案中
for (int i = 2; i < length; i++) {
String oneLine = lines[i];
//Todo: 此處應該檢查每一行資料的正確性
writer.write(oneLine);
writer.newLine(); // 換行
Log.d("write oneLine", oneLine);
}
sqlEdit.setText(null);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (writer != null) {
writer.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 查詢介面(ExecSQL Activity):介面展示如下:
查詢的時候先取出where語句,然後解析出查詢的時候具體的限定條件,具體程式碼如下:
// 從表中查詢資料
private void queryFromTable()
{
BPlusTree tree = new BPlusTree(tableName, BPLUSTREE_DEGREE);
String insertSql = readInsertSqlFromFile(tableName);
// 將資料逐條插到B+樹上
if (insertDataInTree(tree, insertSql)) {
String querySql = sqlEdit.getText().toString();
String[] twoStr = analyseQuerySqlStatement(querySql);
ArrayList<String> resultList = new ArrayList<String>();
if (twoStr[0].equals(fieldNameArray.get(0))) {
// 查詢主鍵
Object result = tree.search(twoStr[1]);
if (result != null) {
String resultString = formatResultMapToString((Map) result, twoStr[1]);
resultList.add(resultString);
}
} else {
// 查詢其他屬性
Node head = tree.getHead();
resultList = traverseTreeToSearch(head, twoStr);
}
}
}
取出查詢結果存放到ArrayList中,跳轉到查詢結果介面並putExtra把查詢結果傳過去。
- 查詢結果介面(QueryResult Activity):最後就是展示查詢的結果了:
這裡只需展示查詢結果到列表中即可。
後記:當做完整個專案之後回頭看看,似乎做了什麼了不得的事,又似乎什麼都沒有做的樣子。通過這次浮沉歷程,我深刻體會到做好前期準備是多麼的重要,一遍遍改資料結構、資料型別是很痛苦的……還有一點體會就是應該把model層(MVC框架)寫的健壯,具有魯棒性,得益於我花了4、5天的B+樹,我後期的時候只管組織好介面等其他部分即可,不用擔心我的B+樹會鬧彆扭。看來設計模式果然灰常重要的,《大話設計模式》、《設計模式之禪》……你們等著我,我交完這次作業就過來找你們!!!
PS: 我將這次的完整程式放在了這裡,這是初期版本,可能會有更新,下了初期版本想要更新版本的可以私信我。也歡迎各位大牛拍磚,你們的意見是我前進最好的指導,謝謝。