2013年11月11日 星期一

[Crystal Report] 新手入門包 - 套資料

上一集講到Crystal Report的報表檔怎麼完成。

這一集就說說怎麼把正確的資料丟到報表檔呈現出來。
這是我自己常用的方式,也把它做成抽象類別,如果有人有更好或更方便的方法,也請不吝指教。

--套資料--

整體來說,我們可以把步驟分作幾個抽象動作:


  1. 我們要先從資料庫把資料找出來
  2. 把找出來的資料,依照自己的格式丟到自己在做的DataSet
  3. 宣告一個ReportDocument,路徑、DataSource設定好。
  4. 就可以輸出了。

1. 從資料庫裡撈資料

端看個人習慣使用什麼方式在做資料庫的存取。
我的考量是:報表通常都會輸出一些不同table在JOIN來JOIN去的結果,所以就算系統裡有使用Entity Framework之類的物件關聯式資料庫,在查報表的時候我還是習慣直接寫SQL Script。
不管怎麼樣,輸出是一個DataTable還是SqlDataReader都好,只要能正確把資料讀出來、正確了解各個欄位的定義就好。
Dim Command as SqlCommand = New SqlCommand()
Command.Connection = new SqlConnection(ConnectionString)
Command.CommandText = "SELECT * FROM [TableName] " + vbNewLine 'SQL Script
Command.Connection.Open()
Dim SearchResult as DataTable = CommandToDataTable(Command)

2. 整理格式,丟到DataSet

這裡指的DataSet是指在做報表時做的那個自訂DataSet。
有的時候我們從資料抓出來的資料會需要做一些格式的轉換才會顯示在頁面上,就在這個階段的來完成。
我個人在這裡常做的事是把存在DB裡的7碼民國年把年月日用斜線隔開,或是把代碼檔轉換成中文。
Dim dtReturn As dtDepartmentDocRegisteredList = New dtDepartmentDocRegisteredList()
For Each row As DataRow In SearchResult.Rows
    Dim dr As DataRow = dtReturn.DataTable1.NewRow()
    dr("ReceivedDate") = FormatMandarinDate(row("ReceivedDate").ToString(), "/")
    dr("DocumentType") = DictionaryManager.GetValue(GetType(CodeDocumentType), row("DocumentType"))
    dr("Organizer") = row("Organizer")
    dr("SendDate") = FormatMandarinDate(row("SendDate").ToString(), "/")
    dtReturn.DataTable1.Rows.Add(dr)
Next
Return dtReturn

3. 宣告、設定ReportDocument

ReportDocument是Crystal Report Runtime裡的元件,應該是早就安裝好了。要用的時候Include或Import一下應該就有了。
必備的設定是報表檔的路徑,還有用該元件的SetDataSource方法把整理好的DataSet設定給他。
如果要完成的報表比較複雑,你可能還會加參數、設定一些雜七雜八的東西。
Dim doc As ReportDocument = New ReportDocument()
doc.Load("C:\XXXXX\XXX.rpt") '磁碟路徑。我要用的話都會用MapPath來讓Framework自己轉。
doc.SetDataSource(dtReturn)  '整理出來的DataSet丟進去

4. 匯出


這裡介紹的匯出動作會把報表含你輸出的資料一起弄成特定格式的檔案,再存到主機裡。
在我近期所做的專案裡,他們匯出報表的方式統一採用先存成檔案,再供Client端做檔案下載。所以該方式還算堪用。
Dim oExDo As New DiskFileDestinationOptions
'輸出的檔案名稱,ExportFormatType.PortableDocFormat是指PDF。
Dim FileName As String = String.Format("{0}_{1}.{2}", ReportName, DateTime.Now.Ticks, GetReportExtension(ExportFormatType.PortableDocFormat))

oExDo.DiskFileName = "C:\XXXXX\" + FileName
doc.ExportOptions.ExportDestinationType = ExportDestinationType.DiskFile
doc.ExportOptions.ExportFormatType = FileType
doc.ExportOptions.DestinationOptions = oExDo

doc.Export()  'Export to PDF
doc.Close()
doc.Dispose()

中間有任何一個地方壞掉或不合Crystal Report的規則,會直接噴Exception出來。


