December 2007 - Posts

用暴力攻擊法找出長度不足的Oracle欄位

搞破解的人大概都知道什麼叫暴力攻擊法吧!

今天被一個ORACLE Error搞得極度火大,利用OracleCommand INSERT資料,ORACLE冷冷地說: "ORA-01401 inserted value too large for column",很好,給的字串太長? 為何不說哪個欄位有問題? 我用OracleParamer沒法直接轉出SQL語法去SQLPLUS驗證,而且總共有三四十個欄位耶~~ 你不要光搖頭!! 你倒是說呀!! 你給我說呀!!(猛力搖晃肩膀)

網路上查到ORACLE 10g會比較親切地指出問題出在哪一個欄位,雖然現在用的是9i,只要花少少幾百萬就可以讓ORACLE更親切耶! 真是太超值! 太划算了... orz

可想而知,解決這問題的正途是人工逐一檢查比對各欄位的長度。不過今天在盛怒之下,我做了一件不理性的事,寫了一小段暴力攻擊法,逐一縮短字串長度,直到指令執行成功為止,如此就可以知道那一個欄位長度要多短才會過關。

不過,這裡有幾個假設:

1.所有欄位中只有一個超出長度
2.你想知道明確的值位長度上限(不然直接將欄位設成空字串會比較有效率)
3.使用的ORACLE資料庫是測試開發性質,經得起你催殘
   (換句話說,使用風險自負,不管被DBA譙、被老板罵、被公司開除都與我無關)
4.你知道將catch Block當成正常流程極無效率,會這麼做只是為了洩憤
5.使用完畢後應前往寺廟為自己的嗔怒業障懺悔贖罪

適度的宣泄情緒有助於程式開發人員的心理健康,想了想,還是把這段跑起來很爽的Code跟大家分享一下,阿彌陀佛!

//Log Command Error
System.Diagnostics.Debug.WriteLine("Command Error!!");
System.Diagnostics.Debug.WriteLine("CommandText=" + cmd.CommandText);
foreach (OracleParameter p in cmd.Parameters)
{
    System.Diagnostics.Debug.WriteLine(
        string.Format("Parameter[{0}](Type:{1})(Len:{2})={3}",
        p.ParameterName, 
        p.DbType.ToString(),
        p.Value.ToString().Length, p.Value.ToString())
        );
}
//Brute Force Attack 
(Warning: Never run these unless you know what you are doing)
using (OracleConnection bfcn = new OracleConnection(_cnnString))
{
    bfcn.Open();
    cmd.Connection = bfcn;
    foreach (OracleParameter p in cmd.Parameters)
    {
        if (p.OracleType == OracleType.VarChar || 
            p.OracleType == OracleType.NVarChar)
        {
            string v = p.Value.ToString();
            string origVal = v;
            for (int i = v.Length - 1; i >= 0; i--)
            {
                p.Value = v.Substring(0, i);
                try
                {
                    cmd.ExecuteNonQuery();
                    System.Diagnostics.Debug.WriteLine("Hallelujah");
                    System.Diagnostics.Debug.WriteLine(
                        string.Format("Parameter[{0}]-Len[{1}]",
                        p.ParameterName,
                        i));
                    break;
                }
                catch
                {
                }
            }
            p.Value = origVal;
        }
    }
}
ORACLE Transaction大車拼!

為了測試使用System.Data.OracleClient、ODP.NET(Oracle.DataAccess.Client)與TransactionScope三者的效能差距,我設計了以下的實驗,分別用三種方式(因ODP.NET 9207還不支援TransactionScope,所以TransactionScope搭配System.Data.OracleClient使用)將三個Insert動作包成Transaction,並各測十次。

static object obj = new object();
static int _idx;
static int getIdx()
{
    lock (obj)
    {
        _idx++;
        return _idx;
    }
}
 
static void testOraTrn1()
{
    using (OracleConnection cn = new OracleConnection(cnStr))
    {
        cn.Open();
        OracleTransaction trn = cn.BeginTransaction();
        OracleCommand cmd = new OracleCommand();
        cmd.Connection = cn;
        cmd.Transaction = trn;
        cmd.CommandText = "INSERT INTO Jeffrey Values (:EmpNo, :EmpName)";
        OracleParameter pEmpNo = 
            cmd.Parameters.Add("EmpNo", OracleType.Int32);
        OracleParameter pEmpName = 
            cmd.Parameters.Add("EmpName", OracleType.VarChar);
        try
        {
            pEmpNo.Value = getIdx();
            pEmpName.Value = "T1-1st";
            cmd.ExecuteNonQuery();
            pEmpNo.Value = getIdx();
            pEmpName.Value = "T1-2nd";
            cmd.ExecuteNonQuery();
            pEmpNo.Value = getIdx();
            pEmpName.Value = "T1-3rd";
            cmd.ExecuteNonQuery();
            trn.Commit();
        }
        catch
        {
            trn.Rollback();
        }
    }
}
 
