作為一個自認(rèn)為特別有人文情懷的人,始終覺得寫技術(shù)貼特別沒勁兒赞警,于是“云上的世界”這個欄目基本上不死不活地空置了兩年琼蚯,最近突然覺得這么空下去挺對不起這個浪漫的專欄名赐俗,于是決定從即日開始阻逮,在這里寫一寫自己在force.com這個龐大的生態(tài)系統(tǒng)中探索時的些許感悟和積累与柑。
Salesforce在北美歐洲澳大利亞日本等地非常地不可一世,但在中國卻因為價格策略市場認(rèn)知等因素夜郁,再加上本地競爭者的有力阻擊,基本上還處在蓄勢待發(fā)的狀態(tài)庙睡,不用說普通讀者事富,即便對大多數(shù)互聯(lián)網(wǎng)或IT從業(yè)者而言,也還是一個相對比較新鮮的事物乘陪。
因此统台,我決定當(dāng)一個布道者。
雖然也了解的不多啡邑,但就當(dāng)投石問路贱勃,說不定有讀者看了這些文章之后決定成為一個Salesforce的管理員、開發(fā)者谤逼、咨詢顧問贵扰、架構(gòu)師呢,要是再能由此遇到一些志同道合的朋友流部,那就更完美了戚绕。
好了,閑話少說枝冀,直奔主題吧舞丛。
首先預(yù)告一下耘子,“Lightning Connect”這個系列都將會是純技術(shù)貼 - 我會努力把技術(shù)貼和和非技術(shù)貼分開,這樣瓷马,不同興趣的讀者可以各取所需 - 如果你不是Salesforce的開發(fā)者拴还,這篇文章可能不適合你。
Salesforce在Winter '15的產(chǎn)品發(fā)布當(dāng)中第一次介紹了Lightning Connect - 這是一個新的將Salesforce與外部數(shù)據(jù)源進(jìn)行整合的接口欧聘。
簡而言之片林,它允許你用非ETL的方式(這么說聽起來貌似很高端,其實說白了就是通過HTTP Callout來獲取數(shù)據(jù))將外部數(shù)據(jù)源集成到Salesforce中來怀骤,創(chuàng)建所謂的external object费封,并承諾這些external object和那些生存在Salesforce自己底端Oracle數(shù)據(jù)庫中的standard object或custom object有相似的行徑與平等的地位。
Lightning Connect本身的設(shè)置并不復(fù)雜蒋伦,Salesforce的免費在線教學(xué)網(wǎng)站Trailhead中有專門的一個Module來介紹弓摘,所以我就不贅言了,有興趣的朋友可以自行移步查看痕届。
https://developer.salesforce.com/trailhead/module/lightning_connect
然而韧献,Lightning Connect的局限性在于它只能識別那些符合OData協(xié)議的數(shù)據(jù)源。
即便一些大公司的產(chǎn)品研叫,比如SAP或Microsoft Dynamics锤窑,都有基于OData協(xié)議的數(shù)據(jù)接口,但畢竟不甚方便嚷炉,于是在Summer '15的產(chǎn)品發(fā)布中渊啰,Salesforce推出了一個讓人激動的new feature,也就是這篇文章的主題 - Lightning Connect Custom Adapters申屹。
Lightning Connect Custom Adapters绘证,或者叫做Apex Connect Framework,允許開發(fā)者自己用Apex來編寫自定義適配器(Custom Adapter)哗讥,然后實現(xiàn)和任何格式或協(xié)議的數(shù)據(jù)源進(jìn)行實時的完美集成嚷那。
Apex為此增加了一個叫做DataSource的命名空間,所有新的方法和數(shù)據(jù)類型都在此命名空間之下定義杆煞。作為開發(fā)者來說魏宽,需要做的只有兩件事:
1. 創(chuàng)建一個Apex class,繼承DataSource.Provider這個接口索绪,實現(xiàn)接口里預(yù)定義的函數(shù)湖员;
2. 創(chuàng)建另一個Apex class,繼承DataSource.DataSourceConnection這個接口贫悄,然后實現(xiàn)接口里預(yù)定義的函數(shù)瑞驱。
這兩個Apex class完成之后,在創(chuàng)建外部數(shù)據(jù)源時你會在Type下拉菜單中看到你自己的DataSource.Provider窄坦,選擇該Provider唤反,這個新的外部數(shù)據(jù)源就和你自定義的Adapter聯(lián)系起來了凳寺。
在新創(chuàng)建的數(shù)據(jù)源中點擊“Validate and Sync”按鈕(該截圖是取自Edit一個已有的數(shù)據(jù)源,所以只有“Sync”按鈕)彤侍,這時后臺代碼會調(diào)用DataSource.DataSourceConnection那個Apex class中的sync()函數(shù)(后文中會看到)肠缨,?并返回外部數(shù)據(jù)源中所有的表信息(該例中外部數(shù)據(jù)源只包含了一個叫做“Looper”的表,真實環(huán)境中每個外部數(shù)據(jù)源可能含有多個表)盏阶。
選擇你想要同步的表晒奕,然后進(jìn)行同步,Salesforce則會按照你在sync()函數(shù)中的定義創(chuàng)建相應(yīng)的外部數(shù)據(jù)(external object)名斟。如果你點擊新生成的外部數(shù)據(jù)查看其詳細(xì)信息的話脑慧,你會發(fā)現(xiàn)界面非常熟悉 - 和Salesforce自身的standard object與custom object基本一樣。
需要注意的是砰盐,external object的后綴是__x闷袒,而custom object的后綴是__c。
另外岩梳,由于外部數(shù)據(jù)的出現(xiàn)囊骤,Salesforce新增加了兩個字段類型External Lookup Relationship Field和Indirect Lookup Relationship Field。簡單說就是冀值,兩個都是用來建立Lookup Relationship的也物,不同之處是,前者external object是parent object池摧,child object可以是standard object或external object焦除;后者external object是child object,而parent object是standard object作彤。這些東西上面那個Module里都有詳細(xì)講解膘魄,在此我就不細(xì)說了。
接下來我們仔細(xì)看看DataSource.Provider和DataSource.DataSourceConnection這兩個接口竭讳。
DataSource.Provider
實現(xiàn)該接口的Apex class需要實現(xiàn)下面三個方法:
getAuthenticationCapabilities()告訴Salesforce外部數(shù)據(jù)源支持何種驗證方法创葡,包括匿名、BASIC绢慢、Oauth灿渴、或者Certificate。這些選項會在上面截圖的Identity Type選項中出現(xiàn)胰舆,本例支持匿名和BASIC骚露,但因為不做任何Http Callout,所以并沒有什么用處缚窿。
getCapabilities()告訴Salesforce外部數(shù)據(jù)源支持何種操作棘幸,ROW_QUERY、ROW_UPDATE倦零、ROW_CREATE误续、ROW_DELETE吨悍、SEARCH、REQUIRE_ENDPOINT蹋嵌、以及QUERY_PAGINATION_SERVER_DRIVEN育瓜。
getConnection()則返回一個實現(xiàn)了DataSource.DataSourceConnection的Apex class實例,來做真正的Adapter工作栽烂。注意躏仇,由于每一次對外部數(shù)據(jù)的SOQL或SOSL操作都會調(diào)用該函數(shù)產(chǎn)生一個DataSource.DataSourceConnection的Apex class實例,所以該Apex clas的構(gòu)造函數(shù)中不應(yīng)該有任何expensive的操作腺办,比如callout之類钙态。
P.S. 關(guān)于DataSource.Provider的一些額外解釋
ROW_QUERY - 所有的SOQL操作,包括瀏覽UI時候系統(tǒng)產(chǎn)生的SOQL操作菇晃;
ROW_UPDATE册倒、ROW_CREATE、ROW_DELETE - 常規(guī)的CRUD操作磺送;
SEARCH - 所有的SOSL操作驻子,包括在UI中進(jìn)行全局搜索;
REQUIRE_ENDPOINT - 控制是否在設(shè)置新外部數(shù)據(jù)源頁面中要求輸入一個endpoint地址估灿;
QUERY_PAGINATION_SERVER_DRIVEN - 告訴Salesforce外部數(shù)據(jù)源的分頁是否是server端控制的崇呵;
DataSource.ConnectionParams - getConnection()函數(shù)中會傳入一個叫做connectionParams的參數(shù),這個參數(shù)的值取決于設(shè)置外部數(shù)據(jù)源時管理員選擇的何種驗證方式 - 如果是BASIC的話則會包含USERNAME和PASSOWRD馅袁,如果是Oauth的話則會包含oauthToken...不過Salesforce建議使用named credentials來做callout域慷,而不是直接把credential提供給外部數(shù)據(jù)源。本例中因為不涉及callout汗销,所以用不著connectionParams犹褒,我會在后續(xù)的文章中介紹相關(guān)的用例。
DataSource.DataSourceConnection
實現(xiàn)DataSource.DataSourceConnection的Apex class實際上是真正做所有工作的幕后英雄弛针。
這個class里面涉及到的東西很多叠骑,但大多數(shù)都只是一些DataSource下新的數(shù)據(jù)結(jié)構(gòu)而已,Salesforce的文檔很詳細(xì)削茁,我在代碼中也做了比較細(xì)致的注釋宙枷,所以基本的東西就不重復(fù)了(DataSource.Filter是一個很有意思的數(shù)據(jù)類型,我在注釋中也給予了額外的一些篇幅來解釋茧跋,提醒一下注意)慰丛。
我這里想著重說的是我覺得最重要的、但文檔中感覺并沒有解釋的很詳盡的兩個概念瘾杭,query()函數(shù)和search()函數(shù)诅病。
先說query()函數(shù)。
文檔中說每次對external object進(jìn)行SOQL操作時都會觸發(fā)query()函數(shù),然后所有和該SOQL相關(guān)的內(nèi)容都會以QueryContext參數(shù)的形式傳遞給query()函數(shù)睬隶,然后完全交給函數(shù)代碼來做相應(yīng)的操作。那么問題來了 - 究竟query()函數(shù)需要處理多少種SOQL用例呢页徐?處理 SELECT columns FROM table 當(dāng)然不用說苏潜,處理 SELECT columns FROM table WHERE externalId = 'something' 也不用說,那么再復(fù)雜一些的SOQL有處理的必要嗎变勇?
我不知道別人有沒有這樣的疑慮恤左,總之我是思忖了許久,后來終于醒悟過來 - 這完全取決于你安笮濉飞袋!你如果希望這個external object僅僅支持最基本的UI SOQL,那就實現(xiàn)上述兩個用例外加 SELECT COUNT()就好链患,你如果希望在代碼中對external object進(jìn)行更復(fù)雜的SOQL操作巧鸭,那就實現(xiàn)那些更復(fù)雜的SOQL操作,當(dāng)然麻捻,如果你沒有在query()代碼中實現(xiàn)相應(yīng)的SOQL操作邏輯纲仍,而又在別處試圖對這個external object做相應(yīng)的操作,那等待你的當(dāng)然就是exception了贸毕。
先來看兩個最簡單的例子郑叠。
假設(shè)你在Salesforce中已經(jīng)通過你的外部數(shù)據(jù)源創(chuàng)建了某個external object,并以此為基礎(chǔ)創(chuàng)建了一個tab(完全和對standard object的操作一樣)明棍,那么你在瀏覽該tab的時候Salesforce后臺會自動對該external object進(jìn)行SOQL查詢乡革。
比如/x00/o 這個操作是查找最近瀏覽過的數(shù)據(jù),而/xoo 則是列出所有的數(shù)據(jù)摊腋。
下面是DEBUG LOG中對兩個操作的數(shù)據(jù)沸版。
第一個截圖是對/xoo/o 的操作,雖然QueryContext的信息看不全兴蒸,但其實它傳進(jìn)來一個filter推穷,columnName是Id,type是EQUALS类咧,columnValue是‘0013....’馒铃,我的query()函數(shù)捕捉到了這些信息,然后對應(yīng)的進(jìn)行了操作(因為我的外部數(shù)據(jù)其實是映射Account數(shù)據(jù)痕惋,所以我做了一個SOQL操作区宇,其它情形還可能是對某個REST API進(jìn)行callout,諸如/endpoint/data?id='0013...'值戳,然后在對返回的數(shù)據(jù)進(jìn)行整理)议谷。
?第二個截圖是對/xoo 的操作,QueryContext中filter為null堕虹,所以我就簡單做了一個SELECT的操作卧晓,返回數(shù)據(jù)芬首。
如果你只是要通過UI來訪問external object,那這些基本上就差不多夠了(除非你設(shè)置定制化view)逼裆,但如果你想讓external object支持更復(fù)雜的SOQL操作郁稍,那就要將這些操作都在你的query()代碼中予以捕捉和實現(xiàn)。
本例中的external object支持了WHERE從句中的大部分SOQL關(guān)鍵字胜宇,我們可以通過workbench做一個實驗:
如果我把代碼中對DataSource.FilterType.LIKE_的支持去掉耀怜,那么執(zhí)行下面SOQL時會出現(xiàn)exception;
如果將代碼中對DataSource.FilterType.LIKE_的支持加回來桐愉,那么數(shù)據(jù)會順利返回财破;
對有AND的邏輯從句也能夠應(yīng)付(DataSource.Filter的type屬性和subfilters屬性居功至偉)。
再說說search()函數(shù)从诲。
理解了query()函數(shù)后左痢,search()函數(shù)就簡單多了。當(dāng)Salesforce進(jìn)行全局搜索的時候系洛,調(diào)用search()函數(shù)抖锥,而你無非就需要考慮兩件事:1. 外部數(shù)據(jù)源中的哪些external objects被搜索(可以通過遍歷SearchContext的tableSelections屬性來作出決定);2. 某個external object中的哪些字段被搜索碎罚。
比如Salesforce提供了一個Util方法searchByName()磅废,其實就是當(dāng)search發(fā)生時,拿著searchPhrase對external object的Name字段進(jìn)行一個關(guān)鍵字為CONTAINS的query荆烈,如果使用這個Util方法拯勉,那么當(dāng)你在Salesforce里做如下針對電話號碼的全局搜索時,對應(yīng)的external object數(shù)據(jù)不會出現(xiàn)憔购,因為該方法只針對Name做query宫峦。
在本例代碼中,我注釋掉了該Util方法玫鸟,轉(zhuǎn)而自定義了一個針對Name和Phone兩個字段的query导绷,這樣的話,做同樣的一個全局搜索屎飘,你可以看到external object對應(yīng)的數(shù)據(jù)出現(xiàn)在了返回結(jié)果中妥曲。
最后補充一句,每當(dāng)你做全局搜索的時候钦购,?一個SearchThreadPools操作會在DEBUG LOG中產(chǎn)生檐盟,你可以看到,在后代的確調(diào)用了我們自定義的search()函數(shù)代碼押桃,做了一個基于Name和Phone兩個字段的query操作葵萎。
好了,這篇文章已經(jīng)很冗長了,就在這里結(jié)束吧羡忘,?下一篇再見~
GitHub
https://github.com/jacky1999cn2000/lightningconnect
參考資料
https://developer.salesforce.com/blogs/engineering/2015/05/introducing-lightning-connect-custom-adapters.html
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_top.htm
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_connector_example_loopback.htm
MAY THE FORCE BE WITH YOU!