--功德圓滿--

到後來,我都把一些常用的程式片段另外取出來放在Utils裡。
像報表匯出、加參數等常用的東西,要我直接寫出來我還不會寫,都直接從網路上咖過來整理一下就可以用了。

2013年11月8日 星期五

[Crystal Report] 新手入門包 - 從無到有

上一次有提到ReportService新手入門,這次就乾脆把Crystal Report的新手入門包也弄一弄,希望對以後有用。

開發環境:
Visual Studio 2010


--Let's Start--

1. 首先,我們要讓開發工具知道我們在幹嘛。

要先去下載Crystal Report的套件,安裝一下。沒有裝這個,就算新增了Crystal Report項目,也只會看到一大堆16進位碼。

要下載我用紅框框框起來的那部分,開發工具才有圖形介面可以讓你開發報表,不然只有runtime而已,開發工具不支援。


2. 安裝好了之後,就可以開始開發了。

在方案總管那裡,在你打算放報表檔案的資料夾裡新增Crystal Report項目,取個好名字給它。
我覺得"Crystal Report1.rpt"很好,就這麼取吧~

他又問我要不要用精靈。按照慣例,開個空白報表就好。

空白報表就長這個樣子啦。

報表在預設的時候分了5個Section:

  1. 報表首:整張報表的頭,如果你一份報表有好幾張紙的份量,放在這個Section裡面的內容只會顯示在第一頁。
  2. 頁首:每一頁都會顯示的內容。
  3. 細目:你要顯示一條一條列表的地方,概念像是DataTable裡的DataRow,或是資料表裡的Record。
  4. 報表尾:只會顯示在最後一頁的部分。通常會放什麼總計、一些註解或是簽名欄。
  5. 頁尾:都會顯示在每一頁的最尾巴。


3. 把報表的尺寸調整好!

如果你沒先做這一步,什麼都畫好了再來調報表,你會調到想哭。


4. 再來,把DataSet建出來。

在ReportingService那篇有提到一個重要的東西:DataSet,這裡也需要。
Crystal Report裡,他可以用微軟自己的DataSet。就和在專案裡加一個檔案一樣,新增一個「資料集」到一個適合的地方。


建好就長這樣



5. 報表樣式自己畫,有用過小畫家的應該都會,這裡就不細講了。

有機會再聊聊我自己常用的小技巧。


6. 用資料庫專家,把DataSet嵌進報表裡。




完事之後,就會看到資料庫專家的下面多一個DataTable,裡面有你弄好的欄位。



7. 把資料庫的欄位一個一個拉到報表裡適合的位子。

什麼叫適合的位子?就看你報表樣式要怎樣畫就怎樣畫。


8. 預覽一下。

你可以在開發工具上切換報表開發和預覽兩個頁籤。預覽一點下去,就會看到你的報表設定完之後,最後跑資料出來會是什麼德性。
裡面的資料都是Crystal Report自己亂數產出來的,和實際資料沒有關係,所以也不要慌,除了內容之外,最後讓使用者看到的大概就是那樣了。


如此一來,一張報表就開發完了。
至於要怎麼套正確的資料,乃至於實際應用在介面上,我們下回再分曉。









你說啥?斷在這裡很不負責任嗎?
沒辦法,上一集Reporting Service的內容也是斷在報表剛做好而已,再來我就沒試過了。
為了兩集的內容相對稱,停在這裡是很合理的。

2013年11月7日 星期四

[ReportingService] 新手入門包 - 從無到有

昨天在整理這半年多來的截圖,看到之前原本要寫的東西沒寫,真的是囧翻了。

這是我在上一個客戶那裡用到的東西:Reporting Service。整體來說好像比Crystal Report還簡單,但我想還是記一下這個新手入門款,免得七久八久沒碰就忘了,下回再摸一遍。

開發環境:
Visual Studio 2008




--開發分隔線--

1. 開始之前,我們會需要一個Reporting Server Project。


2. 接下來,我們會需要一個Shared Data Source。

設定好一下連線字串或帳號密碼就行了,不難。搞不定就多try幾次吧~
事成之後就會多一個剛建出來的DataSource。


3. 再來,就準備要開發新報表囉!