static void testOraTrn2()
{
    using (ODP.OracleConnection cn = 
        new Oracle.DataAccess.Client.OracleConnection(cnStr))
    {
        cn.Open();
        ODP.OracleTransaction trn = cn.BeginTransaction();
        ODP.OracleCommand cmd = cn.CreateCommand();
        cmd.Connection = cn;
        cmd.CommandText = "INSERT INTO Jeffrey Values (:EmpNo, :EmpName)";
        ODP.OracleParameter pEmpNo = 
            cmd.Parameters.Add("EmpNo", ODP.OracleDbType.Int32);
        ODP.OracleParameter pEmpName = 
            cmd.Parameters.Add("EmpName", ODP.OracleDbType.Varchar2);
        try
        {
            pEmpNo.Value = getIdx();
            pEmpName.Value = "T2-1st";
            cmd.ExecuteNonQuery();
            pEmpNo.Value = getIdx();
            pEmpName.Value = "T2-2nd";
            cmd.ExecuteNonQuery();
            pEmpNo.Value = getIdx();
            pEmpName.Value = "T2-3rd";
            cmd.ExecuteNonQuery();
            trn.Commit();
        }
        catch
        {
            trn.Rollback();
        }        
        
    }
}
 
static void testOraTrn3()
{
    using (TransactionScope tx = new TransactionScope())
    {
        using (OracleConnection cn = new OracleConnection(cnStr))
        {
            cn.Open();
            OracleCommand cmd = new OracleCommand();
            cmd.Connection = cn;
            cmd.CommandText = "INSERT INTO Jeffrey Values (:EmpNo, :EmpName)";
            OracleParameter pEmpNo = 
                cmd.Parameters.Add("EmpNo", OracleType.Int32);
            OracleParameter pEmpName = 
                cmd.Parameters.Add("EmpName", OracleType.VarChar);
            try
            {
                pEmpNo.Value = getIdx();
                pEmpName.Value = "T3-1st";
                cmd.ExecuteNonQuery();
                pEmpNo.Value = getIdx();
                pEmpName.Value = "T3-2nd";
                cmd.ExecuteNonQuery();
                pEmpNo.Value = getIdx();
                pEmpName.Value = "T3-3rd";
                cmd.ExecuteNonQuery();
                tx.Complete();
            }
            catch
            {
                //Log error or something
            }
        }
    }
}
 
static void trnTest(int testNo)
{
    int times = 5;
    long total = 0;
    for (int i = 0; i < times; i++)
    {
        Stopwatch sw = new Stopwatch();
        sw.Start();
        switch (testNo)
        {
            case 1:
                testOraTrn1(); break;
            case 2:
                testOraTrn2(); break;
            case 3:
                testOraTrn3(); break;
        }
        sw.Stop();
        Console.WriteLine("TEST{0}-{1:00} {2}ms",
            testNo, i, sw.ElapsedMilliseconds);
        total += sw.ElapsedMilliseconds;            
    }
    Console.WriteLine("TEST{0} Avg = {1}ms",
        testNo, total / times);
}

測試結果出籠:

TEST1-00 350ms
TEST1-01 5ms
TEST1-02 4ms
TEST1-03 5ms
TEST1-04 4ms
TEST1 Avg = 73ms
TEST2-00 281ms
TEST2-01 7ms
TEST2-02 4ms
TEST2-03 4ms
TEST2-04 5ms
TEST2 Avg = 60ms
TEST3-00 1054ms
TEST3-01 8ms
TEST3-02 9ms
TEST3-03 8ms
TEST3-04 8ms
TEST3 Avg = 217ms

賽前預測,TransactionScope會是最慢的,實測結果也是如此。

不過由數字來看,三者都慢在第一次,推論應是第一次建立Connection較花時間,但之後Connection Pooling發威,速度就變快許多。

我的結論是,TransactionScope雖然方便且具備一些不可取代的涵蓋性(例如: 異質資料庫間的交易、包入3rdParty元件內的資料庫更新等),但分散式交易在效能上輸給單一資料庫的Transaction實作一大截,在講究效能的場合,要特別留意。

國語辭典終於回來了

在眾人的罵聲不絕殷殷期待中,教育部的線上國語辭典,終於回來了...

新網址在http://140.111.34.46/newDict/dict/index.html (謝謝Morris的通報),界面有了修改,畫面比以前漂亮,還多了限定成語、歇後語、外來語類別的進階查詢,功能變豐富了。

你這殘忍的小東西,不許不許再這樣離開我了~~~

3D Paper Model

