.NET平台開源項目速覽(7)關於NoSQL數據庫LiteDB的分頁查詢解決過程


  在文章:這些.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。這里研究還不夠深入,代碼比較簡單,就省略了吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM