其實很多非業務邏輯的功能,比如分頁,數據過濾,可以在程序上面節省很多內存和CPU時間,但往往又找不到一個比較通用有效的方法,花了點時間,終于把我想要的在數據庫中分頁和過濾的功能寫了出來,在這里分享。
第一期望:我希望輸入頁碼(pageIndex),每頁顯示的記錄數(pageSize),然后這個存儲過程就可以給我一個當前頁的數據集。
首先,我假設有個表叫Configuration,里面就3個字段,Id, Key, Value,就是一個簡單的數據表,定義如下:
CREATE TABLE [dbo].[Configuration]( [Id] int NOT NULL IDENTITY (1,1), [Key] varchar(20) NOT NULL UNIQUE, [Value] nvarchar(max) NULL)
然后,就可以寫存儲過程了,這里想要說下,我對存儲過程命名的一些習慣,我會以"sp_"開頭,為了標明這個存儲過程是CRUD中的哪個操作,我會再附加一個縮寫,比如這個分頁的主要操作時READ,所以我的名字應該是"sp_r_",然后為了說明它的主要功能是分頁,就有了"sp_r_p_",“p”表示paginate,最后跟上操作的主要的表名,"sp_r_p_configuration"。很多同學都喜歡用比如bypage等等,也可以,但是我更喜歡這種命名法,在特定的約定下,這種簡潔性的命名可可讀性。再者,一個良好的代碼書寫規范是非常必要的,該注釋的地方千萬別省,在多數情況下,代碼的可讀性是非常重要的,除非因性能因素做必須得讓步。
/*PROCEDURE: [dbo].[sp_r_p_configuration]OperaTION: READFEATURES: PaginatePURPOSE: provide a paginated list from the configuration tableCREATOR: Jerry WengCREATETIME: 2014-4-9UPDATETIME: 2014-4-9VERSION: 1.0*/CREATE PROCEDURE [dbo].[sp_r_p_configuration] @pageIndex int = 1, /*which page of the total page*/ @pageSize int=10, /*how many records show in one page*/ @recordCount int OUTPUT, /*output: return the count of the total records in all pages*/ @pageCount int OUTPUT /*output: return the count of the total pages*/AS declare @startRow int; -- the row number of the first record in the page declare @endRow int; -- the row number of the last record in the page set @startRow = (@pageIndex - 1) * @pageSize + 1; set @endRow= @startRow + @pageSize - 1; ;with tmp as (select (ROW_NUMBER() over(order by Id)) as row_id, [Id], [Key], [Value] from dbo.Configuration) select [Id], [Key], [Value] from tmp where row_id between @startRow and @endRow -- calculate the record count in all pages select @recordCount=COUNT(*) from dbo.Configuration -- calculate the page count set @pageCount = CEILING(@recordCount/CAST(@pageSize as float))RETURN 0
這里用的ROW_NUMBER()來實現分頁,性能必拼SQL的方法好,WITH...AS可以省下一個表變量。最后計算總記錄數和頁數很簡單,算下就好了。
第二期望:這樣的,似乎差不多了,但是我還希望能夠有一套規則,來過濾結果集,并且還要有類似AND和OR的運算功能,而且我仍舊不希望用inline-script去實現,能不用盡量不要用。
為了實現這個期望,我需要先定義怎么傳入過濾規則,我最終決定用的是xml數據類型而不是普通字符串,因為普通字符串有長度限制(如果你說可以用text,那我只能說太奢侈,小弟用不起),定短了可能被截斷出現錯誤輸出,定長了,最多也就8000個字符,也浪費,我不喜歡不確定因素,然而xml不一樣,既是結構化數據,又有索引支持,可長可短,客戶端也可直接序列化成字符串傳入,非常方便。于是剩下的是,約定一個規則,確定過濾規則的數據結構,在這里我專門寫了一個function來將xml轉換成table已被后用。P.S. 如果想傳一個數組到存儲過程,也可以用xml類型,比字符串加分隔符更安全。
/*FUNCTION: [dbo].[fun_xmlfilter_parse]OPERATION: READFEATURES: PURPOSE: parse a table from a xml with the specific column nameCREATOR: Jerry WengCREATETIME: 2014-4-9UPDATETIME: 2014-4-9VERSION: 1.0*/CREATE FUNCTION [dbo].[fun_xmlfilter_parse]( @filter xml, /* The format should be: <filter> <rule column="COLUMN_NAME" -- the column name which the rule should be applied match="xxx" -- the key which the rule defines to filter and the result should match required="0|1" -- if 1, the result collection has to meet this rule or return nothing /> <rule .... </filter> */ @column varchar(100))RETURNS @returntable TABLE ( [Match] nvarchar(100), [Required] bit)ASBEGIN ;with tmp as ( select [T].[RULE].value('@column','varchar(100)') as [Column], [T].[RULE].value('@match','nvarchar(100)') as [Match], [T].[RULE].value('@required','bit') as [Required] from @filter.nodes('filter//rule') [T]([RULE]) ) INSERT @returntable select [Match],[Required] from tmp where [Column] = @column RETURN END
xml的結構由一個根節點<filter>開始,里面是多個<rule>節點,具體的規則定義在rule的屬性列表中。column表示具體要過濾哪列的數據,match表示要匹配的數據值,比如我想要過濾列"Value"下,值為"sample"的數據,這樣的話我只需要把xml寫成<filter><rule column="value" match="sample"required="1"/></filter>,最后的哪個required就是來實現AND和OR的操作的,如果是1表示必須匹配,類似AND,0的話可選匹配,類似OR,輸出的表示由COLUMN的值過濾過的,因為比較是以COLUMN為維度的。
最后,新建一個存儲過程,叫"sp_r_fp_configuration",這里比上一個多了一個"f",表示帶有過濾功能。
/*PROCEDURE: [dbo].[sp_r_fp_configuration]OPERATION: READFEATURES: Paginate, FilterPURPOSE: provide a paginated & filterable list from the configuration tableCREATOR: Jerry WengCREATETIME: 2014-4-9UPDATETIME: 2014-4-9VERSION: 1.0*/CREATE PROCEDURE [dbo].[sp_r_fp_configuration] @pageIndex int = 1, /*which page of the total page*/ @pageSize int=10, /*how many records show in one page*/ @filter xml, /*filter string for the records*/ @recordCount int OUTPUT, /*output: return the count of the total records in all pages*/ @pageCount int OUTPUT /*output: return the count of the total pages*/AS if(@filter is null) begin exec dbo.sp_r_p_configuration @pageIndex, @pageSize, @pageCount=@pageCount OUTPUT, @recordCount=@recordCount OUTPUT end else begin declare @tmptbl table (row_id int, Id int, [Key] varchar(100), [Value] varchar(200)) declare @startRow int; -- the row number of the first record in the page declare @endRow int; -- the row number of the last record in the page set @startRow = (@pageIndex - 1) * @pageSize + 1; set @endRow= @startRow + @pageSize - 1; insert into @tmptbl select (ROW_NUMBER() over (order by m.Id)) as row_id, m.[Id], m.[Key], m.[Value] from dbo.Configuration as m outer apply fun_xmlfilter_parse(@filter,'key') as k outer apply fun_xmlfilter_parse(@filter,'value') as v where ( not (isnull(k.[Required],0)=0 and isnull(v.[Required],0)=0) and (isnull(k.[Required],0)=0 or (k.[Required] = 1 and isnull(m.[Key],'') = isnull(k.[Match],''))) and (isnull(v.[Required],0)=0 or (v.[Required] = 1 and isnull(m.[Value],'') = isnull(v.[Match],''))) ) or k.[Required] = 0 and isnull(m.[Key],'') = isnull(k.[Match],'') or v.[Required] = 0 and isnull(m.[Value],'') = isnull(v.[Match],'') select [Id], [Key], [Value] from @tmptbl where row_id between @startRow and @endRow -- calculate the record count in all pages select @recordCount=COUNT(*) from @tmptbl -- calculate the page count set @pageCount = CEILING(@recordCount/CAST(@pageSize as float)) endRETURN 0
這里用了一個表變量,注意表變量是可被緩存的,臨時表沒有哦,用表變量不用with...as是因為后面在計算過濾后的總記錄數的時候,還需用一次過濾后的集合。這里我希望列Key和列Value都可以被過濾,于是用了兩個outer apply來將剛才那個函數的輸出表附加到每行上,在where比較的地方,先比較Required是1的必要條件,當然如果針對某個column沒有rule的話,也就是required is null的情況,是需要排除的,最后再附加連個required=0的可選條件,這樣的話,一個select就可以把各種條件過濾出來了。不過,有個問題是,如果需要過濾的列比較多,那比較語句也要一條條加上去。
最后,來測試下:
我給表填充了這么些數據:
SET IDENTITY_INSERT [dbo].[Configuration] ONINSERT [dbo].[Configuration] ([Id], [Key], [Value]) VALUES (1, N'test1', N'123')INSERT [dbo].[Configuration] ([Id], [Key], [Value]) VALUES (2, N'test2', N'123')INSERT [dbo].[Configuration] ([Id], [Key], [Value]) VALUES (3, N'test3', N'567')INSERT [dbo].[Configuration] ([Id], [Key], [Value]) VALUES (4, N'test4', NULL)INSERT [dbo].[Configuration] ([Id], [Key], [Value]) VALUES (5, N'test5', NULL)INSERT [dbo].[Configuration] ([Id], [Key], [Value]) VALUES (6, N'test6', NULL)INSERT [dbo].[Configuration] ([Id], [Key], [Value]) VALUES (7, N'test7', NULL)INSERT [dbo].[Configuration] ([Id], [Key], [Value]) VALUES (8, N'test8', NULL)INSERT [dbo].[Configuration] ([Id], [Key], [Value]) VALUES (9, N'test9', NULL)INSERT [dbo].[Configur
新聞熱點
疑難解答