Salesforce Files expliqué : ContentDocument, ContentVersion et ContentDocumentLink
Un fichier Salesforce n'est pas un objet mais trois. Comprendre lequel fait quoi évite la plupart des erreurs de manipulation, en requête comme en migration.
Un administrateur qui ouvre pour la première fois le modèle des Fichiers Salesforce est surpris : là où il attendait un objet, il en trouve trois. Cette séparation n’est pas une complication gratuite. Elle explique la plupart des comportements qu’on trouve étranges au début, et elle conditionne toute manipulation de fichiers en masse.
Les trois objets
ContentDocument — le document
ContentDocument représente le document en tant qu’entité, indépendamment de son contenu à un instant donné. C’est lui qui porte l’identité du fichier dans la durée : son titre, son extension, sa taille, son propriétaire.
Il ne contient pas le fichier lui-même. Il pointe vers sa version courante par le champ LatestPublishedVersionId.
Un point à retenir : on ne crée pas un ContentDocument directement. Il apparaît quand on crée une première version.
ContentVersion — le contenu, à un moment donné
ContentVersion porte le contenu binaire du fichier, dans le champ VersionData, ainsi que la date, l’auteur et le numéro de version.
Un même ContentDocument peut compter dix ContentVersion. Une seule a IsLatest = true.
C’est par cet objet qu’on crée un fichier :
- créer une
ContentVersionsansContentDocumentIdengendre un nouveau document ; - créer une
ContentVersionavec unContentDocumentIdajoute une version au document existant.
Cette subtilité est la source d’erreur la plus fréquente des imports en masse : on obtient huit cents documents distincts là où on attendait huit cents versions, ou l’inverse.
ContentDocumentLink — le rattachement
ContentDocumentLink relie un document à un enregistrement. C’est un objet de jonction, avec ContentDocumentId d’un côté et LinkedEntityId de l’autre.
Sa conséquence est considérable : un même document peut être rattaché à autant d’enregistrements que nécessaire, sans être dupliqué. Un contrat lié au compte et à l’opportunité occupe une seule fois l’espace de stockage.
Deux champs méritent attention :
ShareTypedécide de ce que le destinataire peut faire :Vpour lecture seule,Cpour collaborateur,Ipour un accès déduit de celui de l’enregistrement parent.Visibilitydécide qui voit le lien :AllUsers,InternalUsers, ouSharedUsers.
Trois pièges classiques
On ne peut pas interroger ContentDocumentLink librement
Une requête SOQL sur ContentDocumentLink doit filtrer sur ContentDocumentId ou sur LinkedEntityId. Sans filtre, la plateforme refuse.
SELECT ContentDocumentId FROM ContentDocumentLink
WHERE LinkedEntityId = '001...'
La limitation surprend, mais elle a sa logique : la table est immense, et une requête sans filtre reviendrait à demander tous les rattachements de l’org.
Conséquence pratique : pour lister les documents d’un ensemble d’enregistrements, il faut d’abord la liste des identifiants, puis interroger par lots.
Supprimer un lien ne supprime pas le document
Retirer un ContentDocumentLink détache le fichier de l’enregistrement, sans plus. Le document continue d’exister, il n’est simplement plus visible depuis cette fiche.
C’est ce qui produit les documents orphelins : ils occupent du stockage, ils n’apparaissent nulle part, et personne ne les cherche puisque personne ne sait qu’ils sont là.
À l’inverse, supprimer le ContentDocument supprime toutes ses versions et tous ses rattachements d’un coup.
La suppression passe par la corbeille
Un ContentDocument supprimé part à la corbeille et continue de compter dans le quota de stockage jusqu’à sa purge. Après une opération de nettoyage, la page d’utilisation du stockage ne bouge donc pas immédiatement — ce qui fait conclure à tort que le nettoyage a échoué.
Le partage public : ContentDistribution
Un quatrième objet mérite d’être connu. ContentDistribution engendre un lien public vers une version de fichier, consultable sans licence Salesforce.
Ses champs utiles :
ExpiryDate, la date au-delà de laquelle le lien cesse de fonctionner ;PreferencesPasswordRequired, qui protège le lien par un mot de passe ;PreferencesAllowOriginalDownload, qui autorise ou non le téléchargement ;ViewCount, le nombre de consultations ;DistributionPublicUrl, le lien lui-même.
C’est l’outil natif du partage externe. Sa limite : quiconque possède l’adresse accède au document, et le suivi se réduit à un compteur.
Et les Attachments dans tout ça ?
L’objet Attachment est le mécanisme antérieur. Son modèle tient en un objet : un ParentId, un Body. Ni version, ni rattachement multiple, ni partage public.
La plateforme n’y investit plus depuis des années, et toutes les fonctions récentes sont du côté des Fichiers. Pour un projet documentaire engagé aujourd’hui, la conversion est un préalable, pas une option.
Ce qu’il faut retenir
ContentDocument est le document, ContentVersion son contenu à un instant donné, ContentDocumentLink son rattachement à un enregistrement. Cette séparation permet le versionnement et le rattachement multiple sans duplication, qui sont les deux forces du modèle.
Les pièges tiennent en trois lignes : la requête sur les liens exige un filtre, retirer un lien ne supprime pas le document, et la corbeille retient le stockage.
Toute solution documentaire native sérieuse s’appuie sur ces objets sans les contourner. C’est le cas de gedone : il ajoute la connaissance du client, du tag et de la date de validité, sans jamais déplacer ni copier le fichier lui-même.