may the Force be with you 雲上の世界

Lightning Connect Custom Adapters (技术贴)

salesforce_lightning

 

作为一个自认为特别有人文情怀的人,始终觉得写技术贴特别没劲儿,于是“云上的世界”这个栏目基本上不死不活地空置了两年,最近突然觉得这么空下去挺对不起这个浪漫的专栏名,于是决定从即日开始,在这里写一写自己在force.com这个庞大的生态系统中探索时的些许感悟和积累。

 

Salesforce在北美欧洲澳大利亚日本等地非常地不可一世,但在中国却因为价格策略市场认知等因素,再加上本地竞争者的有力阻击,基本上还处在蓄势待发的状态,不用说普通读者,即便对大多数互联网或IT从业者而言,也还是一个相对比较新鲜的事物。

 

因此,我决定当一个布道者。

 

虽然也了解的不多,但就当投石问路,说不定有读者看了这些文章之后决定成为一个Salesforce的管理员、开发者、咨询顾问、架构师呢,要是再能由此遇到一些志同道合的朋友,那就更完美了。

 

好了,闲话少说,直奔主题吧。

 

首先预告一下,“Lightning Connect”这个系列都将会是纯技术贴 – 我会努力把技术贴和和非技术贴分开,这样,不同兴趣的读者可以各取所需 – 如果你不是Salesforce的开发者,这篇文章可能不适合你。

 

Salesforce在Winter ’15的产品发布当中第一次介绍了Lightning Connect – 这是一个新的将Salesforce与外部数据源进行整合的接口。

 

简而言之,它允许你用非ETL的方式(这么说听起来貌似很高端,其实说白了就是通过HTTP Callout来获取数据)将外部数据源集成到Salesforce中来,创建所谓的external object,并承诺这些external object和那些生存在Salesforce自己底端Oracle数据库中的standard object或custom object有相似的行径与平等的地位。

 

Lightning Connect本身的设置并不复杂,Salesforce的免费在线教学网站Trailhead中有专门的一个Module来介绍,所以我就不赘言了,有兴趣的朋友可以自行移步查看。

 

https://developer.salesforce.com/trailhead/module/lightning_connect

 

Screen Shot 2015-10-29 at 10.14.21 PM

 

然而,Lightning Connect的局限性在于它只能识别那些符合OData协议的数据源。

 

即便一些大公司的产品,比如SAP或Microsoft Dynamics,都有基于OData协议的数据接口,但毕竟不甚方便,于是在Summer ’15的产品发布中,Salesforce推出了一个让人激动的new feature,也就是这篇文章的主题 – Lightning Connect Custom Adapters。

 

Lightning Connect Custom Adapters,或者叫做Apex Connect Framework,允许开发者自己用Apex来编写自定义适配器(Custom Adapter),然后实现和任何格式或协议的数据源进行实时的完美集成。

 

Apex为此增加了一个叫做DataSource的命名空间,所有新的方法和数据类型都在此命名空间之下定义。作为开发者来说,需要做的只有两件事:

 

1. 创建一个Apex class,继承DataSource.Provider这个接口,实现接口里预定义的函数;

 

2. 创建另一个Apex class,继承DataSource.DataSourceConnection这个接口,然后实现接口里预定义的函数。

 

这两个Apex class完成之后,在创建外部数据源时你会在Type下拉菜单中看到你自己的DataSource.Provider,选择该Provider,这个新的外部数据源就和你自定义的Adapter联系起来了。

 

Screen Shot 2015-10-29 at 6.09.34 PM

 

在新创建的数据源中点击“Validate and Sync”按钮(该截图是取自Edit一个已有的数据源,所以只有“Sync”按钮),这时后台代码会调用DataSource.DataSourceConnection那个Apex class中的sync()函数(后文中会看到),并返回外部数据源中所有的表信息(该例中外部数据源只包含了一个叫做“Looper”的表,真实环境中每个外部数据源可能含有多个表)。

 

Screen Shot 2015-10-29 at 6.10.10 PM

 

