关于CANNOT_CASCADE_PRODUCT_ACTIVE错误

某日,一个悠闲的午后,我泡的一杯毛尖正在阳光下散发着热气。嗯,接到一个小活儿。平平稳稳,不需要经历大风大浪,不需过五关斩六将的小活儿——-导数据。

这次需要更新的数据是Product。就是那个标准SObject,Product2,三位prefix是01t。人们常说用标准SObject用多了总会遇见坑,但人生又何尝不是如此。不是
完全控制在自己手里的东西,只能去了解,去接受,最后习以为常。

果然,这次就掉坑里了。工具执行结束之后,一看Log。在1000条数据里面,有50多条报了如下的错误。
StatusCode: CANNOT_CASCADE_PRODUCT_ACTIVE
Error Message: Cannot update associated pricebook entries without the Manage Price Books permission
然后看了下报错的数据,也没什么特别明显的特征。 貌似是无差别报错。

唔。。。万事不决查文档。
很快就在《SOAP API Developer Guide》定位到了这个Status Code。
https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_concepts_core_data_objects.htm


CANNOT_CASCADE_PRODUCT_ACTIVE
An update to a product caused by a cascade can’t be done because the associated product is active.

《SOAP API Developer Guide》

咦?怎么详描述的与系统返回的信息不一样?


系统反馈给我的信息是“由于没有Manage Price Books Permission, 所以无法更新关联的Pricebook”。只是说由于我没有Manage Price Books权限才报错,但是我更新Product为什么又会在意我有没有Manage Price Books的权限?

而文档给的信息是“由于product是active,所以无法完成由联级更新引起的被关联的product。 ”这写的是啥啥啥?什么叫caused by a cascade,谁是发出动作的人?是我的话我又cascade了谁?谁又Associated了我?

既然《SOAP API Developer Guide》的描述完全不知所云,那么就从系统直接反馈的错误信息入手吧。查了一下使用的账号,Profile和Permission上确实都没有PriceBook的CRUD权限。不过。。。。如果是因为没有PriceBook的权限的话,为什么绝大部分数据还更新成功了?如果真是这样,由于我没有权限,应该一条都不会成功才对。

结合两条截然不同的信息来看。线索指向了两个关键点。
1. PriceBook的权限。
2. Product的active状态。

于是做了一个试验,在没有PriceBook编辑权限的前提下,尝试去变更Product的active状态。果然报出的相同的错误信息。
就是说,如果想更新product的active状态,需要Pricebook的编辑权限。
唔。。。。


故事当然没有到此结束,不甘心的我突发奇想的在本地硬盘搜索了一下这个status code,结果在一本上古文档里发现了这样一段描述。

CANNOT_CASCADE_PRODUCT_ACTIVE

You cannot activate or deactivate this product without also being able to edit pricebooks.

某上古文档

原来错误信息还有第三个版本。这个版本明确的指出了问题的所在。

接下来是无责任脑洞推理没有任何实锤和Evidence。

在Salesforce中,Product和Price Book是多对多的关系。一个product可以有多个Price Book,而Price Book也可以包含多个Product。Price Book Entry就是二者的Junction Object。
如果用workbench查看Price Book Entry上关联Product与Price Book的字段的关系属性,会发现该关系具有联级删除属性(cascadeDelete: true)。
所以在此推定Product与Price Book Entry,Price Book与Price Book Entry之间的关系皆为Master-Detail。
在表权限层级。

在Profile中,如果想要拥有子表的编辑权限,那么至少要拥有父表的View权限。反过来,如果连父表的view权限都没有,则子表没有任何权限。

所以,由于我没有Price Book的任何权限,导致失去Price Book Entry的所有权限。
而根据观察,Product在active状态变化的时候,会同步active状态到所有旗下的Product Book Entry。
在此推测Salesforce在Product上加了一个Trigger/Workflow/Flow/Process Builder用来用来同步active状态,而且在这个处理中特意做了权限检查。如果发现没有Price Book Entry的权限,则去检查Price Book的权限,如果也没有权限,就报错。