在枯燥生活中找了一點小休閒---3D紙模!
(表面上說是休閒,其實還是心機很重地想藉著做精細手工藝,改善專注力渙散及過於急躁等資訊焦慮併發症[註])

網路上可以找到不少免費下載的紙模PDF檔,印出來後裁裁黏黏就可以做出漂亮的立體紙模型。其中Canon的3D紙模網站有很多精緻的動物、建築、汽車、飛機... 包羅萬象。如果覺得這些太小兒科,可以試試難度更高、更變態的Yamaha摩托車模型

這些紙模設計得相當精細,只要裁切準確,模型的密合度可做到很高。不過組合的順序要記得參照說明書一步步來,別像我搞到後來,為了要黏狗肚子,得用手指、牙籤伸進狗屁股東摳西按的,很醜也很蠢。

建議製作紙模不可或缺的幾項利器:

1.切割墊: 不必擔心破壞桌面,下刀力道也較易控制
2.筆刀: 可以切出圓弧這類美工刀無法達成的裁切線
3.小剪刀: 對於複雜的曲線,剪刀又比筆刀來得順手
4.相片膠(施敏打硬): 快乾、黏性強,比膠水、白膠好用許多,強烈建議
5.牙籤: 上膠用

Enjoy It!

[註] 不知是年紀大了,或是因為長時間處於資訊太多、時間太少的意識狀況,發現自己已經喪失仔細認真閱讀完一篇文章的能力。看沒兩行,眼睛焦點的移動速度就會開始加速,迫不及待地想在更短的時間內把文字讀完;很快地,大腦的解析速度跟不上眼睛移動,開始由閱讀變成瀏覽,再轉成瞥過。腦子跟不上眼睛之後,心思漸漸就飛到九霄雲外,等眼睛掃到文末才驚覺: 我怎麼已經讀完了,這篇文章在說什麼? (騎車的人應該都有一邊騎車一邊想事情的經驗吧? 常常不知不覺到了目的地,卻想不起來途中怎麼過的十字路口,也記不得經過了哪些地方,差不多是這種感覺)
這種注意力無法集中的現象,我將其歸咎為"資訊焦慮併發專注力渙散症侯群"。

TIPS-Case Sensitive In Oracle Table/Column Name

印象中,ORACLE的Table Name, Column Name都是不分大小寫的。不過今天我摔了第二次,決心把這個Tip寫下來...

建Oracle Table,大部分我會徒手寫CREATE TABLE Script,偶爾偷懶會用Aqua Data Studio。今天用想驗證Transaction,於是用Aqua Data Studio的管理UI建了一個Table: Jeffrey。資料表建出來了,UI上看得到,但SELECT * FROM Jeffrey卻一直得到ORA-00942: table or view does not exist的訊息。

印象中之前似乎遇過一次,但原因、解法完全不記得了。求救同事,她檢查Table Create Script看到以下寫法:
CREATE TABLE "Jeffrey"
(
  "EmpNo"  NUMBER(3)                            NOT NULL,
  "Name"   VARCHAR2(16 BYTE)                    NOT NULL
)

這下子我全都想起來了,原來使用Aqua Data Studio建立資料表時,會忠實地保留使用者輸入的大小寫,等於在下指令時,用雙引號包住Table Name, Column Name。一般指令中,未包雙引號的名稱則會一律轉成大寫,所以SELECT * FROM Jeffrey其實會去找名為"JEFFREY"的資料表,因而擦身而過。

這問題以前遇過一次,偷懶沒記下來的結果是很久之後又花了時間摸索第二次(雖然這次有貴人相助,很快得到答案,但還是花了時間),That's why you see this post.

KB-給ASPX義大利麵寫法的好人卡

上回我提了一個解決套用MasterPage時ClientID會突變的方法,其中提到: 我不喜歡document.getElementById('<% =TextBox1.ClientID%>').value這種ASP時代義大利麵式的寫法。

網友eric問: 為什麼不愛?

義大利麵先生,你是個好人,但是...

第一個理由是我喜歡Server-Side Logic編譯過並藏在DLL中,不要顯露在易被取得的ASPX內。不過,在這個應用情境上我們只用來標示WebControl.ClientID,稱不上是後端邏輯,原則上這點可以忽略。

第二個理由是我不喜歡在網頁裡夾帶<% ... %>這種ASP/ASPX專屬語法。在開發網站時,我常會先做一個配色噁心、版面混沌,只有程式設計師搞得出來,也只有程式設計師可以忍受的醜小鴨網頁,再交給團隊中的美學達人(我不喜歡稱這群專家叫美工,對一個配色殘障來說,她們是神~~~)用巧手變成天鵝。當網頁中摻雜了這些ASPX獨有語法,就容易因其他廠牌設計工具無法識別而在處理過程中受損。

