對於開始接觸基於ORM技術開發的ERP程序,在相當長的時間內還是會考慮SQL語句,而不是ORM查詢。即便是在很熟悉ORM查詢,也不如對SQL語句的了解程度。於是想做出一個查詢工具,把SQL語句轉化為C#代碼,用於查詢。
這樣的程序片段在很多地方都需用用到。
比如SQL語句
SELECT RECNUM ,CCY ,DESCRIPTION ,SUSPENDED ,DEFAULT_RATE FROM [Currency]
查詢當前的貨幣及其名稱,默認匯率。打開高級查詢功能,把這幾個字段拖動到SQL語句窗口中,點擊按鈕Execute即可在結果窗格中看到生成的ORM語句片段。
生成的LLBL Gen Pro代碼片段
ICurrencyManager currencyManager = ClientProxyFactory.CreateProxyInstance<ICurrencyManager>(); ExcludeIncludeFieldsList fieldlist = new ExcludeIncludeFieldsList(false); fieldlist.Add(CurrencyFields.RECNUM); fieldlist.Add(CurrencyFields.CCY); fieldlist.Add(CurrencyFields.DESCRIPTION); fieldlist.Add(CurrencyFields.SUSPENDED); fieldlist.Add(CurrencyFields.DEFAULT_RATE); CurrencyEntity currency = currencyManager.GetCurrency(this., null, fieldlist);
再來分析一下,如何實現這個過程。
1 樹結點多選功能
如上圖中所示,我選中樹節點Currency中的多個字段,然后把它拖動到SQL語句窗口中,自動生成SQL查詢語句。這里要實現樹節點多選功能。WinForms內置的樹控件不支持此功能,需要另找控件。
這里,我選用CodeProject上的控件MultiSelectTreeView,它的功能用法如下介紹所示
Summary description for MultiSelectTreeView. The MultiSelectTreeView inherits from System.Windows.Forms.TreeView to allow user to select multiple nodes. The underlying comctl32 TreeView doesn't support multiple selection. Hence this MultiSelectTreeView listens for the BeforeSelect && AfterSelect events to dynamically change the BackColor of the individual treenodes to denote selection. It then adds the TreeNode to the internal arraylist of currently selectedNodes after validation checks. The MultiSelectTreeView supports 1) Select + Control will add the current node to list of SelectedNodes 2) Select + Shitft will add the current node and all the nodes between the two (if the start node and end node is at the same level) 3) Control + A when the MultiSelectTreeView has focus will select all Nodes.
2 鼠標拖動編程
樹結點中設置AllowDrag,加入事件響應方法ItemDrag
private void treeTables_ItemDrag(object sender, ItemDragEventArgs e) { if (e.Button == MouseButtons.Left) { DoDragDrop(e.Item, DragDropEffects.Copy); } }
要拖進的SQL TextEdtor,則對它加入事件響應方法
private void txtSqlScript_DragDrop(object sender, DragEventArgs e) { TreeNode draggedNode = (TreeNode)e.Data.GetData(typeof(TreeNode)); if (draggedNode.Tag == "Column") { List<string> columns=new List<string>(); foreach(TreeNode node in treeTables.SelectedNodes) { columns.Add(node.Text.Substring(0, node.Text.IndexOf("(") )); } string tableName = draggedNode.Parent.Text; string sql = string.Format("SELECT {0} FROM [{1}] ", string.Join(",",columns), tableName); txtSqlScript.InsertText(sql); } else if (draggedNode.Tag == "Table") { txtSqlScript.InsertText(string.Format("[{0}]",draggedNode.Text)); } }
這幾句話,根據節點代表的含義(字段,表名),來構造SQL查詢語句。
3 SQL語句解析
Visual Studio Database Edition提供了SQL語句解析功能,引用這兩個程序集,實現類似的代碼
public static string Execute(QueryDataSource dataSource,string sql) { string csharpCode = string.Empty; IList<ParseError> Errors; var parser = new TSql100Parser(false); StringReader reader = new StringReader(sql); IScriptFragment result = parser.Parse(reader, out Errors); var Script = result as TSqlScript; foreach (var ts in Script.Batches) { foreach (var st in ts.Statements) { IterateStatement(st,ref csharpCode,dataSource); } } return csharpCode; }
此方法根據傳入的SQL語句,返回C#代碼。LLBL Gen 是一項ORM技術,ORM首要解決的問題是C#代碼如何轉化為對數據庫操作的SQL語句。在這里,我把這個過程反過來,根據SQL語句,得到C#代碼。
數據表Currency 對應的程序中的實體類型是CurrencyEntity,它的字段RECNUM對應於C#實體類型CurrencyEntity中的Recnum,這個映射關系存儲於生成的C#代碼中。
/// <summary>Inits CurrencyEntity's mappings</summary> private void InitCurrencyEntityMappings() { this.AddElementMapping( "CurrencyEntity", "MIS", @"dbo", "Currency", 29 ); this.AddElementFieldMapping( "CurrencyEntity", "AcctApForex", "ACCT_AP_FOREX", true, "NVarChar", 30, 0, 0, false, "", null, typeof(System.String), 0 ); this.AddElementFieldMapping( "CurrencyEntity", "AcctArForex", "ACCT_AR_FOREX", true, "NVarChar", 30, 0, 0, false, "", null, typeof(System.String), 1 ); this.AddElementFieldMapping( "CurrencyEntity", "ApInvoBal", "AP_INVO_BAL", true, "Decimal", 0, 2, 16, false, "", null, typeof(System.Decimal), 2 ); this.AddElementFieldMapping( "CurrencyEntity", "ApLinvoBal", "AP_LINVO_BAL", true, "Decimal", 0, 2, 16, false, "", null, typeof(System.Decimal), 3 ); this.AddElementFieldMapping( "CurrencyEntity", "ApLnetBal", "AP_LNET_BAL", true, "Decimal", 0, 2, 16, false, "", null, typeof(System.Decimal), 4 ); this.AddElementFieldMapping( "CurrencyEntity", "ApLopenBal", "AP_LOPEN_BAL", true, "Decimal", 0, 2, 16, false, "", null, typeof(System.Decimal), 5 ); this.AddElementFieldMapping( "CurrencyEntity", "ApNetBal", "AP_NET_BAL", true, "Decimal", 0, 2, 16, false, "", null, typeof(System.Decimal), 6 ); }
運行時,只需要調用此方法即可得到它們的映射關系,以實現將SQL語句的object(table,column)轉化為C#程序對應的object(Entity,property)。
4 Debug功能的實現
為了實現即使運行效果,將上面的生成的SQL語句,編譯成C#程序集,再執行程序集,得到返回的結果。
CodeDomProvider codeProvider = null; if (Language == LanguageType.CSharp) codeProvider = new CSharpCodeProvider(); else if (Language == LanguageType.VB) codeProvider = new VBCodeProvider(); //create the language specific code compiler ICodeCompiler compiler = codeProvider.CreateCompiler(); //add compiler parameters CompilerParameters compilerParams = new CompilerParameters(); compilerParams.CompilerOptions = "/target:library"; // you can add /optimize compilerParams.GenerateExecutable = false; compilerParams.GenerateInMemory = true; compilerParams.IncludeDebugInformation = false; // add some basic references compilerParams.ReferencedAssemblies.Add("mscorlib.dll"); compilerParams.ReferencedAssemblies.Add("System.dll"); compilerParams.ReferencedAssemblies.Add("System.Data.dll"); compilerParams.ReferencedAssemblies.Add("System.Drawing.dll"); compilerParams.ReferencedAssemblies.Add("System.Xml.dll"); compilerParams.ReferencedAssemblies.Add("System.Windows.Forms.dll");
如果團隊中允許多語言並行開發,此功能也可以實現將生成的C#代碼,轉化為VB代碼。
在這里,需要構造一個Main方法,把生成的代碼嵌入到這個Main方法中去,返回一個對象,把這個對象反射到Debug的網格窗口中。反射調用的入口方法如下所示
private object CallEntry(Assembly assembly, string entryPoint) { object result = null; try { //Use reflection to call the static Main function Module[] mods = assembly.GetModules(false); Type[] types = mods[0].GetTypes(); foreach (Type type in types) { MethodInfo mi = type.GetMethod(entryPoint, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); if (mi != null) { if (mi.GetParameters().Length == 1) { if (mi.GetParameters()[0].ParameterType.IsArray) { string[] par = new string[1]; // if Main has string [] arguments result=mi.Invoke(null, par); } } else { result= mi.Invoke(null, null); } } } LogErrMsgs("Engine could not find the public static " + entryPoint); } catch (Exception ex) { LogErrMsgs("Error: An exception occurred", ex); } return result; }
最后一步,綁定結果到網格中
void BindEntity2Grid(IEntity2 entity) { grid.RowHeadersVisible = false; grid.AutoGenerateColumns = false; grid.Columns.Clear(); Type type = entity.GetType(); PropertyInfo[] propertyInfos = type.GetProperties(); for (int i = 0; i < propertyInfos.Length; i++) { PropertyInfo property = propertyInfos[i]; if (excludeColumns.Contains(property.Name)) continue; DataGridViewTextBoxColumn column = new DataGridViewTextBoxColumn(); column.HeaderText = property.Name; column.DataPropertyName = property.Name; column.Name = property.Name; grid.Columns.Add(column); } for (int i = 0; i < propertyInfos.Length; i++) { PropertyInfo property = propertyInfos[i]; if (excludeColumns.Contains(property.Name)) continue; grid.Rows[0].Cells[property.Name].Value = ReflectionHelper.GetPropertyValue(entity, property.Name); } }
這里的目的是讀取對象的屬性,生成Grid的列,並填充值。
這里使用的SQL語法高亮控件來自於CodeProject的FastColoredTextBox。 讀取一個數據庫中所有的表及表的字段用到如下的SQL語句,供您參考。
SELECT name FROM sysobjects WHERE xtype='U' SELECT syscolumns.NAME AS [ColumnName], systypes.NAME AS [ColumnType], syscolumns.length AS [ColumnLength],syscolumns.isnullable AS [Nullable] FROM syscolumns left join systypes on syscolumns.xusertype=systypes.xusertype WHERE id=(select id from sysobjects where name='Currency‘
此功能有個明顯的Bug,沒有把功能編碼顯示在地址欄中,因為它繼承於FormBase,需要改成FunctionFormBase可達到目的,界面可能需要重新排版一下,改變繼承的基類會讓窗體設計器重新加載基類控件,子類的控件會丟失。
[FunctionCode("SAUMAQ")] public partial class AdvancedQuery : FormBase { private string _fileName; public AdvancedQuery() { InitializeComponent(); } }