摘要:如果我們在Java中也提供有一套完整的結構化資料處理和計算類庫,那這個問題就能得到解決:即享受到架構的優勢,又不致於降低開發效率。
本文分享自華為雲社群《Java結構化處理SPL》,作者:石臻臻的雜貨鋪。
現代Java應用架構越來越強調資料儲存和處理分離,以獲得更好的可維護性、可擴充套件性以及可移植性,比如火熱的微服務就是一種典型。這種架構通常要求業務邏輯要在Java程式中實現,而不是像傳統應用架構中放在資料庫中。
應用中的業務邏輯大都會涉及結構化資料處理。資料庫(SQL)中對這類任務有較豐富的支援,可以相對簡易地實現業務邏輯。但Java卻一直缺乏這類基礎支援,導致用Java實現業務邏輯非常繁瑣低效。結果,雖然架構上有各種優勢,但開發效率卻反而大幅下降了。
如果我們在Java中也提供有一套完整的結構化資料處理和計算類庫,那這個問題就能得到解決:即享受到架構的優勢,又不致於降低開發效率。
需要什麼樣的能力?
Java下理想的結構化資料處理類庫應當具備哪些特徵呢?我們可以從SQL來總結:
1 集合運算能力
結構化資料經常是批量(以集合形式)出現的,為了方便地計算這類資料,有必要提供足夠的集合運算能力。
如果沒有集合運算類庫,只有陣列(相當於集合)這種基礎資料型別,我們要對集合成員做個簡單地求和也需要寫四五行迴圈語句才能完成,過濾、分組聚合等運算則要寫出數百行程式碼了。
SQL提供有較豐富的集合運算,如 SUM/COUNT 等聚合運算,WHERE 用於過濾、GROUP 用於分組,也支援針對集合的交、並、差等基本運算。這樣寫出來的程式碼就會短小很多。
2 Lambda語法
有了集合運算能力是否就夠了呢?假如我們為 Java 開發一批的集合運算類庫,是否就可以達到 SQL 的效果呢?
沒有這麼簡單!
以過濾運算為例。過濾通常需要一個條件,把滿足條件的集合成員保留。在 SQL 中這個條件是以一個表示式形式出現的,比如寫 WHERE x>0,就表示保留那些使得 x>0 計算結果為真的成員。這個表示式 x>0 並不是在執行這個語句之前先計算好的,而是在遍歷時針對每個集合成員計算的。本質上,這個表示式本質上是一個函式,是一個以當前集合成員為引數的函式。對於 WHERE 運算而言,相當於把一個用表示式定義的函式用作了 WHERE 的引數。
這種寫法有一個術語叫做 Lambda 語法,或者叫函式式語言。
如果沒有 Lambda 語法,我們就要經常臨時定義函式,程式碼會非常繁瑣,還容易發生名字衝突。
SQL中大量使用了 Lambda 語法,不在於必須過濾、分組運算中,在計算列等不必須的場景也可以使用,大大簡化了程式碼。
3 在 Lambda 語法中直接引用欄位
結構化資料並非簡單的單值,而是帶有欄位的記錄。
我們發現,SQL 的表示式引數中引用記錄欄位時,大多數情況可以直接使用欄位名稱而不必指明欄位所屬的記錄,只有在多個同名欄位時才需要冠以表名(或別名)以區分。
新版本的 Java 雖然也開始支援 Lambda 語法了,但只能把當前記錄作為引數傳入這個用 Lambda 語法定義的函式,然後再寫計算式時就總要帶上這個記錄。比如用單價和數量計算金額時,如果用於表示當前成員的引數名為 x,則需要寫成“x. 單價 *x. 數量”這種囉嗦的形式。而在 SQL 中可以更為直觀地寫成 " 單價 * 數量”。
4 動態資料結構
SQL還能很好地支援動態資料結構。
結構化資料計算中,返回值經常也是有結構的資料,而結果資料結構和運算相關,沒辦法在程式碼編寫之前就先準備好。所以需要支援動態的資料結構能力。
SQL中任何一個 SELECT 語句都會產生一個新的資料結構,在程式碼中可以隨意新增刪除欄位,而不必事先定義結構(類)。Java 這類語言則不行,在程式碼編譯階段就要把用到的結構(類)都定義好,原則上不能在執行過程中動態產生新的結構。
5 解釋型語言
從前面幾條的分析,我們已經可以得到結論:Java 本身並不適合用作結構化資料處理的語言。它的 Lambda 機制不支援特徵 3,而且作為編譯型語言,也不能實現特徵 4。
其實,前面說到的 Lambda 語法也不太適合採用編譯型語言來實現。編譯器不能確定這個寫到引數位置的表示式是應該當場計算出表示式的值再傳遞,還是把整個表示式編譯成一個函式傳遞,需要再設計更多的語法符號加以區分。而解釋型語言則沒有這個問題,作為引數的表示式是先計算還是遍歷集合成員時再計算,可以由函式本身來決定。
SQL確實是解釋型語言。
引入 SPL
Stream是Java8以官方身份推出的結構化資料處理類庫,但並不符合上述的要求。它沒有專業的結構化資料型別,缺乏很多重要的結構化資料計算函式,不是解釋型語言,不支援動態資料型別,Lambda語法的介面複雜。
Kotlin屬於Java生態系統的一部分,它在Stream的基礎上進行了小幅改進,也提供了結構化資料計算型別,但因為結構化資料計算函式不足,不是解釋型語言,不支援動態資料型別,Lambda語法的介面複雜,仍然不是理想的結構化資料計算類庫。
Scala提供了較豐富的結構化資料計算函式,但編譯型語言的特點,也使它不能成為理想的結構化資料計算類庫。
那麼,Java生態下還有什麼可以用呢?
集算器SPL。
SPL是由Java解釋執行的程式語言,具備豐富的結構化資料計算類庫、簡單的Lambda語法和方便易用的動態資料結構,是Java理想的結構化處理類庫。
豐富的集合運算函式
SPL提供了專業的結構化資料型別,即序表。和SQL的資料表一樣,序表是批量記錄組成的集合,具有結構化資料型別的一般功能,下面舉例說明。
解析源資料並生成序表:
Orders=T("d:/Orders.csv")
按列名從原序表生成新的序表:
Orders.new(OrderID, Amount, OrderDate)
計算列:
Orders.new(OrderID, Amount, year(OrderDate))
欄位改名:
Orders.new(OrderID:ID, SellerId, year(OrderDate):y)
按序號使用欄位:
Orders.groups(year(_5),_2; sum(_4))
序表改名(左關聯)
join@1(Orders:o,SellerId ; Employees:e,EId).groups(e.Dept; sum(o.Amount))
序表支援所有的結構化計算函式,計算結果也同樣是序表,而不是Map之類的資料型別。比如對分組彙總的結果,繼續進行結構化資料處理:
Orders.groups(year(OrderDate):y; sum(Amount):m).new(y:OrderYear, m*0.2:discount)
在序表的基礎上,SPL提供了豐富的結構化資料計算函式,比如過濾、排序、分組、去重、改名、計算列、關聯、子查詢、集合計算、有序計算等。這些函式具有強大的計算能力,無須硬編碼輔助,就能獨立完成計算:
組合查詢:
Orders.select(Amount>1000 && Amount<=3000 && like(Client,"*bro*"))
排序:
Orders.sort(-Client,Amount)
分組彙總:
Orders.groups(year(OrderDate),Client; sum(Amount))
內關聯:
join(Orders:o,SellerId ; Employees:e,EId).groups(e.Dept; sum(o.Amount))
簡潔的Lambda語法
SPL支援簡單的Lambda語法,無須定義函式名和函式體,可以直接用表示式當作函式的引數,比如過濾:
Orders.select(Amount>1000)
修改業務邏輯時,也不用重構函式,只須簡單修改表示式:
Orders.select(Amount>1000 && Amount<2000)
SPL是解釋型語言,使用參數列達式時不必明確定義引數型別,使Lambda介面更簡單。比如計算平方和,想在sum的過程中算平方,可以直觀寫作:
Orders.sum(Amount*Amount)
和SQL類似,SPL語法也支援在單表計算時直接使用欄位名:
Orders.sort(-Client, Amount)
動態資料結構
SPL是解釋型語言,天然支援動態資料結構,可以根據計算結果結構動態生成新序表。特別適合計算列、分組彙總、關聯這類計算,比如直接對分組彙總的結果再計算:
Orders.groups(Client;sum(Amount):amt).select(amt>1000 && like(Client,"*S*"))
或直接對關聯計算的結果再計算:
join(Orders:o,SellerId ; Employees:e,Eid).groups(e.Dept; sum(o.Amount))
較複雜的計算通常都要拆成多個步驟,每個中間結果的資料結構幾乎都不同。SPL支援動態資料結構,不必先定義這些中間結果的結構。比如,根據某年的客戶回款記錄表,計算每個月的回款額都在前10名的客戶:
Sales2021.group(month(sellDate)).(~.groups(Client;sum(Amount):sumValue)).(~.sort(-sumValue)) .(~.select(#<=10)).(~.(Client)).isect()
直接執行SQL
SPL中還實現了SQL的直譯器,可以直接執行SQL,從基本的WHERE、GROUP到JOIN、甚至WITH都能支援:
$select * from d:/Orders.csv where (OrderDate<date('2020-01-01') and Amount<=100)or (OrderDate>=date('2020-12-31') and Amount>100) $select year(OrderDate),Client ,sum(Amount),count(1) from d:/Orders.csv group by year(OrderDate),Client having sum(Amount)<=100 $select o.OrderId,o.Client,e.Name e.Dept from d:/Orders.csv o join d:/Employees.csv e on o.SellerId=e.Eid $with t as (select Client ,sum(amount) s from d:/Orders.csv group by Client) select t.Client, t.s, ct.Name, ct.address from t left join ClientTable ct on t.Client=ct.Client
更多語言優勢
作為專業的結構化資料處理語言,SPL不僅覆蓋了SQL的所有計算能力,在語言方面,還有更強大的優勢:
離散性及其支掛下的更徹底的集合化
集合化是SQL的基本特性,即支援資料以集合的形式參與運算。但SQL的離散性很不好,所有集合成員必須作為一個整體參於運算,不能遊離在集合之外。而Java等高階語言則支援很好的離散性,陣列成員可以單獨運算。
但是,更徹底的集合化需要離散性來支援,集合成員可以遊離在集合之外,並與其它資料隨意構成新的集合參與運算 。
SPL兼具了SQL的集合化和Java的離散性,從而可以實現更徹底的集合化。
比如,SPL中很容易表達“集合的集合”,適合分組後計算。比如,找到各科成績均在前10名的學生:
SPL序表的欄位可以儲存記錄或記錄集合,這樣可以用物件引用的方式,直觀地表達關聯關係,即使關係再多,也能直觀地表達。比如,根據員工表找到女經理下屬的男員工:
Employees.select(性別:"男",部門.經理.性別:"女")
有序計算是離散性和集合化的典型結合產物,成員的次序在集合中才有意義,這要求集合化,有序計算時又要將每個成員與相鄰成員區分開,會強調離散性。SPL兼具集合化和離散性,天然支援有序計算。
具體來說,SPL可以按絕對位置引用成員,比如,取第3條訂單可以寫成Orders(3),取第1、3、5條記錄可以寫成Orders([1,3,5])。
SPL也可以按相對位置引用成員,比如,計算每條記錄相對於上一條記錄的金額增長率:Orders.derive(amount/amount[-1]-1)
SPL還可以用#代表當前記錄的序號,比如把員工按序號分成兩組,奇數序號一組,偶數序號一組:Employees.group(#%2==1)
更方便的函式語法
大量功能強大的結構化資料計算函式,這本來是一件好事,但這會讓相似功能的函式不容易區分。無形中提高了學習難度。
SPL提供了特有的函式選項語法,功能相似的函式可以共用一個函式名,只用函式選項區分差別。比如select函式的基本功能是過濾,如果只過濾出符合條件的第1條記錄,只須使用選項@1:
Orders.select@1(Amount>1000)
資料量較大時,用平行計算提高效能,只須改為選項@m:
Orders.select@m(Amount>1000)
對排序過的資料,用二分法進行快速過濾,可用@b:
Orders.select@b(Amount>1000)
函式選項還可以組合搭配,比如:
Orders.select@1b(Amount>1000)
結構化運算函式的引數常常很複雜,比如SQL就需要用各種關鍵字把一條語句的引數分隔成多個組,但這會動用很多關鍵字,也使語句結構不統一。
SPL支援層次引數,通過分號、逗號、冒號自高而低將引數分為三層,用通用的方式簡化複雜引數的表達:
join(Orders:o,SellerId ; Employees:e,EId)
擴充套件的Lambda語法
普通的Lambda語法不僅要指明表示式(即函式形式的引數),還必須完整地定義表示式本身的引數,否則在數學形式上不夠嚴密,這就讓Lambda語法很繁瑣。比如用迴圈函式select過濾集合A,只保留值為偶數的成員,一般形式是:
A.select(f(x):{x%2==0} )
這裡的表示式是x%2==0,表示式的引數是f(x)裡的x,x代表集合A裡的成員,即迴圈變數。
SPL用固定符號~代表迴圈變數,當引數是迴圈變數時就無須再定義引數了。在SPL中,上面的Lambda語法可以簡寫作:A.select(~ %2==0)
普通Lambda語法必須定義表示式用到的每一個引數,除了迴圈變數外,常用的引數還有迴圈計數,如果把迴圈計數也定義到Lambda中,程式碼就更繁瑣了。
SPL用固定符號#代表迴圈計數變數。比如,用函式select過濾集合A,只保留序號是偶數的成員,SPL可以寫作:A.select(# %2==0)
相對位置經常出現在難度較大的計算中,而且相對位置本身就很難計算,當要使用相對位置時,引數的寫法將非常繁瑣。
SPL用固定形式[序號]代表相對位置:
無縫整合、低耦合、熱切換
作為用Java解釋的指令碼語言,SPL提供了JDBC驅動,可以無縫整合進Java應用程中。
簡單語句可以像SQL一樣直接執行:
… Class.forName("com.esproc.jdbc.InternalDriver"); Connection conn =DriverManager.getConnection("jdbc:esproc:local://"); PrepareStatement st = conn.prepareStatement("=T(\"D:/Orders.txt\").select(Amount>1000 && Amount<=3000 && like(Client,\"*S*\"))"); ResultSet result=st.execute(); ...
複雜計算可以存成指令碼檔案,以儲存過程方式呼叫
… Class.forName("com.esproc.jdbc.InternalDriver"); Connection conn =DriverManager.getConnection("jdbc:esproc:local://"); Statement st = connection.(); CallableStatement st = conn.prepareCall("{call splscript1(?, ?)}"); st.setObject(1, 3000); st.setObject(2, 5000); ResultSet result=st.execute(); ...
將指令碼外接於Java程式,一方面可以降低程式碼耦合性,另一方面利用解釋執行的特點還可以支援熱切換,業務邏輯變動時只要修改指令碼即可立即生效,不像使用Java時常常要重啟整個應用。這種機制特別適合編寫微服務架構中的業務處理邏輯。
SPL資料