第三個理由跟效能有點關係。所有的ASPX的內容,最後都會變成一個.cs檔,其中Client-Side Script、純HTML Tag等,都會被轉成字串處理,每用一次<% ... %>,就會切割出兩段字串。有點抽象是吧? 我用以下的例子說明:

<body>
    <form id="form1" runat="server">
    <div>
    </div>
    <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
&nbsp;<asp:Button ID="Button1" runat="server" Text="Button" />
    <br />
    <script type="text/javascript">
    alert('<% =TextBox1.Text %>');
    alert("<% =Button1.Text %>");
    </script>
    </form>
</body>

以上的寫法,實際上會被轉成以下的C# Code:

private void __Renderform1(HtmlTextWriter __w, Control parameterContainer)
{
    __w.Write("\r\n    <div>\r\n    \r\n    </div>\r\n    ");
    parameterContainer.Controls[0].RenderControl(__w);
    __w.Write("\r\n&nbsp;");
    parameterContainer.Controls[1].RenderControl(__w);
    __w.Write("\r\n    <br />\r\n    
<script type=\"text/javascript\">\r\n alert('"
);
    __w.Write(base.TextBox1.Text);
    __w.Write("');\r\n    alert(\"");
    __w.Write(base.Button1.Text);
    __w.Write("\");\r\n    </script>\r\n    ");
}

其中parameterContainer.Controls[0]就是TextBox1、parameterContainer.Controls[1]就是Button1。我們看到_Renderform1先吐出<div></div>,接著請TextBox1產生對應的HTML Tag,然後是&nbsp;,再來輪Button1產生對應的HTML Tag。接下來有意思了,如果我們這裡只是單純的Javascript Block,理論上應該一個__w.Write就會把整個Script Block輸出完。但因為我們用了<% =TextBox1.Text %>與<% =Button1.Text %>,所以整個Script Block被拆成五段輸出。分五次呼叫__w.Write,即使差異有限,但肯定會比一次呼叫來得沒效率,如果可以避免,我還是會設法閃過。

以上是我不愛義大利麵式寫法的理由,原則上這種寫法並非萬惡不赦,只是與我慣用的開發方式與哲學有點違背,算是個人偏好下的結論,這裡就細說從頭,給大家參考參考囉!

KB-MasterPage ClientID Issue

[Abstract]

When using ASP.NET masterpage, the ClientID of webcontrol inside ContentPlaceHolder will get container's ClientID as prefix, like 'ctl00_ContentPlaceHolder1_TextBox1' and this become a big trouble while writing Javascript client-side code. 

Many people suggest using "document.getElementById('<% =TextBox1.ClientID%>')" to solve the problem, but I really don't like embed server-side code in ASPX file, so here's my solution, a flexible Javascript "afa_mpget()" to replace document.getElementById() when using masterpage.

猜謎時間又來了! 想看看,以下的Code有什麼地方有問題?

<%@ Page Language="C#" AutoEventWireup="true"  CodeFile="Default.aspx.cs" 
Inherits="_Default" MasterPageFile="~/MasterPage.master" %>
<asp:Content runat="server" ContentPlaceHolderID="ContentPlaceHolder1">
    <asp:TextBox ID="TextBox1" runat="server" Text="Hello"></asp:TextBox>
    <script type="text/javascript">
    alert(document.getElementById("TextBox1").value);
    </script>
</asp:Content>

答案是Javascript alert時會找不到物件,為什麼? 在網頁上View Source,答案馬上揭曉,當網頁套用MasterPage時,TextBox1產生的<INPUT>名稱會變成"ctl00_ContentPlaceHolder1_TextBox1"... 天哪! 由Frameset走向MasterPage,簡單的事變複雜了,要為此回頭嗎?

Google一下,發現大家最常建議最的解法是改寫成
alert(document.getElementById('<% =TextBox1.ClientID%>').value);

不過,我個人實在不喜歡這種ASP時代的義大利麵式寫法,所以就自力救濟了一番。首先我寫了一個MasterPageHelper.cs:

public class MasterPageHelper
{
    public MasterPageHelper()
    {
    }
 
