相信很多同學都用過SubSonic,在07 - 10年ORM興起的時代,SubSonic可以說是DotNet開發人員的救星。雖說現在 EntityFramework大有一統江湖的趨勢,不過在DotNet2.0框架下,SubSonic依然是為數不多的選擇。
最近在維護基於 ExtAspNet 的通用權限管理項目 AppBox ,在使用SubSonic進行多表查詢和數據庫分頁時遇到了點問題,下面我會詳細分享這一經過,以及如何通過修改SubSonic的源代碼來修正這一問題。
我要實現如下的功能
我要實現的功能非常簡單:用戶管理,角色管理,角色用戶管理(一個用戶可以屬於多個角色)。相信很多同學閉着眼睛就能把數據庫給構造出來,不是嗎?
1. 用戶表
2. 角色表
3. 角色用戶表
其中用戶管理和角色管理都很簡單,我要實現的角色用戶管理界面如下所示:
1. 查看角色下的所有用戶
2. 向角色添加現有用戶
數據庫查詢時遇到問題
在查看角色下的所有用戶頁面,需要進行表關聯,相關的SubSonic代碼如下所示:
1: // 查詢 X_User 表
2: SqlQuery q = new Select().From<XUser>();
3: q.Where("1").IsEqualTo("1");
4:
5: // 在用戶名稱中搜索
6: string searchText = ttbSearchUser.Text.Trim();
7: if (!String.IsNullOrEmpty(searchText))
8: {
9: q.And(XUser.NameColumn).ContainsString(searchText);
10: }
11:
12: // 過濾選中角色下的所有用戶
13: object[] values = Grid1.DataKeys[Grid1.SelectedRowIndexArray[0]];
14: int roleId = Convert.ToInt32(values[0]);
15: SqlQuery subQ = new Select(XRoleUser.UserIdColumn).From<XRoleUser>().Where(XRoleUser.RoleIdColumn).IsEqualTo(roleId);
16:
17: q.And(XUser.IdColumn).In(subQ);
18:
19:
20: // 在查詢添加之后,排序和分頁之前獲取總記錄數
21: // Grid1總共有多少條記錄
22: Grid2.RecordCount = q.GetRecordCount();
23:
24: // 排列
25: q.OrderBys.Add(GetSortExpression(Grid2, XUser.Schema));
26:
27: // 數據庫分頁
28: q.Paged(Grid2.PageIndex + 1, Grid2.PageSize);
29: items = q.ExecuteAsCollection<XUserCollection>();
令人不解的時,居然報如下錯誤:
很明顯,SubSonic生成的SQL腳本不對,經過調試發現生成的腳本如下所示:
1: DECLARE @Page int
2: DECLARE @PageSize int
3:
4: SET @Page = 1
5: SET @PageSize = 20
6:
7: SET NOCOUNT ON
8:
9: -- create a temp table to hold order ids
10: DECLARE @TempTable TABLE (IndexId int identity, _keyID Int)
11:
12: -- insert the table ids and row numbers into the memory table
13: INSERT INTO @TempTable
14: (
15: _keyID
16: )
17: SELECT [dbo].[X_User].[Id]
18: FROM [dbo].[X_User]
19: WHERE 1 = @10
20: AND [dbo].[X_User].[Id] IN (SELECT [dbo].[X_RoleUser].[UserId]
21: FROM [dbo].[X_RoleUser]
22: WHERE [dbo].[X_RoleUser].[RoleId] = @RoleId0
23: )
24:
25: AND 1 = @10
26: AND [dbo].[X_User].[Id] IN (SELECT [dbo].[X_RoleUser].[UserId]
27: FROM [dbo].[X_RoleUser]
28: AND [dbo].[X_RoleUser].[RoleId] = @RoleId0
29: )
30:
31: ORDER BY Name DESC
32:
33: -- select only those rows belonging to the proper page
34: SELECT [dbo].[X_User].[Id], [dbo].[X_User].[Name], [dbo].[X_User].[Password], [dbo].[X_User].[Enabled], [dbo].[X_User].[Email], [dbo].[X_User].[Gender], [dbo].[X_User].[RealName], [dbo].[X_User].[QQ], [dbo].[X_User].[MSN], [dbo].[X_User].[CellPhone], [dbo].[X_User].[OfficePhone], [dbo].[X_User].[HomePhone], [dbo].[X_User].[Remark], [dbo].[X_User].[DeptId], [dbo].[X_User].[RoleId], [dbo].[X_User].[CreateTime]
35:
36: FROM [dbo].[X_User]
37: INNER JOIN [dbo].[X_RoleUser] ON [dbo].[X_User].[Id] = [dbo].[X_RoleUser].[UserId]
38:
39: INNER JOIN @TempTable t ON [dbo].[X_User].[Id] = t._keyID
40: WHERE t.IndexId BETWEEN ((@Page - 1) * @PageSize + 1) AND (@Page * @PageSize)
這里面有兩處錯誤:
1. 首先,25 – 29 行的Where子句重復了,相信這個問題一直存在於SubSonic2.2中,只不過大家都沒發現而已
2. 其次,重復的子查詢中Where被替換成了AND,導致這個子查詢沒有Where子句,從而報錯!
如果撇去分頁的SQL腳本不管,正確的SQL腳本應該是這樣的:
1: SELECT [dbo].[X_User].[Id]
2: FROM [dbo].[X_User]
3: WHERE 1 = @10
4: AND [dbo].[X_User].[Id] IN (SELECT [dbo].[X_RoleUser].[UserId]
5: FROM [dbo].[X_RoleUser]
6: WHERE [dbo].[X_RoleUser].[RoleId] = @RoleId0
7: )
8:
9: ORDER BYName DESC
很明顯,SubSonic在生成帶子查詢的分頁SQL腳本時除了問題。
修改SubSonic的源代碼
從Github下載SubSonic2.0的源代碼:https://nodeload.github.com/subsonic/SubSonic-2.0/zip/master
其實下載下來的是SubSonic2.2.1,找到其中的 SqlQuery\SqlGenerators\ANSISqlGenerator.cs 文件:
1:
2: public virtual string BuildPagedSelectStatement()
3: {
4: // 省略的代碼...
5:
6: string wheres = GenerateWhere();
7:
8: //have to doctor the wheres, since we're using a WHERE in the paging
9: //bits. So change all "WHERE" to "AND"
10: string tweakedWheres = wheres.Replace("WHERE", "AND");
11:
12: // 省略的代碼...
13:
14: string sql = string.Format(PAGING_SQL, idColumn, String.Concat(fromLine, joins, wheres), String.Concat(tweakedWheres, orderby, havings),
15: String.Concat(select, fromLine, joins), query.CurrentPage, query.PageSize, sqlType);
16: return sql;
17: }
其中 tweakedWheres 是關鍵,作者還特別指出要把其中 WHERE 替換成 AND,殊不知這樣做對子查詢是破壞性操作,而且下面連接SQL腳本時重復添加了WHERE子句。
修改后的代碼:
1:
2: public virtual string BuildPagedSelectStatement()
3: {
4: // 省略的代碼...
5:
6: string wheres = GenerateWhere();
7:
8: //have to doctor the wheres, since we're using a WHERE in the paging
9: //bits. So change all "WHERE" to "AND"
10: //string tweakedWheres = wheres.Replace("WHERE", "AND");
11:
12: // 省略的代碼...
13:
14: string sql = string.Format(PAGING_SQL, idColumn, String.Concat(fromLine, joins, wheres), String.Concat(orderby, havings),
15: String.Concat(select, fromLine, joins), query.CurrentPage, query.PageSize, sqlType);
16: return sql;
17: }
搞定!
子查詢與表關聯查詢(查看角色下所有用戶)
在上面的例子中,我們使用的是子查詢,對於“查看角色下所有用戶”這個案例,我們還有如下另一種解決辦法(效果完全一樣):
1: // 查詢 X_User 表
2: SqlQuery q = new Select().From<XUser>().InnerJoin(XRoleUser.UserIdColumn, XUser.IdColumn);
3: q.Where("1").IsEqualTo("1");
4:
5: // 在用戶名稱中搜索
6: string searchText = ttbSearchUser.Text.Trim();
7: if (!String.IsNullOrEmpty(searchText))
8: {
9: q.And(XUser.NameColumn).ContainsString(searchText);
10: }
11:
12: // 過濾選中角色下的所有用戶
13: object[] values = Grid1.DataKeys[Grid1.SelectedRowIndexArray[0]];
14: int roleId = Convert.ToInt32(values[0]);
15: q.And(XRoleUser.RoleIdColumn).IsEqualTo(roleId);
16:
17:
18: // 在查詢添加之后,排序和分頁之前獲取總記錄數
19: // Grid1總共有多少條記錄
20: Grid2.RecordCount = q.GetRecordCount();
21:
22: // 排列
23: q.OrderBys.Add(GetSortExpression(Grid2, XUser.Schema));
24:
25: // 數據庫分頁
26: q.Paged(Grid2.PageIndex + 1, Grid2.PageSize);
27: items = q.ExecuteAsCollection<XUserCollection>();
再來看下這段代碼生成的SQL腳本(修正SubSonic2.2.1中的BUG后):
1: DECLARE @Page int
2: DECLARE @PageSize int
3:
4: SET @Page = 1
5: SET @PageSize = 20
6:
7: SET NOCOUNT ON
8:
9: -- create a temp table to hold order ids
10: DECLARE @TempTable TABLE (IndexId int identity, _keyID Int)
11:
12: -- insert the table ids and row numbers into the memory table
13: INSERT INTO @TempTable
14: (
15: _keyID
16: )
17: SELECT [dbo].[X_User].[Id]
18: FROM [dbo].[X_User]
19: INNER JOIN [dbo].[X_RoleUser] ON [dbo].[X_User].[Id] = [dbo].[X_RoleUser].[UserId]
20: WHERE 1 = @10
21: AND [dbo].[X_RoleUser].[RoleId] = @RoleId1
22:
23: ORDER BY Name DESC
24:
25: -- select only those rows belonging to the proper page
26: SELECT [dbo].[X_User].[Id], [dbo].[X_User].[Name], [dbo].[X_User].[Password], [dbo].[X_User].[Enabled], [dbo].[X_User].[Email], [dbo].[X_User].[Gender], [dbo].[X_User].[RealName], [dbo].[X_User].[QQ], [dbo].[X_User].[MSN], [dbo].[X_User].[CellPhone], [dbo].[X_User].[OfficePhone], [dbo].[X_User].[HomePhone], [dbo].[X_User].[Remark], [dbo].[X_User].[DeptId], [dbo].[X_User].[RoleId], [dbo].[X_User].[CreateTime]
27:
28: FROM [dbo].[X_User]
29: INNER JOIN [dbo].[X_RoleUser] ON [dbo].[X_User].[Id] = [dbo].[X_RoleUser].[UserId]
30:
31: INNER JOIN @TempTable t ON [dbo].[X_User].[Id] = t._keyID
32: WHERE t.IndexId BETWEEN ((@Page - 1) * @PageSize + 1) AND (@Page * @PageSize)
向當前角色添加現有用戶
對於這個情況,我們要注意一點,就是供選擇的現有用戶不應當包括哪些已經屬於當前角色的用戶,可以用子查詢來實現:
1: SqlQuery q = new Select().From<XUser>(); //.LeftOuterJoin(XRoleUser.UserIdColumn, XUser.IdColumn);
2: q.Where("1").IsEqualTo("1");
3:
4: // 在職務名稱中搜索
5: string searchText = ttbSearchMessage.Text.Trim();
6: if (!String.IsNullOrEmpty(searchText))
7: {
8: q.And(XUser.NameColumn).ContainsString(searchText);
9: }
10:
11: // 排除已經屬於本角色的用戶
12: int currentRoleId = GetQueryIntValue("id");
13: SqlQuery subQ = new Select(XRoleUser.UserIdColumn).From<XRoleUser>().Where(XRoleUser.RoleIdColumn).IsEqualTo(currentRoleId);
14:
15: q.And(XUser.IdColumn).NotIn(subQ);
16: //q.And(XUser.IdColumn).IsNotEqualTo(1);
17:
18: // 只列出不在當前角色中的用戶
19: //q.AndExpression(XUser.RoleIdColumn.ColumnName).IsNotEqualTo(GetQueryIntValue("id")).Or(XUser.RoleIdColumn).IsNull().CloseExpression();
20:
21: // 在查詢添加之后,排序和分頁之前獲取總記錄數
22: // Grid1總共有多少條記錄
23: Grid1.RecordCount = q.GetRecordCount();
24:
25: // 排列
26: q.OrderBys.Add(GetSortExpression(Grid1, XUser.Schema));
27:
28: // 數據庫分頁
29: q.Paged(Grid1.PageIndex + 1, Grid1.PageSize);
30: XUserCollection items = q.ExecuteAsCollection<XUserCollection>();
小結
雖然SubSonic2.2的代碼已經不更新了,但是在實際應用中,我們可以恰當的修正其源代碼來滿足需求,這也歸功於開源的力量。同時也希望大家能多關注同樣是完全開源的ExtAspNet(基於ExtJS的專業ASP.NET2.0控件庫)。
注:AppBox是捐贈軟件,也就是說你可以通過捐贈作者來獲得AppBox源代碼。