今天開始講解Esper的重中之重——EPL。EPL可以說是Esper的核心,要是不會將簡單的業務需求轉化爲EPL,更別說複雜的EPL了,最後就等着被客戶罵吧。接下來的很多篇都會圍繞EPL講解,大概會有6篇吧,畢竟英文文檔都有140頁了,草草兩篇根本就說不完。廢話不多說,先簡單介紹下什麼是EPL,即使第一篇有說過,但是這裏有必要細說一下。
EPL,全稱Event Processing Language,是一種類似SQL的語言,包含了SELECT, FROM, WHERE, GROUP BY, HAVING 和 ORDER BY子句,同時用事件流代替了table作爲數據源,並且能像SQL那樣join,filtering和aggregation。所以如果各位有SQL基礎的話,簡單的EPL很容易掌握。除了select,EPL也有insert into,update,delete,不過含義和SQL並不是很接近。另外還有pattern和output子句,這兩個是SQL所沒有的。EPL還定義了一個叫view的東西,類似SQL的table,來決定哪些數據是可用的,Esper提供了十多個view,並且保證這些view可以被重複使用。而且用戶還可以擴展view成爲自定義view來滿足需求。在view的基礎上,EPL還提供了named window的定義,作用和view類似,但是更加靈活。。。還有很多東西,我再列舉估計大家都要暈了,就先說說語法吧。
1.EPL Syntax
- [annotations]
- [expression_declarations]
- [context context_name]
- [insert into insert_into_def]
- select select_list
- from stream_def [as name] [, stream_def [as name]] [,...]
- [where search_conditions]
- [group by grouping_expression_list]
- [having grouping_search_conditions]
- [output output_specification]
- [order by order_by_expression_list]
- [limit num_rows]
基本上大部分的EPL都是按照這個格式來定義。看過之前幾篇的同學應該對裏面的某些內容熟悉,比如context context_name,select select_list from stream_def等等。其他的可以先不關心,後面會有詳解。比如wher,group by都會有專門的篇幅進行描述。
2.Time Periods
這部分的內容說的是Esper中時間的表達形式。語法如下:
- time-period : [year-part] [month-part] [week-part] [day-part] [hour-part]
- [minute-part] [seconds-part] [milliseconds-part]
- year-part : (number|variable_name) ("years" | "year")
- month-part : (number|variable_name) ("months" | "month")
- week-part : (number|variable_name) ("weeks" | "week")
- day-part : (number|variable_name) ("days" | "day")
- hour-part : (number|variable_name) ("hours" | "hour")
- minute-part : (number|variable_name) ("minutes" | "minute" | "min")
- seconds-part : (number|variable_name) ("seconds" | "second" | "sec")
- milliseconds-part : (number|variable_name) ("milliseconds" | "millisecond" |
- "msec")
與時間相關的EPL基本都會用到上面列出的東西,舉幾個例子說明下:
- // 計算過去的5分3秒中進入改語句的Fruit事件的平均price
- select avg(price) from Fruit.win:time(5 minute 3 sec)
- // 每一天輸出一次用戶的賬戶總額
- select sum(account) from User output every 1 day
用法比較簡單,大家可以多試試。要注意的是,Esper規定每月的天數都是30天,所以對準確性要求高的業務,以月爲單位進行計算會出現誤差的。
3.Comments
註釋基本上和Java差不多,只不過他沒有/** */和/* */之分,只有/* */和//,畢竟不需要生成文檔,所以就沒那個必要了。//只能單行註釋,而/* */可以多行註釋。示例如下:
- a.單行註釋
- // This comment extends to the end of the line.
- // Two forward slashes with no whitespace between them begin such comments.
- select * from MyEvent
- b.多行註釋
- select * from OtherEvent
- /* this is a very
- *important Event */
- c.混合註釋
- select field1 // first comment
- /* second comment */ field2 from MyEvent
4.Reserved Keywords
EPL裏如果某個事件屬性,或者事件流的名稱和EPL的關鍵字一樣,那麼必須要以`括起來纔可用,`在鍵盤上esc的下面,1的左邊,叫什麼忘記了,估計說出來也有很多人不知道。比如:
- // insert和Order是關鍵字,這個EPL無效
- select insert from Order
- // `insert`和`Order`是屬性名稱和事件流名稱,這個EPL有效
- select `insert` from `Order`
5.Escaping Strings
在EPL中,字符串使用單引號或者雙引號括起來的,那如果字符串裏包含有單引號或者雙引號怎麼辦呢。請看例子:
- select * from OrderEvent(name='John')
- // 等同於
- select * from OrderEvent(name="John")
如果name=John's,則需要反斜槓進行轉義。
- select * from OrderEvent(name="John\"s")
- // 或者
- select * from OrderEvent(name='john\'s')
除了使用反斜槓,還可以使用unicode來表示單引號和雙引號。
- select * from OrderEvent(name="John\u0022s")
- // 或者
- select * from OrderEvent(name='john\u0027s')
注意在Java編寫EPL的時候,反斜槓和無含義的雙引號還得轉義,不然會和String的雙引號衝突。比如
- epService.getEPAdministrator().createEPL("select * from OrderEvent(name='John\\'s')");
- // ... and for double quotes...
- epService.getEPAdministrator().createEPL("select * from OrderEvent(name=\"Quote \\\"Hello\\\"\")");
6.Data Types
EPL支持Java所有的數值數據類型,包括基本類型及其包裝類,同時還支持java.math.BigInteger和java.math.BigDecimal,並且能自動轉換數據類型不丟失精度(比如short轉int,int轉short則不行)。如果想在EPL內進行數據轉換,可以使用cast函數。完整例子如下:
- import com.espertech.esper.client.EPAdministrator;
- import com.espertech.esper.client.EPRuntime;
- import com.espertech.esper.client.EPServiceProvider;
- import com.espertech.esper.client.EPServiceProviderManager;
- import com.espertech.esper.client.EPStatement;
- import com.espertech.esper.client.EventBean;
- import com.espertech.esper.client.UpdateListener;
- /**
- * 可用cast函數將其他的數值數據類型轉爲BigDecimal。
- *
- * @author luonanqin
- *
- */
- class Banana
- {
- private int price;
- public int getPrice()
- {
- return price;
- }
- public void setPrice(int price)
- {
- this.price = price;
- }
- }
- class CastDataTypeListener1 implements UpdateListener
- {
- public void update(EventBean[] newEvents, EventBean[] oldEvents)
- {
- if (newEvents != null)
- {
- EventBean event = newEvents[0];
- // cast(avg(price), int)中間的空格在EPL中可以不寫,但是event.get的時候必須加上,建議用as一個別名來代表轉換後的值
- System.out.println("Average Price: " + event.get("cast(avg(price), int)") + ", DataType is "
- + event.get("cast(avg(price), int)").getClass().getName());
- }
- }
- }
- class CastDataTypeListener2 implements UpdateListener
- {
- public void update(EventBean[] newEvents, EventBean[] oldEvents)
- {
- if (newEvents != null)
- {
- EventBean event = newEvents[0];
- System.out.println("Average Price: " + event.get("avg(price)") + ", DataType is " + event.get("avg(price)").getClass().getName());
- }
- }
- }
- public class CastDataTypeTest
- {
- public static void main(String[] args) throws InterruptedException
- {
- EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();
- EPAdministrator admin = epService.getEPAdministrator();
- String banana = Banana.class.getName();
- String epl1 = "select cast(avg(price),int) from " + banana + ".win:length_batch(2)";
- String epl2 = "select avg(price) from " + banana + ".win:length_batch(2)";
- EPStatement state1 = admin.createEPL(epl1);
- state1.addListener(new CastDataTypeListener1());
- EPStatement state2 = admin.createEPL(epl2);
- state2.addListener(new CastDataTypeListener2());
- EPRuntime runtime = epService.getEPRuntime();
- Banana b1 = new Banana();
- b1.setPrice(1);
- runtime.sendEvent(b1);
- Banana b2 = new Banana();
- b2.setPrice(2);
- runtime.sendEvent(b2);
- }
- }
執行結果:
- Average Price: 1, DataType is java.lang.Integer
- Average Price: 1.5, DataType is java.lang.Double
要提醒的是,如果某個數除以0,那麼默認會返回正無窮大或者負無窮大,不過可以配置這個結果,比如用null來代替。
7.Annotation
EPL也可以寫註解,種類不多,大部分簡單而有效。不過有些註解內容較多,在這裏只是簡單介紹,以後會在具體的使用場景進行詳細講解。首先來了解下註解的語法。
- // 不包含參數或者單個參數的註解
- @annotation_name [(annotation_parameters)]
- // 包含多個屬性名-值對的註解
- @annotation_name (attribute_name = attribute_value, [name=value, ...])
- // 多個註解聯合使用
- @annotation_name [(annotation_parameters)] [@annotation_name [(annotation_parameters)]] [...]
下面講解具體註解時,會結合語法進行說明。
a) @Name 指定EPL的名稱,參數只有一個。例如:@Name("MyEPL")
b) @Description 對EPL進行描述,參數只有一個。例如:@Description("This is MyEPL")
c) @Tag 對EPL進行額外的說明,參數有兩個,分別爲Tag的名稱和Tag的值,用逗號分隔。例如:@Tag(name="author",value="luonanqin")
d) @Priority 指定EPL的優先級,參數只有一個,並且整數(可負可正)。例如:@Priority(10)
e) @Drop 指定事件經過此EPL後不再參與其他的EPL計算,該註解無參數。
f) @Hint 爲EPL加上某些標記,讓引擎對此EPL產生其他的操作,會改變EPL實例的內存佔用,但通常不會改變輸出。其參數固定,由Esper提供,之後的篇幅會穿插講解這個註解的使用場景。
g) @Audit EPL添加此註解後,可以額外輸出EPL運行情況,有點類似日誌的感覺(當然沒有日誌的功能全啦),具體使用場景在此先不提。
h) @Hook 與SQL相關,這裏暫且不說
i) @EventRepresentation 這是用來指定EPL產生的計算結果事件包含的數據形式。參數只有一個,即array=true或array=false。false爲默認值,代表數據形式爲Map,若爲true,則數據形式爲數組。
針對以上幾個簡單的註解,例子如下:
- import com.espertech.esper.client.EPAdministrator;
- import com.espertech.esper.client.EPRuntime;
- import com.espertech.esper.client.EPServiceProvider;
- import com.espertech.esper.client.EPServiceProviderManager;
- import com.espertech.esper.client.EPStatement;
- import com.espertech.esper.client.EventBean;
- import com.espertech.esper.client.UpdateListener;
- class Apple
- {
- private int price;
- public int getPrice()
- {
- return price;
- }
- public void setPrice(int price)
- {
- this.price = price;
- }
- }
- class SomeAnnotationListener implements UpdateListener
- {
- public void update(EventBean[] newEvents, EventBean[] oldEvents)
- {
- if (newEvents != null)
- {
- EventBean event = newEvents[0];
- // 當加上註解@EventRepresentation(array=true)時,結果事件類型爲數組而不是Map。
- // array=false時,也就是默認情況,結果事件類型爲數組是Map。
- System.out.println("Sum Price: " + event.get("sum(price)") + ", Event Type is " + event.getEventType().getUnderlyingType());
- }
- }
- }
- public class SomeAnnotationTest
- {
- public static void main(String[] args) throws InterruptedException
- {
- EPServiceProvider epService = EPServiceProviderManager.getDefaultProvider();
- EPAdministrator admin = epService.getEPAdministrator();
- String apple = Apple.class.getName();
- String epl1 = "@Priority(10)@EventRepresentation(array=true) select sum(price) from " + apple + ".win:length_batch(2)";
- String epl2 = "@Name(\"EPL2\")select sum(price) from " + apple + ".win:length_batch(2)";
- String epl3 = "@Drop select sum(price) from " + apple + ".win:length_batch(2)";
- UpdateListener listenenr = new SomeAnnotationListener();
- EPStatement state1 = admin.createEPL(epl1);
- state1.addListener(listenenr);
- EPStatement state2 = admin.createEPL(epl2);
- state2.addListener(listenenr);
- System.out.println("epl2's name is " + state2.getName());
- EPStatement state3 = admin.createEPL(epl3);
- state3.addListener(listenenr);
- EPRuntime runtime = epService.getEPRuntime();
- Apple a1 = new Apple();
- a1.setPrice(1);
- runtime.sendEvent(a1);
- Apple a2 = new Apple();
- a2.setPrice(2);
- runtime.sendEvent(a2);
- }
- }
執行結果:
- epl2's name is EPL2
- Sum Price: 3, Event Type is class [Ljava.lang.Object;
- Sum Price: 3, Event Type is interface java.util.Map
- Sum Price: 3, Event Type is interface java.util.Map
8.Expression
Expression類似自定義函數,通常用Lambda表達式來建立的(也有別的方法建立),而Lambda表達式就一個“ => ”符號,表示“gose to”。符號的左邊表示輸入參數,符號右邊表示計算過程,計算結果就是這個表達式的返回值,即Expression的返回值。語法如下:
- expression expression_name { expression_body }
- expression_body: (input_param [,input_param [,...]]) => expression
- expression middle { x => (x.max+x.min)/2 } select middle(apple) from Apple as apple
2. express的定義必須在使用它的句子之前完成。使用時直接寫expression的名字和用圓括號包含要計算的參數即可。再次提醒,expression的參數只能是事件流別名,即apple,別名的定義就如上面那樣,事件流之後跟着as,然後再跟別名。
上面這個句子的執行結果,就是middle的計算結果,各位自己試試吧。
對於多個參數的expression定義,例子如下:
- expression sumage { (x,y) => x.age+y.age } select sumage(me,you) from Me as me, You as you
expression_body除了可以用lambda表達式之外,還可以用聚合函數,變量,常量,子查詢語句,甚至另一個表達式。子查詢語句在沒有in或者exist關鍵字的情況下,需要圓括號括起來。示例如下:
- expression newsSubq(md) {
- (select sentiment from NewsEvent.std:unique(symbol) where symbol = md.symbol)
- }
- select newsSubq(mdstream) from MarketDataEvent mdstream
- expression twoPI { Math.PI * 2} select twoPI() from SampleEvent
對於expression裏用另一個expression,EPL不允許在一個句子裏建立兩個expression,所以就出現了Global-Expression。普通的expression只作用於定義它的epl,如上面所有的包含select子句的epl就是如此。Global-Expression的語法如下:
- create expression expression_name { expression_body }
- epService.getEPAdministrator().createEPL("create expression avgPrice { x => (x.fist+x.last)/2 }");
最後再舉個例子說一下某個expression裏用另一個expression。
- // 先定義全局的avgPrice
- create expression avgPrice { x => (x.fist+x.last)/2 }
- // bananaPrice Banana事件中包含了first和last屬性,否則將報錯
- expression bananaPrice{ x => avgPrice(x) } select bananaPrice(b) from Banana as b
expression_body裏的空格只是爲了看着清晰,有沒有空格不會有影響。
對於EPL的語法,今天的內容先說到這,各位消化起來應該還是很快的,如果有不明白的可以找我或者看看文檔。下篇將講解select子句和from子句,敬請期待。