在文章:這些.NET開源項目你知道嗎?讓.NET開源來得更加猛烈些吧!(第二輯) 與 .NET平台開源項目速覽(3)小巧輕量級NoSQL文件數據庫LiteDB中,介紹了LiteDB的基本使用情況以及部分技術細節,我還沒有在實際系統中大量使用,但文章發布后,有不少網友( loogn)反應在實際項目中使用過,效果還可以吧。同時也有人碰到了關於LiteDB關於分頁的問題,還不止一個網友,很顯然這個問題從我的思考上來說,作者不可能不支持,同時也翻了一下源碼,發現Find方法有skip和limite參數,直覺告訴我,這就是的。但是網友進一步提問,這個方法並不是很好用,它也沒有實現的分頁的情況。所以就親自操刀,看看到底是神馬情況?不看不知道,這個過程還真的不是那么回事,不過還是能解決啊。
.NET開源目錄:【目錄】本博客其他.NET開源項目文章目錄
本文原文地址:.NET平台開源項目速覽(7)關於NoSQL數據庫LiteDB的分頁查詢解決過程
1.關於數據庫排序與分頁
在實際的項目中,對於關系型數據庫,數據查詢與排序都應該好辦,升序或者降序唄,但是對數據庫的分頁應該不是直接的函數支持,也需要自己的應用程序中進行處理,然后使用top或者limite之類的來查詢一定范圍內的數據,作為一頁,給前台。例如下面的SQL語句:
Select top PageSize * from TableA where Primary_Key not in (select top (n-1)*PageSize Primary_Key from TableA )
數據的分頁過程中,我們也看到在根據指定條件查詢后,就是記錄集的篩選,所以對於NoSQL數據庫來說,因為沒有了SQL,這些問題不會像常規關系型數據庫那么突出,畢竟你選擇了NoSQL,在大數據面前,如果動不動就查幾千條數據來分頁,也是明顯不合適的。在我的觀點中,要盡量避免無謂的查詢浪費,也不會有人專門去看幾千甚至幾萬條記錄,如果有,也只是從中找到一部分數據,既然這樣何必不一開始就增加條件,過濾掉那些沒用的數據呢。所以數據庫的那些事,業務的合理性也很重要,數據庫也是機器,他們能力也有限,動不動就仍那么多沉重的任務給它,也會受不了啊。
2.LiteDB的查詢排序
2.1 測試前准備工作
為了便於本文的相關代碼演示,我們使用如下的一個實體類,注意Id的問題我們在前面一篇文章中已經說過了,默認是自增的,不需要處理。加進來是為了方便查詢和分頁。實體類基本代碼如下:
public class Customer { /// <summary>自增Id,編號</summary> public int Id { get; set; } /// <summary>年齡</summary> public int Age { get; set; } /// <summary>姓名</summary> public string Name { get; set; } }
然后我們使用如下的方法插入20條記錄,注意該函數是數據初始化,只需要運行一次即可。會在bin目錄生成Sample數據庫文件。我們只拿這些數據做測試。至於以后大數據的查詢以及分頁效率問題,暫時不考慮,我們只單獨處理分頁的情況。
static void InitialDB() { //打開或者創建新的數據庫 using (var db = new LiteDatabase("sample.db")) { //獲取 customers 集合,如果沒有會創建,相當於表 var col = db.GetCollection<Customer>("customers"); for (int i = 0; i < 20; i++) { //創建 customers 實例 var customer = new Customer { //名字循環改變 Name = i % 2 == 1 ? "Jim1_" + i.ToString() : "Jim2" + i.ToString(), Age = i, }; // 將新的對象插入到數據表中,Id是自增,自動生成的 col.Insert(customer); } } }
上面的Name是交替改變的,Jim1和Jim2加上編號,而Age是默認逐步增加了,主要是為了測試排序的情況。
2.2 基本查詢與分頁問題
我們在前面介紹LiteDB的基礎文章。。中,對基本查詢做了介紹。方法很靈活。針對上面的例子,我們假設一個查詢分頁的需求:
查Customer表中,Name以"Jim1"開頭的人集合,按Age降序排列,每3條記錄一頁,打印每一頁的Age列表。
針對上面問題,我們需要先簡單分析一下問題:
1.查詢獲取記錄的總數,可以使用Find或者Count方法直接獲取;
2.查詢條件的是Name,可以使用Linq或者Query來進行;
3.由於LiteDB是NoSQL的,所以不支持內部直接排序了,只能使用Linq的OrderBy或者OrderByDescending了;
4.關於分頁,還是選擇和SQL數據庫類型的方法,使用linq的skip方法來跳過一些記錄。這里留個疑問,因為自己技術有限,平時也只使用基本的linq操作,所以只想到了Skip,知道的朋友接着往下看,別吐槽。解決問題的最終結果可能很簡單,但是過程還是值得回味的,一步步也是學習和總結優化的過程。
3.LiteDB分頁之漸入佳境
由於Linq的Take以前不知道,所有走了一些彎路,同時LiteDB的Find方法中的重載函數之一,skip參數也有一些問題,下一節講到具體問題。
3.1 第一次小試牛刀
考慮到類似SQL的limite和top查詢,我們也在LiteDB中使用這種方式。由於Linq有一個Skip方法,所以選擇它來完成具體數據的選擇,相當於每次都選擇最后幾條。看代碼:
//打開或者創建新的數據庫 using (var db = new LiteDatabase("sample.db")) { //獲取 customers 集合,如果沒有會創建,相當於表 var col = db.GetCollection<Customer>("customers"); //1.計算總的數量 var totalCount = col.Count(Query.StartsWith("Name", "Jim1")); //2.計算總的分頁數量 Int32 pageSize = 3 ;//每一頁的數量 var pages = (int)Math.Ceiling((double)totalCount / (double)pageSize); //3.循環獲取每一頁的數據 Int32 current = int.MaxValue; for (int i = 0; i < pages; i++) { //查找條件,附加了Id的范圍,第一次是最大,后面進行更新 var data = col.Find(n => n.Name.StartsWith("Jim1") && n.Id < current) .OrderBy(n => n.Age) //要求是降序,由於要選擇最后的,只能先升序 .Skip(totalCount - (i + 1) * pageSize)//跳過前面頁的記錄 .OrderByDescending(n => n.Age); //降序排列 current = data.Last().Id;//更新當前查到的最大Id //把Id按照頁的順序打印出來 String res = String.Empty; foreach (var item in data.Select(n => n.Age)) res += (item.ToString() + " , "); Console.WriteLine(res); } }
結果如下:
最后1也只有1條記錄,總共10條記錄也是正常的,總共20條,交替插入的。缺點有幾個:
1.效率比較低,每次都選最后的
2.只能從第1頁獲取,不能獲取單獨頁的,因為上一次的Id不能得到
3.2 完全使用Linq分頁
后來發現了Take方法,雖然我猜測應該有,但苦於自己疏忽,導致尋找的時候錯過了,后來自己打算重新寫一個的時候,又去確認一遍的時候才發現。因為skip都可以實現,沒道理Take不實現啊,原理都是一樣的。如果實現也很簡單的。那看看改進版的基於Linq的分頁。沒有上面那么麻煩了:
//根據頁面號直接獲取 static void SplitPageByPageIndex(int index) { using (var db = new LiteDatabase("sample.db")) { var col = db.GetCollection<Customer>("customers"); //1.計算總的數量 var totalCount = col.Count(Query.StartsWith("Name", "Jim1")); //2.計算總的分頁數量 Int32 pageSize = 3;//每一頁的數量 var pages = (int)Math.Ceiling((double)totalCount / (double)pageSize); //查詢條件 var data = col.Find(n => n.Name.StartsWith("Jim1")) .OrderByDescending(n => n.Age)//降序 .Skip(index * pageSize) //跳過前面頁數數量的記錄 .Take(pageSize); //選擇前面的記錄作為當前頁 //把id按照順序打印出來 String res = String.Empty; foreach (var item in data.Select(n => n.Age)) res += (item.ToString() + " , "); Console.WriteLine(res); } }
結果如下:
和上面是一樣的,但這個顯然要簡潔多了。更加靈活,而且不用降序和升序直接轉換,一次就夠。
3.3 終極解決之擴展分頁方法
根據上面方法,我們可以擴展到LiteDB中去,雖然我一直認為這一點可以做到,但是研究了很久的源碼,測試一直不成功,詳細內容第4節介紹。
我選擇直接在源代碼里面擴展,當然也可以單獨寫一個擴展方法,不過源碼里面更好用,相當於給Find增加一個重載方法,我們在源代碼的Find.cs中增加下面的方法,詳細看注釋:
/// <summary>分頁獲取記錄</summary> /// <typeparam name="TOder">排序字段類型</typeparam> /// <param name="predicate">linq查詢表達式</param> /// <param name="orderSelector">排序表達式</param> /// <param name="isDescending">是否降序,true降序</param> /// <param name="pageSize">每頁大小</param> /// <param name="pageIndex">要獲取的頁碼,從1開始</param> /// <returns>分頁后的數據</returns> public IEnumerable<T> FindBySplitePage<TOder>(Expression<Func<T, bool>> predicate, Func<T, TOder> orderSelector, Boolean isDescending, int pageSize, int pageIndex) { var allCount = Count(predicate);//計算總數 var pages = (int)Math.Ceiling((double)allCount / (double)pageSize);//計算頁碼 if (pageIndex > pages) throw new Exception("頁面數超過預期"); if (isDescending)//降序 { return Find(predicate) .OrderByDescending(orderSelector) .Skip((pageIndex - 1) * pageSize) .Take(pageSize); } else //升序 { return Find(predicate) .OrderBy(orderSelector) .Skip((pageIndex - 1) * pageSize) .Take(pageSize); } }
下面還是使用上面的例子,直接進行調用:
var db = new LiteDatabase("sample.db"); var col = db.GetCollection<Customer>("customers"); //取第二頁,降序 var data = col.FindBySplitePage<Int32>(n => n.Name.StartsWith("Jim1"), n => n.Age, true, 3, 2).ToList(); //把id按照順序打印出來 String res = String.Empty; foreach (var item in data.Select(n => n.Age)) res += (item.ToString() + " , "); Console.WriteLine(res); Console.WriteLine("任務完成");
結果如下,調用總體比較簡單,直接使用linq,輸入頁面數量和頁碼就可以了。當然不需要排序也可以,大家可以根據實際情況優化一下。
到這里,分頁的問題基本是解決了,但還得說一下研究LiteDB遇到的坑。
4.LiteDB的疑問
先看看下面一段普通的代碼,查詢出來的記錄的Id的變化情況,沒有排序:
using (var db = new LiteDatabase("sample.db")) { var col = db.GetCollection<Customer>("customers"); var data = col.Find(n => n.Name.StartsWith("Jim1"));//普通查詢 //把Id按照頁的順序打印出來 String res = String.Empty; foreach (var item in data.Select(n => n.Id)) res += (item.ToString() + " , "); Console.WriteLine(res); }
結果如下:
2 , 12 , 14 , 16 , 18 , 20 , 4 , 6 , 8 , 10 ,
是不是很奇怪?沒有想象的是按照順序輸出。所以這個坑花了我好長時間,怎么試就是不行,既然這樣的話,那么使用LiteDB自帶的下面這個方法:
public IEnumerable<T> Find(Expression<Func<T, bool>> predicate, int skip = 0, int limit = int.MaxValue)
就有問題。這個方法skip的是按照上述順序的。所以追根到底,還是因為直接的使用排序的方法?這里打個問號吧,說不定有,我沒找到。如果有人比較熟悉的,可以告知一下,非常感謝。但是使用linq的方式也很容易的解決問題,應該差不了多少。
5.資源
本文的代碼比較簡單,所有代碼都已經貼在上面了。所以就不放具體代碼了,我打算好好把LiteDB的源碼研究一下,為以后正式的拋棄Sqlite做准備。大家關注博客,如果研究比較深入,會把相關代碼托管到github。這里研究還不夠深入,代碼比較簡單,就省略了吧。