Salesforce et HubSpot adoptent des approches fondamentalement différentes pour la structure des données. Ces différences façonnent les modèles de base, les patterns de jointure, la logique d’enrichissement intermédiaire et la conception du schéma.
Salesforce : orienté métadonnées, relationnel
Salesforce utilise une architecture multi-tenant orientée métadonnées. En coulisses, toutes les données clients vivent dans des tables partagées, avec des métadonnées définissant le schéma pour chaque tenant (org). Mais du point de vue de la modélisation des données, ce qui compte est que les données Salesforce sont intrinsèquement relationnelles avec des clés étrangères bien définies.
Objets standards
Les objets standards suivent des schémas prévisibles et cohérents dans chaque org Salesforce :
- Account — Entreprises ou organisations
- Contact — Personnes associées aux comptes
- Lead — Prospects pas encore associés aux comptes
- Opportunity — Deals dans le pipeline
- Case — Tickets de support
- Campaign — Campagnes marketing et leurs membres
Chaque objet se mappe proprement vers une table brute dans votre entrepôt, puis vers un modèle de base, puis vers un ou plusieurs modèles intermédiaires ou mart.
Objets personnalisés
Les objets personnalisés portent un suffixe __c : Territory__c, Subscription__c, Product_Usage__c. Leurs champs portent également le suffixe : annual_revenue__c, renewal_date__c. Ce suffixe est préservé lors de l’extraction dans votre entrepôt, donc vos modèles de base doivent gérer leur renommage selon les conventions de nommage de votre projet.
-- base__salesforce__territory.sqlSELECT id AS territory__id, name AS territory__name, region__c AS territory__region, target_revenue__c AS territory__target_revenue, owner_id AS territory__owner_idFROM {{ source('salesforce', 'territory__c') }}WHERE NOT _fivetran_deletedTypes de relations
Les relations Salesforce se déclinent en deux variantes :
Les relations Lookup créent un couplage lâche avec une clé étrangère nullable. Un Contact a un lookup vers Account via account_id, mais le Contact peut exister sans Account. Dans votre entrepôt, cela implique des LEFT JOINs — tous les Contacts n’auront pas de données Account correspondantes.
Les relations Master-Detail créent un couplage fort avec des suppressions en cascade. Si l’enregistrement maître est supprimé, tous les enregistrements de détail le sont également. La clé étrangère est obligatoire (non-nullable). Dans votre entrepôt, cela signifie que les INNER JOINs sont sûrs — chaque enregistrement de détail a un maître.
L’implication pratique : lors de l’écriture de vos modèles intermédiaires, vérifiez si une relation Salesforce est Lookup ou Master-Detail. Cela détermine si vous utilisez LEFT JOIN ou INNER JOIN, et si vous devez gérer les NULL dans la colonne de clé étrangère.
Relations polymorphiques
Salesforce a des champs polymorphiques comme WhoId sur Task et Event, qui peuvent référencer soit un Contact soit un Lead. Et WhatId, qui peut référencer un Account, une Opportunity ou un objet personnalisé. Ces champs nécessitent un routage basé sur CASE dans votre couche intermédiaire :
CASE WHEN who_id LIKE '003%' THEN 'Contact' WHEN who_id LIKE '00Q%' THEN 'Lead'END AS who_typeLes préfixes d’ID Salesforce sont déterministes — 003 est toujours Contact, 00Q est toujours Lead, 001 est toujours Account, 006 est toujours Opportunity. Ce routage basé sur les préfixes est fiable mais non évident si vous n’avez pas travaillé avec Salesforce auparavant.
HubSpot : basé sur les associations, many-to-many
HubSpot adopte une approche fondamentalement différente. Au lieu de clés étrangères sur les objets eux-mêmes, HubSpot utilise un système d’associations séparé pour connecter les objets.
Objets de base
Les objets de base de HubSpot sont plus simples que les objets standards Salesforce :
- Contacts — Personnes
- Companies — Organisations
- Deals — Opportunités commerciales
- Tickets — Demandes de support
Les propriétés sont préfixées par property_ dans les données extraites : property_email, property_firstname, property_lifecyclestage. Vos modèles de base les renomment :
-- base__hubspot__contact.sqlSELECT id AS contact__id, property_email AS contact__email, property_firstname AS contact__first_name, property_lastname AS contact__last_name, property_lifecyclestage AS contact__lifecycle_stage, property_hs_lead_status AS contact__lead_status, property_createdate AS contact__created_atFROM {{ source('hubspot', 'contact') }}WHERE NOT _fivetran_deletedLe modèle d’associations
C’est là que HubSpot diverge nettement de Salesforce. Dans Salesforce, un Contact appartient à un Account (via account_id). Dans HubSpot, un Contact peut être associé à plusieurs Companies, et un Deal peut impliquer plusieurs Contacts dans des rôles différents.
Les associations vivent dans des tables de liaison séparées :
contact_company— Lie les contacts aux entreprises (many-to-many)deal_contact— Lie les deals aux contacts (many-to-many)deal_company— Lie les deals aux entreprises (many-to-many)
Chaque association peut porter un label optionnel comme « Decision Maker », « Billing Contact » ou « Primary Company ». Ces labels ajoutent une signification sémantique à la relation mais compliquent votre logique de jointure.
En pratique, cela signifie que vos modèles intermédiaires nécessitent des jointures explicites sur les tables de liaison :
-- int__deal_contact_enriched.sqlWITH
deals AS ( SELECT deal__id, deal__name, deal__amount, deal__stage FROM {{ ref('base__hubspot__deal') }}),
contacts AS ( SELECT contact__id, contact__email, contact__first_name FROM {{ ref('base__hubspot__contact') }}),
associations AS ( SELECT deal_id AS deal__id, contact_id AS contact__id, label AS association__label FROM {{ ref('base__hubspot__deal_contact') }})
SELECT deals.deal__id, deals.deal__name, deals.deal__amount, deals.deal__stage, contacts.contact__id, contacts.contact__email, contacts.contact__first_name, associations.association__labelFROM dealsINNER JOIN associations ON deals.deal__id = associations.deal__idINNER JOIN contacts ON associations.contact__id = contacts.contact__idLa sortie est une ligne par paire deal-contact, pas une ligne par deal. C’est une granularité différente de celle que vous obtiendriez d’un modèle d’opportunités Salesforce, et la logique en aval doit en tenir compte.
Pattern d’association primaire
Quand vous avez besoin d’une seule entreprise par contact (pour correspondre à un modèle similaire à Salesforce), vous devez choisir une association « primaire ». Approches courantes :
- Filtrer sur un label spécifique (par exemple,
WHERE association__label = 'Primary') - Prendre l’association la plus ancienne par date de création
- Prendre l’entreprise avec le plus de contacts associés (un proxy pour l’entreprise « principale »)
Aucune n’est parfaite. Le modèle de données de HubSpot supporte genuinement le many-to-many, donc le réduire à one-to-one implique toujours une décision métier sur quelle association prend la priorité.
Implications de modélisation
Les différences structurelles ont des conséquences directes pour votre projet dbt :
| Préoccupation | Salesforce | HubSpot |
|---|---|---|
| Pattern de jointure | Jointures directes sur clé étrangère | Jointures sur tables de liaison |
| Gestion de la granularité | Directe — la FK préserve la granularité | Risque de fan-out du many-to-many |
| Nombre de modèles de base | Un par objet | Un par objet + un par table de liaison |
| Relation primaire | Intégrée (FK sur l’enfant) | Doit être dérivée des associations |
| Champs personnalisés | Suffixe __c, renommage dans la base | Préfixe property_, renommage dans la base |
La modélisation Salesforce finit par concerner principalement la gestion de la mutabilité, de la hiérarchie (Account → Contact → Opportunity) et des particularités d’extraction comme les champs de formule. La modélisation HubSpot concerne la résolution correcte des relations many-to-many et la décision sur comment aplatir les associations pour le reporting.
Les deux systèmes bénéficient de la même architecture dbt en trois couches, mais la couche intermédiaire fait un travail très différent pour chacun. Pour Salesforce, l’intermédiaire enrichit les entités avec des données liées. Pour HubSpot, l’intermédiaire résout la complexité des associations en jointures interrogeables.