How To Copy Content Objects Between Salesforce Environments
I recently was working on a project for a customer where we had to copy Salesforce Content from one org into another. We didn’t think too much of it when we were drafting up requirements and thought it would be a pretty straight forward task. To my surprise, we spent a considerable amount of unbudgeted hours trying to get these pesky records copied over, and in the end came out more knowledgeable about the schema for the Content objects. I hope this post will help other developers who have the “pleasure” of copying content version records between Salesforce orgs.
So we started out by identifying the Account and Contact records we wanted to copy over, and also included the related child records (pretty easy to do using the sfApex app). As we expected, it discovered the related ContentVersion records. We made sure the FirstPublicationLocationID was set correctly and we kicked off the data copy. We ran into a seemingly non-trivial error message about not specifying the ContentDocumentID so we excluded the ContentDocumentID field and re-ran the data copy. Everything worked out great.
Next, we had to copy over the Content in the public and private libraries. This is when the fun began.
So the first issue we ran into was that the ContentDocument object is queryable and updateable but not createable. This meant that we can’t directly create ContentDocument records. According to the documentation (https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_contentdocument.htm), we shouldn’t specify a value for the ContentDocumentID field on the ContentVersion record since Salesforce automatically creates a ContentDocument record when a ContentVersion record is inserted and assigns it to the ContentDocumentID field.
This definitely created some extra steps for us since we had no way of knowing what the new ID was of the ContentDocument record. This meant that we would have to re-query the ContentVersion object after all the records were inserted to get the ContentVersions and this would mean we couldn’t get it all done in a fully automated way.
So we created the ContentVersion records and to our surprise, we started seeing duplicates all over the place when we checked out the Salesforce org through the web browser. We soon discovered that since we copied 267 ContentVersion records and ignored the ContentDocumentID field, Salesforce created 267 ContentDocuments, even though there were only 93 actual documents in the source.
To get around this, we deleted the ContentVersions we copied in and tried again. This time we filtered on the VersionNumber field where it equaled 1 to get only the first version of each content document. We inserted in those version 1 ContentVersion records into Salesforce. Next, we manually queried Salesforce for the ContentVersion records (specifically the ContentDocumentID field) to grab the related content document IDs. Then, we mapped those content document records to the source content document records.
We then queried the all the non-version 1 ContentVersion records and assigned the ContentDocument map to the ContentDocumentID field. This time, it looked like it worked. When we checked out Salesforce using the developer console, we saw the 93 Content Documents with various version of the document tied to it.
Filled with joy, we emailed the users and said that we were ready for testing… Well, needless to say, the users came and said they had a problem. It turns out that they were going to their libraries and weren’t seeing any content. After digging around some more, we figured out that there wasn’t a link between the ContentDocument and the Library. So we scoured through Salesforce’s documentation.
The first thing we discovered is that creating Content Libraries is not supported through the Metadata API (https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_contentworkspace.htm) so the only way to create them was through the web browser. So, we created the libraries and manually assigned the library member users.
Then we found out that we must copy over the ContentWorkspaceDoc records to associate the ContentDocument to a library (a.k.a. Workspace). Before doing this, we had to create an ID map between the Source Library ID and the Destination Library ID.
So, we grabbed all the new library IDs and created a map of those IDs between the source and destination. We then assigned this map to the ContentWorkspaceDoc.ContentWorkspaceId. Next, we had to assign the ContentDocument map we created earlier to the ContentWorkspaceDoc.ContentDocumentID field.
Next, we crossed our fingers and kicked off the data copy. Success!
Topics: Developer , Salesforce Development , Software Development ,