本文介紹如何構建一個基於 Grails 的資料瀏覽器來視覺化複雜的表格資料。
我是 Grails 的忠實粉絲。當然,我主要是熱衷於利用命令列工具來探索和分析資料的資料從業人員。資料從業人員經常需要檢視資料,這也意味著他們通常擁有優秀的資料瀏覽器。利用 Grails、jQuery,以及 DataTables jQuery 外掛,我們可以製作出非常友好的表格資料瀏覽器。
DataTables 網站提供了許多“食譜式”的教學文件,展示了如何組合一些優秀的範例應用程式,這些程式包含了完成一些非常漂亮的東西所必要的 JavaScript、HTML,以及偶爾出現的 PHP。但對於那些寧願使用 Grails 作為後端的人來說,有必要進行一些說明示教。此外,樣本程式中使用的資料是一個虛構公司的員工的單個平面表格資料,因此處理這些複雜的表關係可以作為讀者的一個練習專案。
本文中,我們將建立具有略微複雜的資料結構和 DataTables 瀏覽器的 Grails 應用程式。我們將介紹 Grails 標準,它是 Groovy 式的 Java Hibernate 標準。我已將程式碼託管在 GitHub 上方便大家存取,因此本文主要是對程式碼細節的解讀。
首先,你需要設定 Java、Groovy、Grails 的使用環境。對於 Grails,我傾向於使用終端視窗和 Vim,本文也使用它們。為獲得現代的 Java 環境,建議下載並安裝 Linux 發行版提供的 Open Java Development Kit (OpenJDK)(應該是 Java 8、9、10 或 11 之一,撰寫本文時,我正在使用 Java 8)。從我的角度來看,獲取最新的 Groovy 和 Grails 的最佳方法是使用 SDKMAN!。
從未嘗試過 Grails 的讀者可能需要做一些背景資料閱讀。作為初學者,推薦文章 建立你的第一個 Grails 應用程式。
正如上文所提,我將本文中員工資訊瀏覽器的原始碼託管在 GitHub上。進一步講,應用程式 embrow 是在 Linux 終端中用如下命令構建的:
cd Projectsgrails create-app com.nuevaconsulting.embrow
域類和單元測試建立如下:
grails create-domain-class com.nuevaconsulting.embrow.Positiongrails create-domain-class com.nuevaconsulting.embrow.Officegrails create-domain-class com.nuevaconsulting.embrow.Employeecd embrowgrails createdomaincom.grails createdomaincom.grails createdomaincom.
這種方式構建的域類沒有屬性,因此必須按如下方式編輯它們:
Position
域類:
package com.nuevaconsulting.embrow class Position { String name int starting static constraints = { name nullable: false, blank: false starting nullable: false }}com.Stringint startingstatic constraintsnullableblankstarting nullable
Office
域類:
package com.nuevaconsulting.embrow class Office { String name String address String city String country static constraints = { name nullable: false, blank: false address nullable: false, blank: false city nullable: false, blank: false country nullable: false, blank: false }}
Enployee
域類:
package com.nuevaconsulting.embrow class Employee { String surname String givenNames Position position Office office int extension Date hired int salary static constraints = { surname nullable: false, blank: false givenNames nullable: false, blank: false : false office nullable: false extension nullable: false hired nullable: false salary nullable: false }}
請注意,雖然 Position
和 Office
域類使用了預定義的 Groovy 型別 String
以及 int
,但 Employee
域類定義了 Position
和 Office
欄位(以及預定義的 Date
)。這會導致建立資料庫表,其中儲存的 Employee
範例中包含了指向儲存 Position
和 Office
範例表的參照或者外來鍵。
現在你可以生成控制器,檢視,以及其他各種測試元件:
-all com.nuevaconsulting.embrow.Positiongrails generate-all com.nuevaconsulting.embrow.Officegrails generate-all com.nuevaconsulting.embrow.Employeegrails generateall com.grails generateall com.grails generateall com.
此時,你已經準備好了一個基本的增刪改查(CRUD)應用程式。我在 grails-app/init/com/nuevaconsulting/BootStrap.groovy
中包含了一些基礎資料來填充表格。
如果你用如下命令來啟動應用程式:
grails run-app
在瀏覽器輸入 http://localhost:8080/
,你將會看到如下介面:
Embrow 應用程式主介面。
單擊 “OfficeController” 連結,會跳轉到如下介面:
Office 列表
注意,此表由 OfficeController
的 index
方式生成,並由檢視 office/index.gsp
顯示。
同樣,單擊 “EmployeeController” 連結 跳轉到如下介面:
employee 控制器
好吧,這很醜陋: Position 和 Office 連結是什麼?
上面的命令 generate-all
生成的檢視建立了一個叫 index.gsp
的檔案,它使用 Grails <f:table/>
標籤,該標籤預設會顯示類名(com.nuevaconsulting.embrow.Position
)和持久化範例識別符號(30
)。這個操作可以自定義用來產生更好看的東西,並且自動生成連結,自動生成分頁以及自動生成可排序列的一些非常簡潔直觀的東西。
但該員工資訊瀏覽器功能也是有限的。例如,如果想查詢 “position” 資訊中包含 “dev” 的員工該怎麼辦?如果要組合排序,以姓氏為主排序關鍵字,“office” 為輔助排序關鍵字,該怎麼辦?或者,你需要將已排序的資料匯出到電子試算表或 PDF 文件以便通過電子郵件傳送給無法存取瀏覽器的人,該怎麼辦?
jQuery DataTables 外掛提供了這些所需的功能。允許你建立一個完成的表格資料瀏覽器。
要基於 jQuery DataTables 建立員工資訊瀏覽器,你必須先完成以下兩個任務:
在目錄 embrow/grails-app/views/employee
中,首先複製 index.gsp
檔案,重新命名為 browser.gsp
:
cd Projectscd embrow/grails-app/views/employeecp gsp browser.gsp
此刻,你自定義新的 browser.gsp
檔案來新增相關的 jQuery DataTables 程式碼。
通常,在可能的時候,我喜歡從內容提供商處獲得 JavaScript 和 CSS;在下面這行後面:
<title><g:message code="default.list.label" args="[entityName]" /></title>
插入如下程式碼:
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script><link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.css"><script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.js"></script><link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/scroller/1.4.4/css/scroller.dataTables.min.css"><script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/scroller/1.4.4/js/dataTables.scroller.min.js"></script><script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/dataTables.buttons.min.js"></script><script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.flash.min.js"></script><script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js"></script><script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/pdfmake.min.js"></script><script type="text/javascript" charset="utf8" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/vfs_fonts.js"></script><script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.html5.min.js"></script><script type="text/javascript" charset="utf8" src="https://cdn.datatables.net/buttons/1.5.1/js/buttons.print.min.js "></script>
然後刪除 index.gsp
中提供資料分頁的程式碼:
<div id="list-employee" class="content scaffold-list" role="main"><h1><g:message code="default.list.label" args="[entityName]" /></h1><g:if test="${flash.message}"><div class="message" role="status">${flash.message}</div></g:if><f:table collection="${employeeList}" /><div class="pagination"><g:paginate total="${employeeCount ?: 0}" /></div></div>
並插入實現 jQuery DataTables 的程式碼。
要插入的第一部分是 HTML,它將建立瀏覽器的基本表格結構。DataTables 與後端通訊的應用程式來說,它們只提供表格頁首和頁尾;DataTables JavaScript 則負責表中內容。
<div id="employee-browser" class="content" role="main"><h1>Employee Browser</h1><table id="employee_dt" class="display compact" style="width:99%;"><thead><tr><th>Surname</th><th>Given name(s)</th><th>Position</th><th>Office</th><th>Extension</th><th>Hired</th><th>Salary</th></tr></thead><tfoot><tr><th>Surname</th><th>Given name(s)</th><th>Position</th><th>Office</th><th>Extension</th><th>Hired</th><th>Salary</th></tr></tfoot></table></div>
接下來,插入一個 JavaScript 塊,它主要提供三個功能:它設定頁尾中顯示的文字方塊的大小,以進行列過濾,建立 DataTables 表模型,並建立一個處理程式來進行列過濾。
<g:javascript>$('#employee_dt tfoot th').each( function() {javascript
下面的程式碼處理表格列底部的過濾器框的大小:
var title = $(this).text();if (title == 'Extension' || title == 'Hired')$(this).html('<input type="text" size="5" placeholder="' + title + '?" />');else$(this).html('<input type="text" size="15" placeholder="' + title + '?" />');});titletitletitletitletitle
接下來,定義表模型。這是提供所有表選項的地方,包括介面的捲動,而不是分頁,根據 DOM 字串提供的裝飾,將資料匯出為 CSV 和其他格式的能力,以及建立與伺服器的 AJAX 連線。 請注意,使用 Groovy GString 呼叫 Grails createLink()
的方法建立 URL,在 EmployeeController
中指向 browserLister
操作。同樣有趣的是表格列的定義。此資訊將傳送到後端,後端查詢資料庫並返回相應的記錄。
var table = $('#employee_dt').DataTable( {"scrollY": 500,"deferRender": true,"scroller": true,"dom": "Brtip","buttons": [ 'copy', 'csv', 'excel', 'pdf', 'print' ],"processing": true,"serverSide": true,"ajax": {"url": "${createLink(controller: 'employee', action: 'browserLister')}","type": "POST",},"columns": [{ "data": "surname" },{ "data": "givenNames" },{ "data": "position" },{ "data": "office" },{ "data": "extension" },{ "data": "hired" },{ "data": "salary" }]});
最後,監視過濾器列以進行更改,並使用它們來應用過濾器。
table.columns().every(function() {var that = this;$('input', this.footer()).on('keyup change', function(e) {if (that.search() != this.value && 8 < e.keyCode && e.keyCode < 32)that.search(this.value).draw();});
這就是 JavaScript,這樣就完成了對檢視程式碼的更改。
});</g:javascript>
以下是此檢視建立的UI的螢幕截圖:
這是另一個螢幕截圖,顯示了過濾和多列排序(尋找 “position” 包括字元 “dev” 的員工,先按 “office” 排序,然後按姓氏排序):
這是另一個螢幕截圖,顯示單擊 CSV 按鈕時會發生什麼:
最後,這是一個截圖,顯示在 LibreOffice 中開啟的 CSV 資料:
好的,檢視部分看起來非常簡單;因此,控制器必須做所有繁重的工作,對吧? 讓我們來看看……
回想一下,我們看到過這個字串:
"${createLink(controller: 'employee', action: 'browserLister')}"
對於從 DataTables 模型中呼叫 AJAX 的 URL,是在 Grails 伺服器上動態建立 HTML 連結,其 Grails 標記背後通過呼叫 createLink() 的方法實現的。這會最終產生一個指向 EmployeeController
的連結,位於:
embrow/grails-app/controllers/com/nuevaconsulting/embrow/EmployeeController.groovy
特別是控制器方法 browserLister()
。我在程式碼中留了一些 print
語句,以便在執行時能夠在終端看到中間結果。
def browserLister() { // Applies filters and sorting to return a list of desired employees
首先,列印出傳遞給 browserLister()
的引數。我通常使用此程式碼開始構建控制器方法,以便我完全清楚我的控制器正在接收什麼。
println "employee browserLister params $params" println()
接下來,處理這些引數以使它們更加有用。首先,jQuery DataTables 引數,一個名為 jqdtParams
的 Groovy 對映:
def jqdtParams = [:]params.each { key, value -> def keyFields = key.replace(']','').split(/\[/) def table = jqdtParams for (int f = 0; f < keyFields.size() - 1; f++) { def keyField = keyFields[f] if (!table.containsKey(keyField)) table[keyField] = [:] table = table[keyField] } table[keyFields[-1]] = value}println "employee dataTableParams $jqdtParams"println()
接下來,列資料,一個名為 columnMap
的 Groovy 對映:
def columnMap = jqdtParams.columns.collectEntries { k, v -> def whereTerm = null switch (v.data) { case 'extension': case 'hired': case 'salary': if (v.search.value ==~ /\d+(,\d+)*/) whereTerm = v.search.value.split(',').collect { it as Integer } break default: if (v.search.value ==~ /[A-Za-z0-9 ]+/) whereTerm = "%${v.search.value}%" as String break } [(v.data): [where: whereTerm]]}println "employee columnMap $columnMap"println()
接下來,從 columnMap
中檢索的所有列表,以及在檢視中應如何排序這些列表,Groovy 列表分別稱為 allColumnList
和 orderList
:
def allColumnList = columnMap.keySet() as Listprintln "employee allColumnList $allColumnList"def orderList = jqdtParams.order.collect { k, v -> [allColumnList[v.column as Integer], v.dir] }println "employee orderList $orderList"
我們將使用 Grails 的 Hibernate 標準實現來實際選擇要顯示的元素以及它們的排序和分頁。標準要求過濾器關閉;在大多數範例中,這是作為標準範例本身的建立的一部分給出的,但是在這裡我們預先定義過濾器閉包。請注意,在這種情況下,“date hired” 過濾器的相對複雜的解釋被視為一年並應用於建立日期範圍,並使用 createAlias
以允許我們進入相關類別 Position
和 Office
:
def filterer = { createAlias 'position', 'p' createAlias 'office', 'o' if (columnMap.surname.where) ilike 'surname', columnMap.surname.where if (columnMap.givenNames.where) ilike 'givenNames', columnMap.givenNames.where if (columnMap.position.where) ilike 'p.name', columnMap.position.where if (columnMap.office.where) ilike 'o.name', columnMap.office.where if (columnMap.extension.where) inList 'extension', columnMap.extension.where if (columnMap.salary.where) inList 'salary', columnMap.salary.where if (columnMap.hired.where) { if (columnMap.hired.where.size() > 1) { or { columnMap.hired.where.each { between 'hired', Date.parse('yyyy/MM/dd',"${it}/01/01" as String), Date.parse('yyyy/MM/dd',"${it}/12/31" as String) } } } else { between 'hired', Date.parse('yyyy/MM/dd',"${columnMap.hired.where[0]}/01/01" as String), Date.parse('yyyy/MM/dd',"${columnMap.hired.where[0]}/12/31" as String) } }}
是時候應用上述內容了。第一步是獲取分頁程式碼所需的所有 Employee
範例的總數:
def recordsTotal = Employee.count() println "employee recordsTotal $recordsTotal"
接下來,將過濾器應用於 Employee
範例以獲取過濾結果的計數,該結果將始終小於或等於總數(同樣,這是針對分頁程式碼):
def c = Employee.createCriteria() def recordsFiltered = c.count { filterer.delegate = delegate filterer() } println "employee recordsFiltered $recordsFiltered"
獲得這兩個計數後,你還可以使用分頁和排序資訊獲取實際過濾的範例。
def orderer = Employee.withCriteria { filterer.delegate = delegate filterer() orderList.each { oi -> switch (oi[0]) { case 'surname': order 'surname', oi[1]; break case 'givenNames': order 'givenNames', oi[1]; break case 'position': order 'p.name', oi[1]; break case 'office': order 'o.name', oi[1]; break case 'extension': order 'extension', oi[1]; break case 'hired': order 'hired', oi[1]; break case 'salary': order 'salary', oi[1]; break } } maxResults (jqdtParams.length as Integer) firstResult (jqdtParams.start as Integer) }
要完全清楚,JTable 中的分頁程式碼管理三個計數:資料集中的記錄總數,應用過濾器後得到的數位,以及要在頁面上顯示的數位(顯示是捲動還是分頁)。 排序應用於所有過濾的記錄,並且分頁應用於那些過濾的記錄的塊以用於顯示目的。
接下來,處理命令返回的結果,在每行中建立指向 Employee
、Position
和 Office
範例的連結,以便使用者可以單擊這些連結以獲取相關範例的所有詳細資訊:
def dollarFormatter = new DecimalFormat('$##,###.##') def employees = orderer.collect { employee -> ['surname': "<a href='${createLink(controller: 'employee', action: 'show', id: employee.id)}'>${employee.surname}</a>", 'givenNames': employee.givenNames, 'position': "<a href='${createLink(controller: 'position', action: 'show', id: employee.position?.id)}'>${employee.position?.name}</a>", 'office': "<a href='${createLink(controller: 'office', action: 'show', id: employee.office?.id)}'>${employee.office?.name}</a>", 'extension': employee.extension, 'hired': employee.hired.format('yyyy/MM/dd'), 'salary': dollarFormatter.format(employee.salary)] }
最後,建立要返回的結果並將其作為 JSON 返回,這是 jQuery DataTables 所需要的。
def result = [draw: jqdtParams.draw, recordsTotal: recordsTotal, recordsFiltered: recordsFiltered, data: employees] render(result as JSON) }
大功告成。
如果你熟悉 Grails,這可能看起來比你原先想象的要多,但這裡沒有火箭式的一步到位方法,只是很多分散的操作步驟。但是,如果你沒有太多接觸 Grails(或 Groovy),那麼需要了解很多新東西 - 閉包,代理和構建器等等。
在那種情況下,從哪裡開始? 最好的地方是了解 Groovy 本身,尤其是 Groovy closures 和 Groovy delegates and builders。然後再去閱讀上面關於 Grails 和 Hibernate 條件查詢的建議閱讀文章。
jQuery DataTables 為 Grails 製作了很棒的表格資料瀏覽器。對檢視進行編碼並不是太棘手,但 DataTables 文件中提供的 PHP 範例提供的功能僅到此位置。特別是,它們不是用 Grails 程式設計師編寫的,也不包含探索使用參照其他類(實質上是查詢表)的元素的更精細的細節。
我使用這種方法製作了幾個資料瀏覽器,允許使用者選擇要檢視和累積記錄計數的列,或者只是瀏覽資料。即使在相對適度的 VPS 上的百萬行表中,效能也很好。
一個警告:我偶然發現了 Grails 中暴露的各種 Hibernate 標準機制的一些問題(請參閱我的其他 GitHub 程式碼庫),因此需要謹慎和實驗。如果所有其他方法都失敗了,另一種方法是動態構建 SQL 字串併執行它們。在撰寫本文時,我更喜歡使用 Grails 標準,除非我遇到雜亂的子查詢,但這可能只反映了我在 Hibernate 中對子查詢的相對缺乏經驗。
我希望 Grails 程式設計師發現本文的有趣性。請隨時在下面留下評論或建議。