这也正是系统直接返回的错误信息的由来:

Cannot update associated pricebook entries without the Manage Price Books permission

至于为何错误信息出现这么多版本,我推测是由于与Product有多对多关系的不仅仅只有Price Book,所以同一个StatusCode从单一情景变为也会复用在其他的情景。所以错误信息在文档里就改成了更加泛指的版本。而这次我遇见的错误是一个很具体的场景,则出现的信息又由泛指的版本变为更加具体的版本。

脑洞完毕。我去加权限了。。

0

Dataloader与External Id

某日,又见某童鞋双手合十,指尖杵在下巴上,做福尔摩斯状。
忍不住凑了过去 。
此童鞋用眼角瞄到有人过来了,嘴角抽动了一下,欲言又止。

“别难为自己了,说说吧。”团结才是力量,对不对。
“好吧。。。”他长长的吐了一口气,手也放了下来。“事情是这样的,我有一张表叫做order。这个order有个字段叫product,会去lookup一个叫product的表。”。说着,他打开了SObject的详细页面。“现在我需要导入历史订单数据,量很大。”

我点了点头,示意他继续,因为目前为止听起来没什么。
“问题是原始数据的product字段并不是product的id,而是product的code。这样的话我没法直接导入系统。” 在我开口之前他赶紧抢先又说道,“product的种类和order的数据量都实在太大了,实在没办法用vlookup处理。已经死机好几次了。”

我弹开了他的手,接过鼠标打开了product表的定义页面。在自定义字段中找寻那个叫code的字段。唔。。。。External Id勾选了,Unique勾选了。不禁扬起了嘴角。

“看,Product的code是external Id。”我指着屏幕。
“可是我们要导入数据的是Order啊,不是Product。”
“那你先说说看,External Id在Dataloader里怎么用?”
“就是Upsert的时候,可以指定External Id,如果匹配上了就update,没匹配上就insert。”他自信的回答道。
“没错。但是还有一种用法。”
“啊?”

众所周知,lookup字段实际储存的值为ID。这也就意味着,在insert或者update数据的时候,我们要将Id写在lookup字段上。如果写的不是Id,或者Id不存在,Dataloader会告诉我们数据写入失败。
一般的情况下,如果我们只有被lookup的表中一个字段的值(Product Code),就不得不先根据这个值(Product的Code)去查出被lookup表的id(Product的Id),然后放在待更新数据的lookup字段上(Order的Product字段),才能继续执行下去(Order)。

不过,老天关上了你的门,一定会打开你的窗,毕竟你才是你生活的主角,所以有主角光环。

事实上,如果你拥有的值(Product Code)是被Lookup表的External Id(Product的Code),那么在插入数据时,可以让Dataloader根据External Id自动的将lookup字段填入对应的Id。这样就不用人工的去查找替换了。

做法如下。
1. 首先,一如往常,准备好csv,点insert,输入账号密码,选择Order,选择csv路径,下一步。
2. 然后,Create or Edit a Map, Auto-Match Fields to Columns。
3. 接下来就不一样了。在匹配完字段时候,点击Save Mapping。选择路径,填入文件名。保存sdl文件。
4. 找到刚刚保存的sdl文件,用文本编辑器打开。修改如下。保存。

#Mapping values
#Wed Apr 24 19:13:21 CST 2019
# Old 
#PRODUCT__C=product__c
# New 
PRODUCT__C=product__r\:code__c
.........

5. 回到Datalaoder,关掉Mapping Dialog窗口,这次点击Choose an Exsiting Map,选择刚刚保存的sdl文件。
6. 一路next。
7. 恭喜插入成功。(如果Product Code存在的话)

“看,你与成功之间只差一个\:”。我拍了拍了他的肩旁。
“恩人慢走”。

