Salesforce per SQL - Tutorial
Last Update: 20.11.2024 |
Einleitung
Dieses Tutorial soll zeigen, wie man unter Verwendung eines speziellen JDBC-Treibers per SQL auf ein Salesforce-CRM-System zugreifen kann.
Wichtiger Hinweis: Die Möglichkeit, per HTTP/SOAP auf die von Salesforce selbst angebotenen Webservice-Schnittstellen zuzugreifen, wird hier nicht beschrieben. Ebenso wird kein Salesforce-Knowhow vermittelt. Grundlegende Salesforce- und SQL-Kenntnisse werden vorausgesetzt.
Voraussetzungen:
Zugang zu einem Salesforce-Testsystem, grundlegende Salesforce-Kenntnisse.
Lobster-Integration-Knowhow (z. B. Schulung, Lobster Academy).
Zugang zu einem Lobster-Integration-System mit Salesforce-Zusatz-Lizenz und zwei konfigurierten Salesforce-Datenbank-Aliasen "salesforce" und "salesforce_nobulk" (siehe auch Abschnitt Salesforce per SQL - Vorbereitungen).
Phase 4 (Datenbank-Knoten) - Account anlegen/ändern
Zunächst wollen wir in Salesforce ein neues Account-Objekt anlegen.
Nehmen wir an, wir exportieren eine Firma aus unserem ERP-System. Dabei entsteht folgende CSV-Datei als Eingangsdatei eines Profils:
ADR;Sample Company LTD;11 Downing Street;London
Im nächsten Schritt legen wir ein Profil mit dem Namen "SF_account_import" an, welches diese Datei eingangsseitig per CSV-Parser in den Quellbaum einliest:
Auf der Zielseite wird ein neuer Datenbank-Knoten angelegt:
DB-Alias: salesforce_nobulk
Tabelle/Proc.: Account
Nun bitte folgende Feld-Zuordnungen herstellen:
Quell-Feld |
Ziel-Feld |
companyname |
Name |
street |
BillingStreet |
city |
BillingCity |
Jetzt noch den Haken bei "Phase 4 aktivieren (SQL Ausführung)" setzen, Profil speichern und einmal starten (durch Hochladen unserer Eingangsdatei über Neudurchlauf).
Im Control Center sollte das Profil nun ohne Fehlermeldung durchgelaufen sein. In Salesforce ist der neu angelegte Account zu sehen:
Soweit, so gut - der neue Account ist in Salesforce angelegt.
Jetzt nehmen wir mal an, die Straße in der Adresse der Firma ändert sich:
ADR;Sample Company LTD;169 Union Street;London
Bei einer "normalen" Datenbanktabelle würde man den Datenbankknoten auf "try update before insert" (oder "only update") setzen und die SQL-Schlüssel-Felder entsprechend füllen, damit klar ist, welcher Datensatz das Update erfahren soll.
Unser aus Salesforce generierter Datenbankknoten hat allerdings bei keinem Feld die Eigenschaft "SQL Schlüssel = Ja" gesetzt - wir wissen nicht, wie wir Lobster Integration mitteilen können, welcher Account upgedated werden soll.
Nun gibt es zwei verschiedene Vorgehensweisen, um dieses Problem zu lösen.
Variante a - ("die Hässliche"):
Jedes Objekt in Salesforce (auch die Accounts) bekommt beim Anlegen eine eindeutige, interne ID (zu sehen im gleichnamigen Feld Id, wenn man ein Select-Statement darauf macht). Über diese ID könnte man das Update machen, allerdings müsste man sich in einem ersten Schritt zunächst "irgendwie" genau die ID ermitteln, auf die das Update durchgeführt werden soll. Keine besonders schöne Lösung, deshalb wird hier auch nicht näher darauf eingegangen.
Variante b - ("die Schöne"):
Auf vielen Salesforcesystemen werden den Objekten (wie z. B. Account) kundenspezifische Felder, sogenannte "External IDs", hinzugefügt. Diese External-ID-Felder werden befüllt mit den eindeutigen IDs aus dem Quellsystem (z. B. ein ERP-System).
Beispiel:
Wir erweitern zunächst unsere Eingangsdatei um die ID aus dem ERP System:
ADR;Sample Company LTD;169 Union Street;London;47311
Natürlich brauchen wir dafür auch ein weiteres Feld in der Quell-Struktur (erp_customer_id):
In Salesforce fügen wir auf Account ein neues Custom Field ERP_CUSTOMER_ID hinzu:
Wichtig ist hier der Haken bei "External ID".
Dieses Feld brauchen wir nun natürlich auch in der Zielstruktur - also am einfachsten den Datenbank-Knoten nochmal löschen, erneut vom Salesforce-Account importieren und die wenigen Zuordnungen nochmal durchführen (siehe oben., zusätzlich Quellstrukturfeld "erp_customer_id" und Zielstrukturfeld "ERP_CUSTOMER_ID__c" zuordnen).
Hinweis: Sollte das neu hinzugefügte Custom Field "ERP_CUSTOMER_ID__c" nicht in der Zielstruktur enthalten sein, so liegt dies am Metadaten-Cache des JDBC-Treibers (siehe auch Abschnitt Salesforce per SQL - Vorbereitungen) - dann ist wahrscheinlich ein Neustart des Integration Servers erforderlich!
Auf dem Datenbankknoten setzen wir den SQL-Modus gleich noch auf "try update before insert" (das ist "Best Practice" bei SQL-Knoten, die auf Salesforce zugreifen).
Als nächstes im Salesforce den zuvor erzeugten Account "Sample Company LTD" wieder entfernen (löschen).
Nun kommt der "Trick":
Auf dem Zielstrukturfeld "ERP_CUSTOMER_ID__c" fügen für den SQL-Spaltennamen ein "EXT_ID" hinzu (mit Leerzeichen nach Spaltennamen):
Jetzt starten wir das Profil nochmal, im Salesforce wird wieder ein Account "Sample Company LTD" angelegt.
Dann ändern wir die Straße in der Eingangsdatei ein weiteres Mal:
ADR;Sample Company LTD;20 Times Square;London;47311
Mit dieser geänderten Eingangsdatei starten wir das Profil erneut - es wurde kein neuer Account angelegt, der bestehende wurde mit der neuen Straße upgedated.
Hinweis: Mit der besonderen Syntax "<Spaltenname> EXT_ID" kann man dem JDBC-Treiber mitgeben, welches Feld (es muss sich natürlich im Salesforce auch um ein External-ID-Feld handeln) z. B. für ein insert/update/upsert zur Identifizierung des gewünschten Salesforce-Objektes verwendet werden soll.
Phase 5 (SQLBulkUnit): Contact anlegen
Nun sollen dem soeben angelegten Account noch mehrere Kontakte (vom Salesforce-Objekttyp "Contact") hinzugefügt werden.
Wir erhalten von unserem ERP-System folgende CSV-Datei mit den Daten der Kontakte:
CON;47311;SMITH;7844;smith@samplecompany.com
CON;47311;SCOTT;7369;scott@samplecompany.com
CON;47311;TURNER;7788;turner@samplecompany.com
In den Eingangsdaten ist eine "erp_contact_id" enthalten - wir legen (analog zum obigen Beispiel "erp_customer_id" auf Account) in Salesforce auf "Contact" ein neues Custom Field "ERP_CONTACT_ID" an:
Ein weiteres Profil "SF_contact_import" (mit entsprechender Quellstruktur, um die Kontakt-CSV einlesen zu können) wird angelegt:
Auf der Zielseite importieren wir wieder einen Datenbankknoten:
DB Alias: "salesforce" (Wir wollen mit der SQLBulkUnit arbeiten, deshalb wird hier nicht wie im ersten Profil der Alias "salesforce_nobulk" verwendet.)
Tabelle/Proc.: "Contact"
Hinweis: Auch hier gilt wieder (analog zu oben: ERP_CUSTOMER_ID auf Account): Sollte das neu hinzugefügte Custom Field "ERP_CONTACT_ID" nicht in der Zielstruktur enthalten sein, so liegt dies am Metadaten-Cache des JDBC-Treibers.
Hinweis: Der SQL-Modus des Datenbankknotens muss immer auf "try update before insert" gesetzt werden! Die SQLBulkUnit erwartet dies so. Vergisst man das, kann es in Phase 5 zu unklaren Fehlermeldungen wie "Malformed SQL Statement:..." kommen, weil die Syntax "EXT_ID" nicht aufgelöst wird.
Nun bitte folgende Feldzuordnungen herstellen:
Quellstrukturfeld |
Zielstrukturfeld |
contactname |
LastName |
erp_contact_id |
ERP_CONTACT_ID__c |
|
|
Und auf dem Zielfeld "ERP_CONTACT_ID__c" den SQL-Spaltennamen auf "ERP_CONTACT_ID__c EXT_ID" erweitern (siehe oben - wieder um die angelegten Contact-Objekte gegebenenfalls später anhand der erp_contact_id referenzieren zu können).
Der Pfad des Datenbankknotens wird auf den Quellstrukturknoten CON gesetzt - sonst importieren wir ja nur den Kontakt aus der ersten Zeile der Eingangsdatei.
Im Profil in Phase 5 wählen wir dieses Mal die SqlBulkUnit aus.
Wichtig ist hier, den Parameter "Target system is Salesforce" auf "true" zu setzen!
Von den anderen Parametern ist (bei Verwendung des Salesforce JDBC-Treibers) nur noch die Einstellung "max. sql statements in batch" relevant - diese steuert die Anzahl der Records pro Batch in einem Salesforce Bulk Job. Die anderen Einstellungen werden mit "Target system is Salesforce" ignoriert.
Zu guter Letzt legen wir in Phase 6 im Profil noch einen Antwortweg des Typs "Datei" an. Bei diesem setzen wir "Ist Backup der Zieldaten" auf "true" und den Inhalt bei den Inhalts-Einstellungen auf "Ausgabe von IU". Die SQLBulkUnit liefert nämlich, in Verbindung mit Salesforce, erfreulicherweise eine mit den Ergebnissen der Bulk Operation zurück (dazu gleich mehr).
Jetzt das Profil speichern und einmal über Neudurchlauf mit unserer Eingangsdatei mit den 3 Kontakten starten.
Wenn wir nun einen kurzen Blick ins Control Center werfen, werden wir feststellen, dass der Job ohne Fehlermeldung durchgelaufen ist. Allerdings lassen sich keine neu angelegten Kontakte im Salesforce auffinden. Woran liegt das?
Schauen wir uns zunächst die Ausgabedatei der SQLBulkUnit an (über Control Center/Logs/Übersicht/rechte Maustaste auf dem Profillauf/Zieldatei ansehen):
/*----- SalesforceJob: 7502o00000ZHb02AAD ----<
ApexProcessingTime=0
ApiActiveProcessingTime=8
ApiVersion=51.0
AssignmentRuleId=null
ConcurrencyMode=Parallel
ContentType=ZIP_XML
CreatedById=0052o000008ZaBCAA0
CreatedDate=2021-07-20T11:17:58.000Z
ExternalIdFieldName=ERP_CONTACT_ID__c
ID=7502o00000ZHb02AAD
JobID=7502o00000ZHb02AAD
NumberBatchesCompleted=1
NumberBatchesFailed=0
NumberBatchesInProgress=0
NumberBatchesQueued=0
NumberBatchesTotal=1
NumberRecordsFailed=3
NumberRecordsProcessed=3
NumberRetries=0
ObjectName=null
Operation=upsert
State=Closed
SystemModStamp=2021-07-20T11:18:03.000Z
TotalProcessingTime=144
object=Contact
/*----- Load Started: <Tue Jul 20 13:17:55 CEST 2021>-------------------------------*/
Job Identifier=3325
Total number of rows read 0
Total number of rows successfully loaded 0
Total number of rows that failed to load 0
Darin sieht man u.a. schon mal ein paar interessante Dinge:
a) Die Jobnummer in Salesforce: "SalesforceJob: 7502o00000ZHb02AAD"
b) Bei 3 Records gab es Fehler: "NumberRecordsFailed=3"
Wir wissen also schon mal, es ist was schiefgegangen. Um jedoch die genaue Ursache herauszufinden, werfen wir einen Blick ins Salesforce.
Über "Setup/Environments/Jobs/Bulk Data Load Jobs" können wir unseren Job anhand der Jobnummer aus der Ergebnisdatei der Integration Unit finden. In der Ergebnisdatei der Integration Unit ist immer noch ein "AAD" (oder eine andere dreistellige Kombination von Buchstaben) an die eigentliche Salesforce-Jobnummer angehängt. Wir suchen also nach: "7502o00000ZHb02":
Durch Klick auf die "Job ID" lässt man sich die Details zu dem Salesforce-Joblauf anzeigen:
Nun kann man über "View Result" einen Blick in das Ergebnis des Salesforce-Joblaufs werfen (nach dem Klick auf "View Result" wird ein Download angeboten, dabei handelt es sich um eine XML-Datei):
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<
results
xmlns
=
"http://www.force.com/2009/06/asyncapi/dataload"
>
<
result
>
<
errors
>
<
fields
>OwnerId</
fields
>
<
message
>Owner ID: owner cannot be blank</
message
>
<
statusCode
>INVALID_CROSS_REFERENCE_KEY</
statusCode
>
</
errors
>
<
success
>false</
success
>
<
created
>true</
created
>
</
result
>
<
result
>
<
errors
>
<
fields
>OwnerId</
fields
>
<
message
>Owner ID: owner cannot be blank</
message
>
<
statusCode
>INVALID_CROSS_REFERENCE_KEY</
statusCode
>
</
errors
>
<
success
>false</
success
>
<
created
>true</
created
>
</
result
>
<
result
>
<
errors
>
<
fields
>OwnerId</
fields
>
<
message
>Owner ID: owner cannot be blank</
message
>
<
statusCode
>INVALID_CROSS_REFERENCE_KEY</
statusCode
>
</
errors
>
<
success
>false</
success
>
<
created
>true</
created
>
</
result
>
</
results
>
Das ist des Rätsels Lösung. Wir haben dreimal die gleiche Fehlermeldung erhalten:
<fields>OwnerId</fields>
<message>Owner ID: owner cannot be blank</message>
-> Das Feld "OwnerId "darf nicht leer sein!
Anscheinend muss beim Anlegen eines Contact-Objektes in Salesforce immer eine Owner ID mitgegeben werden (bei Account war dies nicht zwingend der Fall).
Wenn wir mit dem Object Manager in Salesforce einen Blick auf die Felder von "Contact" werfen, stellen wir fest, dass es sich bei "Contact Owner" um ein Lookup-Object auf "User" handelt:
Wir müssen also im Profil das Zielstrukturfeld "OwnerId" mit der internen ID eines User-Objektes füllen.
Dazu machen wir es uns zunächst mal einfach. Wir setzen (z. B. über Admin-Konsole/Tools/SQL Monitor) ein Select-Statement an den DB Alias "salesforce_nobulk" ab:
select
id
from
user
where
username =
'<hier ihren gewünschten Salesforce Usernamen eintragen>'
Als Ergebnis erhalten wir in etwa so etwas:
0052o000008ZaBCAA0
Diese ermittelte User-ID tragen wir zunächst als Fixwert im Zielstrukturfeld "OwnerId" ein.
Nun starten wir das Profil "SF_contact_import" erneut mit unserer Eingangsdatei mit den 3 Kontakten. Jetzt sollten die 3 Kontakte in Salesforce angelegt werden.
Wie sie sicherlich schon erahnt haben, war dies wiederum die "hässliche" Variante . Kommen wir nun zur "schönen"
.
Zunächst löschen wir die 3 neuen Kontakte über die Salesforce-Oberfläche wieder.
Dann fügen wir dem Objekt "User" zunächst eine External ID in Form eines Custom Fields "ERP_PERSONAL_NR" hinzu (dieses Feld dient zur eindeutigen Referenzierung des gewünschten Users - wie das funktioniert, wird gleich klar):
Damit wir nun den gewünschten Salesforce User auch über die ERP_PERSONAL_NR identifizieren können, müssen wir diese natürlich auf dem User-Objekt noch setzen (z. B. über die Salesforce-GUI: Service Setup/ADMINISTRATION/Users):
Nun lässt sich schon mal der gewünschte User auch über die ERP_PERSONAL_NR finden ("__c" wird angehängt, da es sich um ein Custom Field handelt):
select
id
from
user
where
ERP_PERSONAL_NR__c =
'4711'
Dieses Select-Statement könnten wir nun auf dem Zielstrukturfeld "OwnerId" per Select-Funktion absetzen, um die Owner ID mit der entsprechenden internen ID des gewünschten Users zu füllen.
Schon ein bisschen schöner, aber es geht noch eleganter - hier kommt der nächste "Trick":
Wir erinnern uns: Beim Feld "Contact Owner" des Contact Objekts handelt es sich um ein Lookup-Object auf "User" (Data Type=Lookup(User) ).
Das bedeutet: "Contact Owner" hat eine sogenannte Nachschlagebeziehung (lookup relationship) zum User-Objekt (siehe auch Abschnitt "Salesforce per SQL - Vorbereitungen").
Diese Relationship können wir nun nutzen, um uns das Select-Statement zur Ermittlung der internen ID des Users zu sparen.
Dafür benötigen wir zunächst den Relationship-Namen. Diesen sehen wir in den Details des Feldes "Contact Owner" unter "Field Name" (hierfür wurde leider ein nicht besonders sprechender Name in Salesforce gewählt):
Der Relationship-Name lautet in diesem Fall also "Owner".
Hinweis: Handelt es sich bei dem lookup Feld um ein Benutzerdefiniertes Feld, dann ist der Relationship-Name der Feldname plus __r, zB wenn der API Name "Produkt__c" ist und der Feldname "Produkt" ist, dann lautet derRelationship-Name "Produkt__r"
Mit der Syntax "<relationshipname>.<external_id>" können wir den Treiber dazu bringen, dass er selbständig die gewünschte User ID ermittelt.
Dafür setzen wir im Profil den SQL-Spaltennamen des Feldes "OwnerId" auf "Owner.ERP_PERSONAL_NR__c"
Hinweis: Hier bitte auch tatsächlich die Anführungsstriche " am Anfang und Ende mit in den SQL-Spaltennamen schreiben.
Setzen Sie den Feldwert per Fixwert auf 4711.
Das bedeutet übersetzt:
"Schaue in der Relationship Owner (zeigt auf "User") nach, welcher User die ERP_PERSONAL_NR 4711 hat, nimm dessen ID und fülle diese in OwnerId."
Profil speichern, einmal laufen lassen - et voilà:
Die drei Kontakte sind in Salesforce wieder vorhanden (mit dem User, bei dem wir vorher die ERP_PERSONAL_NR auf 4711 gesetzt hatten, als Contact Owner) - und dies ganz ohne Verwendung der internen User ID im Profil!
Wichtig:
Bei <external_id> muss es sich um den API-Namen eines Feldes handeln, dass tatsächlich als External ID angelegt wurde , ansonsten funktioniert der Lookup nicht und es kommt zu Fehlermeldungen.
Ebenso funktioniert der Lookup nicht, wenn es sich bei dem Typ des Feldes um ein Polymorphic Field handelt (Lookup auf mehrere unterschiedliche Objekte. Beispiel in den Standardobjekten ist das Feld "Related To" des Objekts "Event"). Hier hilft nur, sich die gewünschte ID per Select-Statement zu holen.
Hierzu noch ein paar Hinweise:
Neben den Lookup-Beziehungen gibt es in Salesforce auch noch sogenannte Master-Detail-Beziehungen (nicht bei Standard-Objekten, da ein Standardobjekt laut Salesforce-Doku kein Detail sein kann).
Auch bei Master-Detail funktioniert die oben genannte Syntax, jedoch mit einer Abweichung:
Bei Objekten, die Master-Detail-Beziehungen haben, ist der relationshipname immer der API-Name des Feldes, nur mit "__r" anstatt "__c" am Ende.
Beispiel:
Das Objekt Kennzahl hat ein Feld AccountRef__c, welches vom Typ Master-Detail(Account) ist.
Somit lautet der relationshipname hier: AccountRef__r
Soweit, so gut. Allerdings sind wir noch nicht ganz fertig. Wir müssen die neu angelegten Kontakte auch noch unter unseren neuen Account "Sample Company LTD" hängen.
Dafür gibt es im Contact ein Feld AccountId, welches wiederum vom Typ Lookup(Account) ist. Den zur Füllung der AccountId notwendigen Mechanismus kennen Sie jetzt ja schon: Wir verwenden wieder die Syntax <relationshipname>.<external_id> auf dem SQL-Spaltennamen.
Dafür ermitteln wir zunächst über den Salesforce Object Manager den relationship-Namen (Details zum Feld "Account Name" in "Contact" - Field Name) = "Account".
Als External ID verwenden wir unsere weiter oben hinzugefügte ERP_CUSTOMER_ID.
Den SQL-Spaltennamen des Ausgangsfelds AccountId ändern wir somit zu: " Account.ERP_CUSTOMER_ID__c " (hier bitte auch wieder die Anführungsstriche nicht vergessen!).
Nun müssen wird das Feld AccountId noch mit der ERP Customer ID des Accounts füllen, unter welchen wir die 3 Kontakte hängen wollen. Die ERP Customer ID ist bereits in den Eingangsdaten enthalten, wir brauchen also nur noch eine Zuordnung von AccountId auf Quellstrukturfeld "erp_customer_id".
Profil speichern, einmal laufen lassen - jetzt sollten in Salesforce die 3 Kontakte unterhalb von Account "Sample Company LTD" angezeigt werden.
Dateiupload
Nun wollen wir unserem zuvor angelegten Account "Sample Company LTD" noch ein Attachment in Form einer Datei hinzufügen.
Hierfür erstellen wir ein neues Profil "SF_file_upload" mit folgender CSV-Eingangsdatei:
FILE;<Pfad zur Upload-Datei>
<Pfad zur Upload-Datei> = Pfadangabe zur Datei, die hochgeladen werden soll, z. B. "./temp/upload/sample.pdf"
Natürlich brauchen wir wieder eine entsprechende Quellstruktur:
Auf der Zielseite wird wieder ein neuer Datenbank-Knoten angelegt:
DB Alias: "salesforce_nobulk"
Tabelle/Proc.: "ContentVersion"
Die Datei muss Base64-kodiert im Zielstrukturfeld "VersionData" hochgeladen werden. Dafür verwenden wir die Funktion "read file into base64 coded byte-array(..)":
Parameter a der Funktion wird befüllt mit dem Inhalt des Quellstrukturfeldes "path" -> Der lokale Pfad zu unserer Datei, die wir hochladen wollen (diese muss natürlich existieren).
Neben "VersionData" gibt es noch eine Reihe weiterer Felder, die Pflichtfelder sind und daher befüllt werden müssen:
Feldname |
Bedeutung |
Wert in unserem Beispiel |
Title |
Der Name des Attachments, wie er in der Salesforce-Oberfläche angezeigt wird. |
Fixwert: "my pdf" |
PathOnClient |
Der Pfad zur Datei auf dem Client-Rechner. |
Feldzuordnung zum Quellstrukturfeld "path". |
FirstPublishLocationId |
Die Objekt ID, zu der diese ContentVersion zugeordnet werden soll |
Select-Funktion: select id from account where ERP_CUSTOMER_ID__c = '47311' |
Da es sich bei FirstPublishLocationId um ein Polymorphic Field (Lookup auf mehrere unterschiedliche Objekte) handelt, kann hier nicht mit "<relationshipname>.<external_id>" ein Lookup durchgeführt werden. Man muss sich die ID tatsächlich "von Hand" (z. B. per Select) holen.
Profil speichern (Phase 4 aktivieren nicht vergessen), einmal über Neudurchlauf der Eingangsdatei starten. Nun sollte in Salesforce im Account "Sample Company LTD" die hochgeladene Datei zu sehen sein:
Geschafft.