选择你想要同步的表,然后进行同步,Salesforce则会按照你在sync()函数中的定义创建相应的外部数据(external object)。如果你点击新生成的外部数据查看其详细信息的话,你会发现界面非常熟悉 – 和Salesforce自身的standard object与custom object基本一样。

 

需要注意的是,external object的后缀是__x,而custom object的后缀是__c。

 

另外,由于外部数据的出现,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里都有详细讲解,在此我就不细说了。

 

Screen Shot 2015-10-29 at 6.10.25 PM

 

Screen Shot 2015-10-29 at 6.10.42 PM

 

接下来我们仔细看看DataSource.Provider和DataSource.DataSourceConnection这两个接口。

 

DataSource.Provider

 

实现该接口的Apex class需要实现下面三个方法:

 

getAuthenticationCapabilities()告诉Salesforce外部数据源支持何种验证方法,包括匿名、BASIC、Oauth、或者Certificate。这些选项会在上面截图的Identity Type选项中出现,本例支持匿名和BASIC,但因为不做任何Http Callout,所以并没有什么用处。

 

getCapabilities()告诉Salesforce外部数据源支持何种操作,ROW_QUERY、ROW_UPDATE、ROW_CREATE、ROW_DELETE、SEARCH、REQUIRE_ENDPOINT、以及QUERY_PAGINATION_SERVER_DRIVEN。

 

getConnection()则返回一个实现了DataSource.DataSourceConnection的Apex class实例,来做真正的Adapter工作。注意,由于每一次对外部数据的SOQL或SOSL操作都会调用该函数产生一个DataSource.DataSourceConnection的Apex class实例,所以该Apex clas的构造函数中不应该有任何expensive的操作,比如callout之类。

 

P.S. 关于DataSource.Provider的一些额外解释

 

ROW_QUERY – 所有的SOQL操作,包括浏览UI时候系统产生的SOQL操作;

 

ROW_UPDATE、ROW_CREATE、ROW_DELETE – 常规的CRUD操作;

 

SEARCH – 所有的SOSL操作,包括在UI中进行全局搜索;

 

REQUIRE_ENDPOINT – 控制是否在设置新外部数据源页面中要求输入一个endpoint地址;

 

QUERY_PAGINATION_SERVER_DRIVEN – 告诉Salesforce外部数据源的分页是否是server端控制的;

 

DataSource.ConnectionParams – getConnection()函数中会传入一个叫做connectionParams的参数,这个参数的值取决于设置外部数据源时管理员选择的何种验证方式 – 如果是BASIC的话则会包含USERNAME和PASSOWRD,如果是Oauth的话则会包含oauthToken…不过Salesforce建议使用named credentials来做callout,而不是直接把credential提供给外部数据源。本例中因为不涉及callout,所以用不着connectionParams,我会在后续的文章中介绍相关的用例。

 

Screen Shot 2015-10-30 at 2.48.10 PM

 

DataSource.DataSourceConnection

 

实现DataSource.DataSourceConnection的Apex class实际上是真正做所有工作的幕后英雄。

 

这个class里面涉及到的东西很多,但大多数都只是一些DataSource下新的数据结构而已,Salesforce的文档很详细,我在代码中也做了比较细致的注释,所以基本的东西就不重复了(DataSource.Filter是一个很有意思的数据类型,我在注释中也给予了额外的一些篇幅来解释,提醒一下注意)。

 

我这里想着重说的是我觉得最重要的、但文档中感觉并没有解释的很详尽的两个概念,query()函数和search()函数。

 

先说query()函数。

 

文档中说每次对external object进行SOQL操作时都会触发query()函数,然后所有和该SOQL相关的内容都会以QueryContext参数的形式传递给query()函数,然后完全交给函数代码来做相应的操作。那么问题来了 – 究竟query()函数需要处理多少种SOQL用例呢?处理 SELECT columns FROM table 当然不用说,处理 SELECT columns FROM table WHERE externalId = ‘something’ 也不用说,那么再复杂一些的SOQL有处理的必要吗?

 