总结:
1. 被lookup表需要有External ID。
2. 在SDL文件中,将lookup字段的__c变成__r(标准字段去掉Id),加上“\:”,加上External Id的API Name。insert或者update的时候使用此sdl。
3. 必须仅且只仅匹配到一条记录才可以成功写入。

参考官方文档:
https://help.salesforce.com/articleView?id=000002783&type=1

后记:
有人问我说,唉,明明写的技术相关的内容,文风为什么这么不正经?
其实原因不外乎两个。第一个是想看正八经儿的说明文就应该去看官方文档,而不是等着官方文档的翻译。Salesforce的文档和知识库是很强大和全面的。第二个是每一篇文章的灵感来源都是真实被提出过的问题,我觉得自古以来,最好的承载知识的方式就是带入情景,讲故事。官方文档似的说明文是告诉你怎么一步一步的用锤子钉钉子,而我是想用故事告诉你可以用锤子来解决钉钉子的问题。
总之,想要一个工具速查手册的话,应该去看官方文档知识库才对。

1+

解决OwnCloud的No input file specified.错误

临关机睡觉前,总觉得不升级点什么睡不踏实。比如手机应用,电脑操作系统,IDE和浏览器插件之类的,都要点一圈保证是最新的。
结果今天发现没有可以更新的。。。不行,难受。那就看看Godday的host的PHP有没有新版本吧。

由于租用时间较早,当年还是PHP5.2的天下。结果PHP7都快过时了,服务器可选择的PHP版本仍然停留在PHP5。俗话说,世界上最好的PHP版本是PHP6(并没有6,直接从5到7),我退而求其次,升级到7还不行?毕竟在性能上有着天差地别。

于是乎满怀期待的点开了Program Language Version。呦,提供了
新版本 PHP5.6。行吧,总比没有强。于是痛快的选择了这个稍微高一点点的版本。

很快升级就完成了。按流程,依次点开各个服务进行回归测试。博客,正常。论坛,正常…………网盘,“No input file specified.”。糟糕了。

网盘用的是老牌私有云盘OwnCloud,网上能搜出的信息不算太多。
于是乎冷静下来,分析目前的情况。
首先,变化的只有PHP的版本,而且不是大版本变动。服务本身没有变动。
其次,其他服务,比如Wordpress等皆运行正常。

所以,推测是OwnCloud的PHP设置问题。
由于Godaddy为Shared Host,所以并不能直接修改PHP设置,而是要通过.htcaccess文件去实现基于路径的调整。随之,我找到了网盘的根目录,打开了.htcaccess。
留意到了如下片段

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
…………
</IfModule>

咦?我怎么记得RewriteEngine下面应该有个RewriteBase啊。
这小问题,于是顺手加上了“RewriteBase /”。保存。

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteBase /
  RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
…………
</IfModule>

刷新之后,还是 “No input file specified.” 。哎呀?Wordpress的配置文件就是这么写的啊。然后又开始疯狂搜索信息,发现了很多在Godaddy上部署了Owncloud的受害者遇到过相同的问题,但是都没有找到答案。

看来还是得靠自己。经过一番努力地思考,难道因为是SharedHost,所以必须用绝对路径?而且由于当年不会给子域名加SSL,所以作为早期架起的服务,Owncloud并没有分配子域名,所以不该写根目录?
于是根据Goddady的Control Panel提供的绝对路径地址,拼上Owncloud的所在路径,写在了RewriteBase中。

<IfModule mod_rewrite.c>
  RewriteEngine on
  RewriteBase /XXXX/XXXX/XXX/99999999/html/cloud/owncloud5/
  RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
…………
</IfModule>

再次访问OwnCloud的URL,恢复正常。

总结,
1. RewriteEngine后面要接RewriteBase
2. 如果不是域名的根目录,要写全路径。
3. 如果相对路径不好用,尝试绝对路径。

PHP7什么时候才提供啊。。。GoDaddy!

1+