在圖裡,我個人習慣打開空報表再一點一點慢慢弄。Wizard這種東西讓我…嗯,不是很習慣,它通常…很肥。

個人在畫報表上的習慣,我會把尺規打開,在對線對刻度的時候比較方便,有刻度可以定位,可以減少畫歪的情況
留意一下報表的寬度高度,還有上下左右邊界留白,不然在產出報表(印出來或是產成PDF的時候會因為超過邊界而多印一頁。


4. 開發報表最重要的事,就是把DataSet嵌進報表裡。


pop出DataSet屬性視窗之後,就可以開始寫SQL在裡面、設定參數。
這個屬性視窗中間有Query的那一大空格,就是讓你寫SQL的地方,開發工具會根據你SELECT出來的欄位,顯示在左邊。


5. 最後,你應該會是需要把DataSet裡面一筆一筆的Row顯示在報表裡。

只要在報表本體裡按右鍵→插入→表格,再把左邊DataSet裡的欄位一格一格拖進去就行了。





這麼著,一張簡單的報表就這麼從無到有地製作出來了,感覺比Crystal Report簡單滿多的。
至少表格的線不用再一條一條畫。

2013年11月6日 星期三

[MSSQL] 備份組包含現有的xxx資料庫以外的資料庫

今天玩資料庫遇到比較特別的情況,卡了一下,還不小心把人家的db搞掛了,還好重開就好了。
需求很簡單:我要從A機器裡的db,複製到B機器裡。
其實就是備份/還原嘛~聽起來沒什麼了不起。

但是DBMS就是一種會把聽起來很簡單的事情搞得很複雜的東西。(好啦,我知道「備份/還原」都是被抽象化的結果,裡面藏著很複雜的邏輯。)



備份:
備份上自己沒遇到什麼問題。
在SQL Server上要備份下來的資料庫上面按右鍵→工作→備份。會跑出下面那個畫面。


備份路徑稍微注意一下,以自己方便(或習慣)為主。
出來的備份檔應該都會是.bak檔,然後是放在A機器裡。


接著,把這個.bak檔複製到B機器裡。這個別問我啊,隨身碟、檔案分享、FTP…什麼都好。




還原:
還原的部分就沒這麼順遂了。
同樣的,在B機器的SQL Server上要還原的資料庫上按右鍵→工作→還原→資料庫。會跑出下面的畫面。


設定一下.bak檔的路徑之後就開始了,期待它完成,我今天的工作就結束了。
問題就是他媽的沒這麼爽,才按下去它就跑出一個Exception,上面寫「備份組包含現有的xxx資料庫以外的資料庫備份問題」之類的訊息。



我太懶了,我懶得寫找解法碰釘子的過程,直接寫怎麼處理。
1. 先把DB弄離線。

ALTER DATABASE [dbname] SET offline

2. 執行下列指令:
restore database DBName
from disk = 'D:\DBName.bak'
with
move 'DBName' to 'C:\Program Files\...\xxx.mdf', 
move 'DBName_log' to 'C:\Program Files\...\xxx_log.LDF',
NoRecovery,
Replace
go

留意一下.mdf和.ldf檔路徑就好。
如果執行成功了,就會出現「幾秒內成功處理了 多少頁(0.000 MB/sec)。

3. 再把DB弄上線。
ALTER DATABASE [dbname] SET online

4. 還原!
資料庫上按右鍵→工作→還原→資料庫,.bak路徑設好、勾還原,按確定。

5. 忐忑不安地等待,搞定就開心~ 搞不定就GG。

2013年2月7日 星期四

[ .NET ] 讓DataTable自己過濾資料


前陣子接到一個需求,那個情境整個讓我整個大發火:允許使用者輸入查詢條件,針對已查詢出來的結果再進行查詢

聽起來是一個很好用的功能,如果查出來的資料有30頁分頁,用這個方式來過濾資料確實是對使用者來說相當方便。
真正讓我發火的是:那個頁面是40多種查詢項目共用的,不同的查詢項目,GridView上產生的欄位會不一樣,而聽到這個需求的時候,我還在考慮是要從資料庫重新拉資料,還是只是把已經抓出來的資料藏在什麼地方,要的時候再拿出來。更重要的是,我聽到這個需求的時候,隔天就要驗收,而我還一點頭緒也沒有。
順便宣達一下:在開發功能的時候,真的要有人隨時檢視合約和工作項目之間的關聯,真的不要到驗收前才發現有一些該完成的項目沒列在工作項目裡。

--該冷靜一下的分隔線--

時程的關係,當時已經沒有時間再去改寫DAO,讓使用者在操作介面的時候讓系統重新跟資料庫不斷地拉少量資料,所以決定在頁面上動手腳。
不知道從哪時候開始就有聽說過.NET的伺服器元件裡有一個地方可以讓人寫類似SQL的過濾句,又隱約看過DataTable裡有個Select之類的東西,就打算從這方面開始著手。

查了一下又試了一下,發現有點不太容易使用。
DataTable.Select(string expression)中,Select裡可以塞一段類似SQL裡的WHERE條件句的字串,把DataTable裡的資料再做一次過濾(參考expression的寫法)。壞就壞在他回傳的是DataRow陣列,而且還是這個DataTable自己的DataRow,不是另外Create一塊新的DataRow來放,所以也不能把這些DataRow再加到其它的DataTable裡,我要怎麼顯示在介面上都不太對,這個東西就變成練習用的玩具,不算產出。

後來survey各路名門文獻來尋找靈感,就這麼無意間讓我知道由DataTable.DefaultView回傳回來的一個DataView,裡面除了裝著DataTable的資料之外,官方還說可以進行資料的篩選和排序。

是我要的篩選

於是我就在這個DataView.RowFilter裡加那些原本寫在DataTable.Select裡的expression,再把GridView和DataTable重新Bind一下,就可以得到我要的結果了!

    private void SelectDataTable(string keyword) {
        DataTable dt = (DataTable)ViewState["dtSource"];

        if (keyword == "")
        {
            dt.DefaultView.RowFilter = "";
            ViewState["RowFilter"] = "";
        }
        else
        {
            StringBuilder expWhere = new StringBuilder();
            foreach (DataColumn col in dt.Columns)
            {
                if (col.ColumnName == "SeqID") { continue; }
                if (col.DataType != typeof(string)) { continue; }
                expWhere.Append(string.Format("{0} LIKE '%{1}%' OR ", col.ColumnName, keyword));
            }
            dt.DefaultView.RowFilter = ((ViewState["RowFilter"].ToString() != "") ? ViewState["RowFilter"] + " AND " : "") + "(" + expWhere.ToString().Remove(expWhere.Length - 3, 3) + ")";
            ViewState["RowFilter"] = dt.DefaultView.RowFilter;
        }
        ViewState["dtSource"] = dt;
        SetGridView(dt);
    }



老天保佑,It's work!


下面是畫面圖。為保護當事人已馬賽克(?)處理

圖1. 查出來的資料很多,有8頁

 圖2. 透過RowFilter來過濾資料

圖3. 再濾一次。這個功能會讓關鍵字查詢用AND的方式把前後的查詢一次一次濾掉,不過這裡看不出來....


Ref:

2012年12月16日 星期日

[嘆世間] 我談加班

最近在客戶那裡常常聊到加班。幾個客戶公司的年輕人們這陣子常常被主管拗加班,我自己也有時候會被要求留下來做晚一點,對此,其它人是敢怒敢怨而不敢言。我想藉此來聊聊我想聊聊我自己對「加班」這件事的想法。


--前言到內文之間的分隔線--

定義「加班」:勞基法第四章第三十條明文規定:勞工每日正常工作時間不得超過八小時,每週工作總時數不得超過四十八小時。
因此,每天工作超過基本工作時數一個小時以上,對我來說就算是加班。至於加班的酬勞如何計算,就看各家公司自己如何規定,只要是對員工有一定程度的保障,我就都還可以接受。


那我是怎麼看待「加班」這件事呢?那就是去你媽的

我在資訊業上班。我個人覺得這種消耗腦力、精神力的工作,一個正常的成年人,每天可以全心全意專注在工作上,不可能超過四、五個小時,就算是分段進入專注的狀況,一整天下來也不會超過七個小時。
專注工作的時候是生產力最強的時候,其它剩下的上班時間,用來處理一些不需要專注思考的雜事、省視一下自己的工作…有時候休息一下,上個廁所、抽根菸、聊聊msn、瀏覽一下facebook我覺得都不算什麼過份的事。
而一個人的工作效率要達到最高,精神力在專注的狀態下要維持得久,每天都必須要有充足的休息時間才行。不僅僅只是單純肉體所需的睡眠,還要能讓精神得以放鬆。
綜合以上理由,我認為上班時間就專心地工作,下班後就好好休息,讓隔天能有足夠的精神和體力來面對工作,這樣才是對工作、對做事的人都好的做法。
所以,加班只能偶爾為之,不能成為常態。否則將進入「加班→休息時間少、沒辦法充份休息→工作效率差、產出受到限制→再加班→休息時間更少…」這樣的惡性循環,事情永遠沒有做好的一天,只會讓自己一天比一天還累,一天比一天更虛弱,而要做的事情一直都沒有被少。
尤其是在精神渙散的時候寫程式,你可能會在睡飽之後看到自己的Code裡面有一些根本不該出現的錯誤,然後得花更多時間修改這些蠢問題。


所以當有人要我加班,最好要給我一個合理的理由。否則,和顏悅色地拒絕是客氣;打槍是基本;如果我那時候在煩其它事,就會被我罵回去。
順便一提,趕專案從來就不是合理的理由。會需要從來就不是開發程式的人的問題,主管、專案經理或是談需求的人…這樣的高階上游角色才是真正該負責的,多做一點點東西可以視為舉手之勞,趕很多嘛…我倒想問問,為什麼我要花我的時間來收你們捅出來的包?


--以上就是我對加班的看法--


「你是我花錢請來的人,我叫你做什麼你就給我做!」
『你一天只買我八小時,其它時間我是自由的。』

「我隨時都可以叫你滾!」
『你現在願意花錢找我來工作,就是因為我的技術能解決你的問題。在你的問題沒完全解決之前,我認為我都是安全的。』

「幫我做嘛…我付你加班費。」
『再多錢都買不回我失去的時間,還有健康。』



我不是把資方全然妖魔化,我也明白當老闆的有很多足以決定公司興亡的壓力。
但是,誰不是人生父母養?誰沒有家庭?


當你們想要你們的傭兵能隨時隨地滿足你們的所需,請想一想在他們背後等待他們回家的家人們。

我相信良好的雇傭互動關係,雙方都可以好過很多,而且事情可以做得比較順。



2012年10月31日 星期三

[ SQL ] JOIN in T-SQL

最近在某神祕專案裡常常會看到現有的Code在抓報表資料的時候,會讓資料庫一次、一次、一次地取大量資料,然後在server-side程式裡做轉換、統整、用LINQ查詢、統計以後再輸出給client端。
這樣的做法雖然在程式的閱讀上會很明確,哪個欄位代表什麼意義或過濾什麼條件都會很清楚,但是資料量超過一定程度以後,每一次從資料庫取出來的資料量本身就是一個負擔,再加上把資料做型態上的轉換、不斷宣告新物件裝資料、再用LINQ做查詢整合,從下命令到得到結果中間時間會變得非常久。

LINQ很方便,可以用,但不要過度地依賴。把SQL Script的基本功打好,一次把要所需的資料全部精準地過濾、計算,回傳到server-side程式的時候就是一個完整的結果,再來就只要輸出就行了,雖然可能中間會JOIN好幾張表,但只要Index建好,查詢效率就不會太差。

一個案子裡的程式開發人員能力有高有低,不是每個人都能夠寫出DBA等級的查詢句,再加上時程壓力,很多開發人員都會不願意動腦子想SQL該如何下比較好,而是急著把成果生出來而以最直覺的方式去下指令,然後就會寫出在迴圈裡把資料庫的連線開開關關,然後抓出來的資料還要轉成特別的物件再用LINQ去下過濾條件的程式,按下查詢之後可能天都黑了才生出報表(前提是不會跑出奇奇怪怪的Exception)。
這樣的情況一路開發到後期,或是再下一期兩期,後面的每一隻程式都是複製貼上,大部分的查詢都是用同樣的方式在開開關關建建放放,系統負擔不大才怪。

根據這樣的假設和跳躍式思考,我認為如果能夠活用各種JOIN的方式,把所需的資料一口氣全找出來,能夠減少資料庫的負擔,進而增加系統處理的效能。

聽說這篇原本是要寫JOIN?

--廢話終於講完了的分隔線--

簡單來說,JOIN就是把兩個集合(資料表),透過不同的方式得到不同的集合。而在T-SQL(SQL Server)中,常用到的JOIN方式不外乎那幾種:
  1. INNER JOIN:取出來的資料為兩個集合都有的資料。
  2. LEFT (RIGHT) OUTER JOIN:以左邊(或右邊)的集合為基底,查詢與另一張表相對應的欄位。
  3. FULL OUTER JOIN:左外關聯和右外關聯的聯集。
  4. CROSS JOIN :兩個集合的每一筆資料都會被取出來。

來用範例讓這中間的差異變得更明顯。



現在,我們有一些資料表和資料....


這是會員,裡面只有簡單的ID、姓名和職業,其中職業是以代碼來表示,來源是另一種表。
在這裡,我特別設計了一個會員,假設他用了一些特殊的方式寫入這個系統,使他允許在「職業」那一欄沒有輸入東西。


職業,就只有ID和名稱而已,超級基本的代碼表。



產品,內容有ID、名稱、類型代碼和庫存量,和會員那裡一樣的是:類型代碼的來源也是另一張表。


老梗的代碼表,這是產品類型


我的例子還需要一些「交易記錄」才完整,長得就像上面一樣。我要記錄的資料並不多,只要知道「哪個會員在什麼時間買了什麼東西,買了幾個」就可以了。所以設計出來就如圖所示。
會員就存會員ID、產品就存產品ID、交易時間就存寫入資料表當下的系統時間,還有一個整數的數量。


就這樣,我們就建好了簡單的環境,再來就是仔細的看一下各種JOIN方法會呈現什麼結果。



INNER, LEFT, RIGHT JOIN:

假設今天我要找出會員及其職業名稱,那我可以這樣下句子,並且得到以下的結果.....


我也可以這樣下....


我還可以這樣下....


差異在哪裡?
INNER JOIN所抓出來的,會是JOIN左右兩邊資料表都有互相關聯的資料。這個環境裡,沒有會員是「無業」,也有一個戳戳的傢伙沒有輸入職業,這兩種東西就不會顯示出來。
LEFT JOIN就是LEFT OUTER JOIN。它就會以JOIN左邊的Member當底,然後把JOIN右邊的Job相對應的資料撈出來顯示。重點來囉!JOIN左邊的Member資料,只要沒有下特別的條件去過慮,那每一筆資料都會被取出來,即使Job裡沒有相對應的欄位,它也會弄一筆資料出來,空的欄位就用null來表示。
RIGHT JOIN和LEFT JOIN不一樣的地方,只有方向而已,把上面那段的左邊改成右邊就行了。範例就是…沒有無業的人,它還是會把無業抓出來,然後把前面的欄位放null。

還有一種特別的JOIN叫作FULL OUTER JOIN,其實就是....LEFT JOIN和RIGHT JOIN有出現過的資料全放一起就對了,專業的名詞叫作「聯集」。瞧,例子在下面。LEFT JOIN和RIGHT JOIN有出現過的資料全出現在下面。


CROSS JOIN:
說起來CROSS JOIN是一種滿特別的JOIN方式:它不管JOIN左右兩邊資料表的資料有沒有任何關聯,它都會把每一組配對組合資料都顯示出來。下面那張圖,得到的結果就是職業和產品類型的各種配對。


「這種沒有關聯的兩張表,配對出來的結果有什麼用?」

如果你想知道「各種職業購買各種產品的記錄筆數總和」之類的東西,這就有用了。
如此一來,我就不用在程式裡跑迴圈下查詢查到死。




--好睏哦....--

善用JOIN,你的人生將會一片光明 (超敷衍)。
至少在面對統計報表或是神祕的實體關係時比較不會有所畏懼。


是的,我最近在搞報表,快被搞死了。


[範例Code,含建置和查詢]