我不知道别人有没有这样的疑虑,总之我是思忖了许久,后来终于醒悟过来 – 这完全取决于你啊!你如果希望这个external object仅仅支持最基本的UI SOQL,那就实现上述两个用例外加 SELECT COUNT()就好,你如果希望在代码中对external object进行更复杂的SOQL操作,那就实现那些更复杂的SOQL操作,当然,如果你没有在query()代码中实现相应的SOQL操作逻辑,而又在别处试图对这个external object做相应的操作,那等待你的当然就是exception了。

 

先来看两个最简单的例子。

 

假设你在Salesforce中已经通过你的外部数据源创建了某个external object,并以此为基础创建了一个tab(完全和对standard object的操作一样),那么你在浏览该tab的时候Salesforce后台会自动对该external object进行SOQL查询。

 

比如/x00/o 这个操作是查找最近浏览过的数据,而/xoo 则是列出所有的数据。

 

Screen Shot 2015-10-29 at 6.07.46 PM

 

下面是DEBUG LOG中对两个操作的数据。

 

第一个截图是对/xoo/o 的操作,虽然QueryContext的信息看不全,但其实它传进来一个filter,columnName是Id,type是EQUALS,columnValue是‘0013….’,我的query()函数捕捉到了这些信息,然后对应的进行了操作(因为我的外部数据其实是映射Account数据,所以我做了一个SOQL操作,其它情形还可能是对某个REST API进行callout,诸如/endpoint/data?id=’0013…’,然后在对返回的数据进行整理)。

 

第二个截图是对/xoo 的操作,QueryContext中filter为null,所以我就简单做了一个SELECT的操作,返回数据。

 

Screen Shot 2015-10-29 at 6.08.17 PM

 

Screen Shot 2015-10-29 at 6.08.32 PM

 

如果你只是要通过UI来访问external object,那这些基本上就差不多够了(除非你设置定制化view),但如果你想让external object支持更复杂的SOQL操作,那就要将这些操作都在你的query()代码中予以捕捉和实现。

 

本例中的external object支持了WHERE从句中的大部分SOQL关键字,我们可以通过workbench做一个实验:

 

如果我把代码中对DataSource.FilterType.LIKE_的支持去掉,那么执行下面SOQL时会出现exception;

 

Screen Shot 2015-10-30 at 4.20.58 PM

 

如果将代码中对DataSource.FilterType.LIKE_的支持加回来,那么数据会顺利返回;

 

Screen Shot 2015-10-30 at 4.22.12 PM

 

对有AND的逻辑从句也能够应付(DataSource.Filter的type属性和subfilters属性居功至伟)。

 

Screen Shot 2015-10-30 at 4.23.00 PM

 

再说说search()函数。

 

理解了query()函数后,search()函数就简单多了。当Salesforce进行全局搜索的时候,调用search()函数,而你无非就需要考虑两件事:1. 外部数据源中的哪些external objects被搜索(可以通过遍历SearchContext的tableSelections属性来作出决定);2. 某个external object中的哪些字段被搜索。

 

比如Salesforce提供了一个Util方法searchByName(),其实就是当search发生时,拿着searchPhrase对external object的Name字段进行一个关键字为CONTAINS的query,如果使用这个Util方法,那么当你在Salesforce里做如下针对电话号码的全局搜索时,对应的external object数据不会出现,因为该方法只针对Name做query。

 

Screen Shot 2015-10-29 at 6.05.03 PM

 

在本例代码中,我注释掉了该Util方法,转而自定义了一个针对Name和Phone两个字段的query,这样的话,做同样的一个全局搜索,你可以看到external object对应的数据出现在了返回结果中。

 

Screen Shot 2015-10-29 at 6.02.24 PM

 

最后补充一句,每当你做全局搜索的时候,一个SearchThreadPools操作会在DEBUG LOG中产生,你可以看到,在后代的确调用了我们自定义的search()函数代码,做了一个基于Name和Phone两个字段的query操作。

 

Screen Shot 2015-10-29 at 6.06.13 PM

 

Screen Shot 2015-10-29 at 6.09.05 PM

 

好了,这篇文章已经很冗长了,就在这里结束吧,下一篇再见~

 

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!

 

The_Force

facebooktwittergoogle_pluslinkedinmailby feather

Leave a Reply

Your email address will not be published. Required fields are marked *

Powered by: Wordpress