    public static void RegisterMPGet(MasterPage mp)
    {
        List<string> lstCph = new List<string>();
        lstCph.Add(mp.ClientID);
        searchContentPlaceHolder(mp.Page.Form, lstCph);
        StringBuilder sb = new StringBuilder();
        sb.Append(@"
<script type=""text/javascript"">
function afa_mpget(objId) {
    var inp = document.getElementById(objId);
");
        foreach (string cphId in lstCph)
        {
            sb.AppendFormat(
"   if (!inp) inp = document.getElementById(\"{0}_\" + objId);\n",
                cphId);
        }
        sb.Append("return inp;\n}\n</script>");
        Literal js = new Literal();
        js.Text = sb.ToString();
        mp.Page.Form[請看下方更新]Header.Controls.AddAt(0, js);
    }
 
    public static void searchContentPlaceHolder(Control ctrl, 
List<string> lst)
    {
        if (ctrl is ContentPlaceHolder)
            lst.Add(ctrl.ClientID);
        else if (ctrl.HasControls())
            foreach (Control c in ctrl.Controls)
                searchContentPlaceHolder(c, lst);
    }
}

在MasterPage的Page_Load事件中,呼叫MasterPageHelper.RegisterMPGet(this),就會在網頁中加入一個用元件名稱自動尋找各ConentPlaceHolder下元件的彈性函數--afa_mpget(fieldName):

<script type="text/javascript">
function afa_mpget(objId) {
    var inp = document.getElementById(objId);
   if (!inp) inp = document.getElementById("ctl00_" + objId);
   if (!inp) inp = document.getElementById("ctl00_ContentPlaceHolder1_" + objId);
   if (!inp) inp = document.getElementById("ctl00_ContentPlaceHolder2_" + objId);
return inp;
}
</script>

接著,我們用afa_mpget取代document.getElementById,就又回到以前幸福快樂的生活囉!

<asp:Content runat="server" ContentPlaceHolderID="ContentPlaceHolder1">
    <asp:TextBox ID="TextBox1" runat="server" Text="Hello"></asp:TextBox>
    <script type="text/javascript">
    </script>
</asp:Content>
<asp:Content runat="server" ContentPlaceHolderID="ContentPlaceHolder2">
    <asp:TextBox ID="TextBox2" runat="server" Text="World"></asp:TextBox>
    <script type="text/javascript">
    alert(afa_mpget("TextBox1").value);
    alert(afa_mpget("TextBox2").value);
    </script>
</asp:Content>

Update 2008-01-03
Page.Form.Controls.AddAt會破壞ViewState,故改成Header.Controls.Add,說明在此

Update 2008-01-19
強化版搜尋範圍擴及UserControl,說明在此

VS2008-Session Exception Of Custom WebControl

I have an old custom webcontrol worked fine for years on VS 2005.  When I edited a ASP.NET 2.0 web site project with VS 2008, tried to drag the .NET 2.0 webcontrol  from toolbox to the web page, I got this exception in IDE.

"Session state can only be used when enableSessionState is set to true, either in a configuration file or in the Page directive. Please also make sure that System.Web.SessionStateModule or a custom session state module is included in the <configuration>\<system.web>\<httpModules> section in the application configuration."

Search for this issue via Google, most of the discussions focus on session not enabled or WSS issue.  I am very sure the enableSessionState is enabled and no WSS issue in my web site.  Will it be design time session issue?

I reviewed my code and found this:

        protected override void OnInit(EventArgs e)
        {
            base.OnInit (e);
            if (!Page.IsPostBack)
            {
                //Generate a unique key
                Text=Guid.NewGuid().ToString("N").ToUpper();
                //Store some variable to session
                Page.Session[Text+"_State"]="Start";
            }
        }

Yeah, I use Session object in OnInit, maybe it's the cause.  So the above code was modified as

if (!this.DesignMode) 
    Page.Session[Text+"_State"]="Start";

Now my custom webconrol get back again in VS2008, case closed.

[My Conclusion]
When Page.Session object is used during design time, VS2005 will ignore it, but VS2008 will throw an "session can not be used..." exception and cause a error desgin time display.

TIPS-Track Active Item in Solution Explorer

這是我使用VS2005/VS2008時的一個小困擾...

在VS.NET 2003時代,編輯某個Form.cs或Form.aspx.cs時,Solution Explorer會自動跳到正在編輯的Form上,所以按一下View Designer就可以跳到該Form的Design View編輯畫面。

不過,此功能在VS 2008上卻無效,於是有一種情境讓我很困擾: 編輯FormA.cs,在FormA.cs中看到FormB frmB=...,利用Go To Definition跳到FormB.cs,接著想看一下FormB長什麼樣子,按一下View Designer,看到的卻是FormA的表單設計畫面。orz...

研判在Solution Explorer找出正在編輯的物件是個挺耗資源的動作(尤其當專案內項目很多時,可是Editor都知道檔案位置了,搜索作業應該不必逐一查找比對,為什麼還會耗資源?),在VS2008裡,這個Solution Explorer與Editor同步的功能預設是關閉的。Google了一下,總算找到地方,選項如下: (還找到一些Performance Tips教導這個選項關閉可以提高效能,這大概就是它預設是unchecked的理由吧!)

重返母校

週六早上參加了女兒幼稚園的運動會,事實上是政大實小的運動會,政大實幼是政大家族(政大從研究所、大學、高中、國中、小學到幼稚園都有,很神奇吧?)最幼齒的成員,特地在運動會客串一個氣球傘表演。

一群蘿莉跟正太抓著一大塊彩色傘布玩得不亦樂乎,最後傘布向上一抖,再全體躲進傘布下,包成一個大"蒙古包"。靠! 看起來很好玩耶,為什麼我小時候沒有... (地滾)

運動會在政大實小的操場舉行,屆指一算,資深校友已經畢業二十五年了。以前覺得大得要命的校園,現在看來小得可憐。小時候,世界上最大的組織便是這個每年級只有兩班的迷你小學,後來陸續見識過一個年級十六班的國中、好幾千名光頭男組成的新訓中心,世界大了、眼界開了、童心卻也再也喚不回來了...

二十五年後重回校園,學校歷經幾度擴充改建,記憶中的老校舍只剩下上圖的明德樓依然屹立,當年它以四層樓的高度傲視全校,現在卻是整個校園最矮的建物。望著它,腦海硬碟開始嘎嘎作響,搜索塵封的回憶...

明德樓前有四棵高達三層樓的尤加利樹,我總喜歡摘葉子揉揉,聞它的香氣...
(為啥? 你是無尾熊嗎?)

在地下室開月會講話被老師指正,不爭氣地落淚...
(受不了,沒看過這麼好面子又這麼娘的)

下雨天在頂樓風雨操場打躲避球跟同學後腦相撞,自己沒事,同學卻腦震盪請假三天
(少林功夫好耶! 真正好~~ 我沒鐵頭功啦,同學撞完又後腦著地才這麼嚴重滴)

在操場撿球,被其他小朋友搖浪船撞到後腦勺破皮見骨,肇事小朋友一臉驚恐的哭臉彷彿歷歷在目...
(就說我沒鐵頭功唄!)

噢... 那無憂無慮的童年!

StringBuilder串接字串的迷思

大部分的.NET開發者都知道,要做大量的字串相加,StringBuilder比string相加快上N倍。這個效能差異源於String物件的特性,每次"動態相加"時必須捨棄原字串佔用的記憶體空間,重新配置記憶體儲存相加後的新字串內容。只是背後的原理實在曲折,於是我們腦海只會留下"串接字串千萬要用StringBuilder,用string相加會被人笑"的簡化結論。

前些時候協助做Code Review,看到一段SQL查詢程式出現有趣的寫法。

一般為了方便閱讀,將長長的SQL依SELECT, FROM, WHERE拆成多行是很好的寫作習慣;不過在這個例子中,StringBuilder被拿來串接靜態變數,直覺上並不能發揮提高效能的效果。依我的認知,靜態字串的相加會在編譯階段時轉化成單一字串,理論上會比動用StringBuilder物件來得快。但沒測過,我也不敢確定答案與速度差距。所以我準備了三種字串組合方式,在力求SQL指令要拆行以利閱讀的前題下,第一種用StringBuilder,第二種用加號相加,第三種則用@"..."讓字串內容得以直接換行。

static string GetSqlString_1()
{
    StringBuilder sqlCmd;
    sqlCmd = new StringBuilder("");
    sqlCmd.Append(
"select customer.customername || ' ' as cstname,vendor.vbename || ' ' as vbename,");
    sqlCmd.Append(" tradepara.cparavalue || ' ' as Bank,");
    sqlCmd.Append(" vendor.brkcap ||' '|| vendor.brkcapvalue as CCASS,");
    sqlCmd.Append(" contract.tradername ||'      '|| contract.traderphone as Person,");
    sqlCmd.Append(" contract.bankno || ' ' as BIC,contract.traderemail || ' ' as email");
    sqlCmd.Append(" from sbtrade");
    sqlCmd.Append(" left join customer on sbtrade.corpid = customer.corpid");
    sqlCmd.Append(" and sbtrade.customerid = customer.customerid");
    sqlCmd.Append(" left join vendor on sbtrade.corpid = vendor.corpid");
    sqlCmd.Append(" and sbtrade.secbrkid = vendor.secbrkid");
    sqlCmd.Append(" left join tradepara on sbtrade.corpid = tradepara.corpid");
    sqlCmd.Append(" and tradepara.tradeid=:tradeid");
    sqlCmd.Append(" and sbtrade.customerid = tradepara.customerid");
    sqlCmd.Append(" and tradepara.cparaid='CUSTBANKNAME'");
    sqlCmd.Append(" left join contract on sbtrade.corpid = contract.corpid");
    sqlCmd.Append(" and sbtrade.mktcodeid = contract.mktcodeid");
    sqlCmd.Append(" and sbtrade.secbrkid = contract.secbrkid");
    sqlCmd.Append(" and contract.sbktype='CSI'");
    sqlCmd.Append(" and sbtrade.secaccount = contract.secaccount");
    sqlCmd.Append(" where sbtrade.corpid=:gCorpId and sbtrade.tradetype='BS'");
    sqlCmd.Append(" and to_char(sbtrade.tradedate,'yyyy/MM/dd') = :tradedate");
    sqlCmd.Append(" and sbtrade.mktcodeid=:mktcodeid");
    sqlCmd.Append(" and sbtrade.secbrkid = :secbrkid");
    sqlCmd.Append(" and sbtrade.secaccount = :secaccount");
    sqlCmd.Append(" and sbtrade.customerid = :customerid");
 
    return sqlCmd.ToString();
}
 
static string GetSqlString_2()
{
    string sql = 
"select customer.customername || ' ' as cstname,vendor.vbename || ' ' as vbename," +
    " tradepara.cparavalue || ' ' as Bank," +
    " vendor.brkcap ||' '|| vendor.brkcapvalue as CCASS," +
    " contract.tradername ||'      '|| contract.traderphone as Person," +
    " contract.bankno || ' ' as BIC,contract.traderemail || ' ' as email" +
    " from sbtrade" +
    " left join customer on sbtrade.corpid = customer.corpid" +
    " and sbtrade.customerid = customer.customerid" +
    " left join vendor on sbtrade.corpid = vendor.corpid" +
    " and sbtrade.secbrkid = vendor.secbrkid" +
    " left join tradepara on sbtrade.corpid = tradepara.corpid" +
    " and tradepara.tradeid=:tradeid" +
    " and sbtrade.customerid = tradepara.customerid" +
    " and tradepara.cparaid='CUSTBANKNAME'" +
    " left join contract on sbtrade.corpid = contract.corpid" +
    " and sbtrade.mktcodeid = contract.mktcodeid" +
    " and sbtrade.secbrkid = contract.secbrkid" +
    " and contract.sbktype='CSI'" +
    " and sbtrade.secaccount = contract.secaccount" +
    " where sbtrade.corpid=:gCorpId and sbtrade.tradetype='BS'" +
    " and to_char(sbtrade.tradedate,'yyyy/MM/dd') = :tradedate" +
    " and sbtrade.mktcodeid=:mktcodeid" +
    " and sbtrade.secbrkid = :secbrkid" +
    " and sbtrade.secaccount = :secaccount" +
    " and sbtrade.customerid = :customerid";
    return sql;
}
 
static string GetSqlString_3()
{
    string sql = @"
select customer.cstname || ' ' as customername,vendor.vbename || ' ' as vbename,
tradepara.cparavalue || ' ' as Bank, 
vendor.brkcap ||' '|| vendor.brkcapvalue as CCASS, 
contract.tradername ||'      '|| contract.traderphone as Person, 
contract.bankno || ' ' as BIC,contract.traderemail || ' ' as email 
from sbtrade 
left join customer on sbtrade.corpid = customer.corpid 
and sbtrade.customerid = customer.customerid 
left join vendor on sbtrade.corpid = vendor.corpid 
and sbtrade.secbrkid = vendor.secbrkid 
left join tradepara on sbtrade.corpid = tradepara.corpid 
and tradepara.tradeid=:tradeid 
and sbtrade.customerid = tradepara.customerid 
and tradepara.cparaid='CUSTBANKNAME' 
left join contract on sbtrade.corpid = contract.corpid 
and sbtrade.mktcodeid = contract.mktcodeid 
and sbtrade.secbrkid = contract.secbrkid 
and contract.sbktype='CSI' 
and sbtrade.secaccount = contract.secaccount 
where sbtrade.corpid=:gCorpId and sbtrade.tradetype='BS' 
and to_char(sbtrade.tradedate,'yyyy/MM/dd') = :tradedate 
and sbtrade.mktcodeid=:mktcodeid 
and sbtrade.secbrkid = :secbrkid 
and sbtrade.secaccount = :secaccount 
and sbtrade.customerid = :customerid ";
    return sql;
}

接著我跑以下的程式分別執行GetSqlString_1(),GetSqlString_2()及GetSqlString_3()各100萬次,事前預測是1最慢,2,3一樣快,沒想到,慢的程度比想像中大多了。

Stopwatch sw = new Stopwatch();
int times = 1000000;
sw.Start();
for (int i = 0; i < times; i++)
    GetSqlString_1();
sw.Stop();
Console.WriteLine(string.Format("Test1={0}ms", sw.ElapsedMilliseconds));
sw.Reset();
sw.Start();
for (int i = 0; i < times; i++)
    GetSqlString_2();
sw.Stop();
Console.WriteLine(string.Format("Test2={0}ms", sw.ElapsedMilliseconds));
sw.Reset();
sw.Start();
for (int i = 0; i < times; i++)
    GetSqlString_3();
sw.Stop();
Console.WriteLine(string.Format("Test3={0}ms", sw.ElapsedMilliseconds));
return;

測試結果:

Test1=4152ms
Test2=6ms
Test3=7ms

【結論】

以StringBuilder提升字串相加效率主要應用於大量的反覆字串動態(in runtime)串接,靜態字串的串接在編譯時就會自動變成單一字串,牽扯到StringBuilder物件的建立與呼叫反而變慢許多。因此StringBuilder請應用在連續大量的Runtime動態字串相接才不會未得其利,反受其害。

多行式靜熊字串的表示,在C#中,我強烈推薦使用GetSqlString_3()的Literal String寫法! 用來表示多行文字的SQL語法、Javascript,極為簡潔方便。

本機使用者的登入Script

想在本機使用者登入時加掛一些作業,Logon Script是個不錯的解法,當年NT4 MCSE學的差不多都忘光了,花了點時間才摸出來。從使用者管理員可以指定登入時要RUN的程式:

原本我打算用絕對路徑指定本機目錄下的logon.bat,卻發現為了因應使用者可能在不同機器登入的情境(雖然此點不適用於本機使用者),Logon Script只能使用相對路徑。去Google廟參拜,求得靈籤一張,說明必須在本機手動建立%SystemRoot%\System32\Repl\Imports\Scripts(或自訂目錄也成)並將其分享成Netlogon。

關於Javascript函數的宣告時機

Javascript燈謎時間又來了...

<html><body>
<script type="text/javascript">
test("Before");
function test(m) 
{
    alert(m);
}
test("After");
</script>
</body></html>

以上的Code,Before與After都會出現嗎? 在我印象中,Javascript函數一定要先宣告才能呼叫,但顯然有點誤差! 在上面的例子中,test("Before");與test("After");都會順利執行!

不過,注意到了嗎? 我是說有誤差,而非記錯了。原因是函數宣告前呼叫的狀況,只適用在同一個Script Block下。再來看一個例子:

<html><body>
<input type="button" onclick="test('OK')" value="test">
<script type="text/javascript">
test("Before Block");
</script>
<script type="text/javascript">
function test(m) 
{ alert(m); }
</script>
<script type="text/javascript">
test("After Block");
</script>
</body></html>

在以上的例子中,test("Before Block");被放在之前的Script Block而導致出錯,但後方Script Block的test("After Block");則OK。順手再測了宣告函數前就標成<INPUT>的onclick事件,也是可行的。

Goggle到一篇不錯的文章,順便記下來。

TIPS-還原NCR編碼

網友Eric問到關於NCR轉換的事,查了一下,發現我過去有介紹過toNCR()的做法,倒沒提到fromNCR(),這裡補上,給有需要的人參考。

程式只有短短幾行,主要靠Regex強大的比對功能解決大部份的難題。一口氣將字串中所有的&#nnnn;抓出來,nnnn轉成整數再轉成char,接著將字串的編碼一一置換掉,搞定!

private string fromNCR(string s)
{
    foreach (System.Text.RegularExpressions.Match m 
        in System.Text.RegularExpressions.Regex.Matches(s,"&#(?<ncr>\\d+?);")) 
        s = s.Replace(m.Value, 
            Convert.ToChar(int.Parse(m.Groups["ncr"].Value)).ToString());
    return s;
}
【茶包射手系列】忠貞不二的CSS檔

悶! 抓了好久!

同事設計好頁面樣式,給了我一個HTML跟CSS,讓我套用到ASP.NET網頁上。我在ASPX裡用<link href="site.css" type="text/css" rel="stylesheet" />套用樣式,卻怎麼都無法生效! 最詭異的是,CSS無法生效的ASPX,用IE檢視原始檔後另存成HTML,跟ASPX放在同一個目錄下用IE檢視,CSS效果就出得來,這這這...

IE Developer Toolbar Trace Style功能檢查,發現在ASPX中,指定Class Name也對不出有效的Style,在HTML中則沒有問題。好個CSS檔,居然也搞起意識形態,誓死相挺HTML到底,打死不為ASPX站台!

搞了一陣子,忽然想到以前遇過js的Encoding問題,仔細一看,CSS中的字型名稱用到中文"標楷體",果然又是編碼問題! 把CSS另存成UTF-8編碼,藥到病除,Case Closed!

More Posts Next page »

Search

Go

<December 2007>
SunMonTueWedThuFriSat
2526272829301
2345678
9101112131415
16171819202122
23242526272829
303112345
 
RSS
【工商服務】


BlogLook Score and Rank

Syndication