Résoudre le fait que les clés primaires ne font pas partie de votre domaine d'activité


25

Dans presque toutes les circonstances, les clés primaires ne font pas partie de votre domaine d'activité. Bien sûr, vous pouvez avoir des objets importants pour l'utilisateur avec des indices uniques ( UserNamepour les utilisateurs ou OrderNumberpour les commandes) mais dans la plupart des cas, il n'est pas nécessaire pour les entreprises d'identifier ouvertement les objets de domaine par une seule valeur ou un ensemble de valeurs, pour quiconque, mais peut-être un utilisateur administratif. Même dans ces cas exceptionnels, en particulier si vous utilisez des identificateurs uniques globaux (GUID) , vous aimerez ou voudrez utiliser une clé alternative plutôt que d'exposer la clé primaire elle-même.

Donc, si ma compréhension de la conception basée sur le domaine est précise, les clés primaires n'ont pas besoin et ne doivent donc pas être exposées, et un bon débarras. Ils sont moches et étouffent mon style. Mais si nous choisissons de ne pas inclure les clés primaires dans le modèle de domaine, il y a des conséquences:

  1. Naïvement, les objets de transfert de données (DTO) qui dérivent exclusivement de combinaisons de modèles de domaine n'auront pas de clés primaires
  2. Les DTO entrants n'auront pas de clé primaire

Donc, est-il sûr de dire que si vous voulez vraiment rester pur et éliminer les clés primaires dans votre modèle de domaine, vous devez être prêt à pouvoir traiter chaque demande en termes d'index uniques sur cette clé primaire?

Autrement dit, laquelle des solutions suivantes est la bonne approche pour traiter l'identification d'objets particuliers après la suppression de PK dans les modèles de domaine?

  1. Être capable d'identifier les objets dont vous avez besoin pour traiter avec d'autres attributs
  2. Récupérer la clé primaire dans le DTO; c'est-à-dire, l'élimination du PK lors du mappage de la persistance au domaine, puis la recombinaison du PK lors du mappage du domaine au DTO?

EDIT: Rendons cela concret.

Dites mon modèle de domaine est VoIPProviderqui comprend des domaines tels que Name, Description, URL, ainsi que des références aiment ProviderType, PhysicalAddresset Transactions.

Supposons maintenant que je souhaite créer un service Web qui permettra aux utilisateurs privilégiés de gérer le VoIPProviders.

Peut-être qu'un identifiant convivial est inutile dans ce cas; après tout, les fournisseurs de VoIP sont des entreprises dont les noms ont tendance à être distincts au sens informatique et même assez distincts au sens humain pour des raisons commerciales. Il suffit donc de dire qu'un unique VoIPProviderest complètement déterminé par (Name, URL). Alors maintenant, disons que j'ai besoin d'une méthode PUT api/providers/voippour que les utilisateurs privilégiés puissent mettre à jour les VoIPfournisseurs. Ils envoient un VoIPProviderDTO, qui inclut de nombreux champs de la VoIPProvider, mais pas tous , y compris un certain aplatissement potentiel. Cependant, je ne peux pas lire dans leurs pensées et ils ont encore besoin de me dire de quel fournisseur nous parlons.

Il semble que j'ai 2 (peut-être 3) options:

  1. Inclure une clé primaire ou une clé alternative dans mon modèle de domaine et l'envoyer au DTO, et vice versa
  2. Identifiez le fournisseur qui nous intéresse via l'index unique, comme (Name, Url)
  3. Introduisez une sorte d'objet intermédiaire qui peut toujours mapper entre la couche de persistance, le domaine et le DTO d'une manière qui n'expose pas les détails d'implémentation de la couche de persistance - par exemple en introduisant un identifiant temporaire en mémoire lors du passage du domaine au DTO et vice-versa,

1
Point à méditer: souvent, la communication avec les experts du domaine s'appauvrit lorsque la PK de substitution est utilisée lorsqu'une bonne clé d'entreprise existe. Il semble que nous finissions par travailler pour le cadre ORM, et non l'inverse.
Tulains Córdova

@ user61852 bien quel que soit l'ORM, même si vous êtes vraiment de bas niveau, vous avez toujours besoin d'une clé primaire dans l'implémentation de la couche de base de données. Je suis donc d'accord pour dire que la PK de substitution vous offre des avantages par rapport à la PK réelle utilisée par un mécanisme de persistance spécifique, mais si cette PK représente vraiment un objet métier significatif, elle est nécessairement unique et possède donc au moins une propriété liée à l'entreprise unique définissant ça, non?
tacos_tacos_tacos

1
Tous les avantages des substituts sont liés à l'ordinateur et aucun liés à l'homme.
Tulains Córdova

2
@ user61852: Je suis d'accord à 100% (ai-je écrit quelque chose de différent?). Pour les moyens de communication, utilisez une "clé métier". Ajoutez également des contraintes uniques pour n'importe quelle clé d'entreprise. Mais évitez d'utiliser des clés d'entreprise pour implémenter réellement vos références de base de données.
Doc Brown

2
les clés d'entreprise sont éternellement uniques - jusqu'à ce qu'elles ne le soient pas. Si vous utilisez les clés métier en tant que principal lorsque cela se produit, la modification des règles métier interrompt davantage de choses.
psr

Réponses:


31

C'est ainsi que nous résolvons cela (depuis plus de 15 ans, quand même le terme «conception pilotée par domaine» n'a pas été inventé):

  • lorsque vous mappez le modèle de domaine à une implémentation de base de données ou à un modèle de classe dans un langage de programmation spécifique, vous disposez d'une règle simple et cohérente comme «pour chaque objet de domaine mappé à une table relationnelle, la clé primaire est« TablenameID ».
  • cette clé primaire est complètement artificielle, elle a toujours le même type et n'a aucune signification commerciale - juste une clé de substitution
  • la "version graphique" de votre modèle de domaine (celui que vous utilisez pour parler à vos experts de domaine) ne contient pas de clés primaires. Vous ne les exposez pas directement aux experts (mais vous les exposez à quiconque implémente réellement du code pour le système).

Ainsi, chaque fois que vous avez besoin d'une clé primaire à des fins techniques (comme le mappage de relations avec une base de données), vous en avez une disponible, mais tant que vous ne voulez pas la "voir", changez votre niveau d'abstraction en "modèle d'experts de domaine" ". Et vous n'avez pas à maintenir "deux modèles" (un avec PK et un sans); au lieu de cela, conservez uniquement un modèle sans PK et utilisez un générateur de code pour créer le DDL pour votre base de données, qui ajoute automatiquement le PK selon les règles de mappage.

Notez que cela n'interdit pas d'ajouter des "clés professionnelles" comme un "OrderNumber" supplémentaire, en plus du substitut OrderID. Techniquement, ces clés d'entreprise deviennent des clés alternatives lors du mappage vers votre base de données. Évitez simplement de les utiliser pour créer des références à d'autres tables, préférez toujours utiliser les clés de substitution si possible, cela rendra les choses beaucoup plus faciles.

À votre commentaire: l'utilisation d'une clé de substitution pour identifier les enregistrements n'est pas une opération commerciale, c'est une opération purement technique. Pour que cela soit clair, regardez votre exemple: tant que vous ne définissez pas de contraintes uniques supplémentaires, il serait possible d'avoir deux objets VoIPProvider avec la même combinaison de (nom, URL), mais différents VoIPProviderID.


Qu'en est-il de l'étape consistant à extraire l'objet de domaine de l'objet de persistance renvoyé (modèle d'entité ou de ligne de table, ou autre), à ​​passer à DTO, à le reprendre et à revenir à la persistance? Cela se fait-il uniquement via une clé de substitution (c'est-à-dire une définition de l'unicité orientée métier) qui nécessite une résolution sur chaque opération de persistance?
tacos_tacos_tacos

1
@tacos_tacos_tacos: restons sur votre exemple VoIPProvider. J'ajouterais en fait un "VoIPProviderID" à votre DTO, au moins du côté "implémentation" (si vous avez aussi une version graphique pour vos experts de domaine, je ne la montrerais probablement pas là). À des fins de mise à jour, la manière standard d'identifier un VoIPProvider spécifique doit être le "VoIPProviderID" que vous avez récupéré lors de l'extraction des données de la base de données. Si les utilisateurs de votre API préfèrent une identification par (nom, URL), indiquez-le en plus. ...
Doc Brown

... et si les performances semblent devenir un problème réel et mesurable, vous pouvez également envisager de mettre en cache le mappage (nom, URL) vers VoIPProviderID quelque part. Mais je ne recommanderais pas de mettre en œuvre une telle optimisation au préalable, prématurément.
Doc Brown

1
Les clés naturelles semblent parfois vraiment convaincantes, mais il est trop facile de les graver (par exemple, "whoops, maintenant j'ai plusieurs locataires et ce n'est plus unique").
Casey

1
@corsiKa: dans le contexte de ce que l'OP a demandé, je recommanderais fortement d'avoir une clé purement générée automatiquement "OrderID" (qui n'est imprimée sur aucun reçu, mais seulement utilisée pour des éléments internes comme les références de base de données), et une clé métier distincte "OrderNumber" (qui peut, par exemple, contenir quelque chose comme l'année en cours, qui peut être utilisé pour le tri et le filtrage, qui peut être modifié / corrigé par la suite, et qui peut être imprimé sur les reçus). OP a demandé "Domain Driven Design", le "OrderNumber" fait partie du modèle de domaine, tandis que le "OrderID" n'est qu'un détail d'implémentation.
Doc Brown

4

Vous devez être en mesure d'identifier de nombreux objets par un index unique, et ce n'est pas ce qu'est une clé primaire (ou du moins implique qu'une est présente).

Des index uniques sont disponibles afin que vous puissiez restreindre davantage votre schéma de base de données, et non comme un remplacement de gros pour les PK. Si vous n'exposez pas les PK parce que c'est laid, mais exposez une clé unique à la place ... vous ne faites rien de différent. (Je suppose que vous n'obtenez pas un PK et une colonne d'identité mélangés ici?)


Lorsque je veux effectuer une opération impliquant une instance spécifique d'un objet de domaine qui est persistant, je dois être en mesure d'inclure dans le DTO de manière explicite (via une sorte de clé, principale ou un ID convivial alternatif qui est unique à cette table et pourrait aussi bien être un index sur la table) ou implicitement (via une combinaison de champs dont les valeurs identifient de manière unique un enregistrement spécifique) ... et en outre, dans un sens pratique, je devrais envoyer dans le DTO une certaine forme de suivi des modifications si je le faisais dans l'autre sens (OriginalVal vs NewVal pour tous les champs qui identifient l'enregistrement), non?
tacos_tacos_tacos

la question explicite ou implicite n'est-elle pas la même différence? Vous pouvez avoir un PK qui s'étend sur plusieurs colonnes, tout comme un index unique. Je ne vois pas de différence entre eux à vos fins.
gbjbaanb

Bien sûr, nous pourrions avoir un PK sur plusieurs colonnes par exemple. Mais pour moi, il y a une fuite dans la base de données (le stockage) qui ne devrait rien avoir à voir avec le cœur et l'âme, l'entité commerciale. Si un certain nombre de champs d'entité commerciale s'étend sur le PK de la base de données, alors parfait. Mais cela ne devrait pas nécessairement être l'inverse, n'est-ce pas?
tacos_tacos_tacos

Vous y pensez trop. Un index unique est tout autant un artefact du schéma DB qu'un PK. Pensez-y de cette façon - un PK n'est que le premier (ou principal) index unique. son «spécial» parce que vous n'avez généralement besoin que d'un tel index.
gbjbaanb

Certes, mais tout objet de domaine significatif doit être identifiable strictement à partir d'au moins un de ses champs liés à l'entreprise, non? Le fait que cela définit un index dans la base de données est plus pour des raisons de performances que pour être utilisé pour interroger la base de données facilement ... Je préférerais un PK à une colonne à un index unique à 6 colonnes, et ils servent vraiment à des fins différentes - un PK (ou un index avec un petit nombre de champs) est également disponible pour la commodité du DBA / DBD, non?
tacos_tacos_tacos

4

Sans clés primaires dans le frontend, le backend n'a aucun moyen facile de savoir ce que vous envoyez. Pour résoudre ce problème, vous auriez besoin d'une tonne de travail supplémentaire pour analyser les données, ce qui nuirait aux performances et prendrait probablement plus de temps et serait plus laid que d'attacher une clé à chaque élément.

À titre d'exemple, disons que je souhaite modifier un message dans une application; comment l'application pourrait-elle savoir quel message je souhaite modifier sans clé primaire attachée? L'édition d'objets se produit tout le temps et cela sans clés est presque impossible. Mais si vous avez des objets qui ne sont pas censés être modifiés, sautez la clé si vous pensez que cela vous distrait, mais avoir des clés primaires peut améliorer les performances ici.


Eh bien, le message est un exemple extrême de, mais même alors nous savons MessageSender, MessageRecipient, TimeSent- qui doit être unique.
tacos_tacos_tacos

1
@tacos_tacos_tacos, alors comment créez-vous les FK pour les autres tables? Il doit s'agir de MessageSenderId, qui correspond probablement à une table Users sur UserId. Vous ne voudriez pas utiliser le nom d'utilisateur comme clé entre les tables, car cela pourrait changer et devenir un cauchemar de maintenance. C'est pourquoi vous joignez généralement les tables uniquement à l'aide des clés primaires, et non d'une autre colonne (il existe certainement des exceptions). Cette structure db doit encore être appliquée. Maintenant, vous pouvez toujours accéder à un modèle CQRS pour votre application ... auquel cas les règles changent. Surtout si vous utilisez également Event Sourcing.
CaffGeek

4

La raison pour laquelle nous utilisons un PK non lié à l'entreprise est de garantir que notre système dispose d'une méthode simple et cohérente pour déterminer ce que veut l'utilisateur.

Je vois que vous avez répondu avec un commentaire: MessageSender, MessageRecipient, TimeSent (pour un message). Vous pouvez TOUJOURS avoir une ambiguïté de cette façon (par exemple, avec des messages générés par le système qui se déclenchent sur quelque chose qui se produit souvent). Et comment allez-vous valider MessageSender et MessageRecipient ici? Supposons que vous les validiez en utilisant FirstName, Lastname, DateOfBirth, vous allez éventuellement vous retrouver dans une situation où vous avez 2 personnes nées le même jour avec le même nom exact. Sans oublier que vous rencontrerez une situation où vous aurez un message nommé tacostacostacos-America-1980-Doc Brown-France-1965-23/5/2014-11:43:54.003UTC+200. C'est un monstre d'un nom, et vous n'avez toujours aucune garantie que vous n'en aurez qu'un seul.

la raison pour laquelle nous utilisons une clé primaire est parce que nous SAVONS qu'elle sera unique pour la durée de vie du logiciel, quelles que soient les données saisies, et nous SAVONS que ce sera un format prévisible (que se passe-t-il si votre clé ci-dessus a un tiret dans un nom d'utilisateur - tout votre système va à la merde).

Vous n'avez pas besoin de montrer votre identifiant à votre utilisateur. Vous pouvez masquer cela (à la vue si nécessaire, via l'URL).

Une autre raison pour laquelle un PK est si utile est quelque chose que vous pouvez déduire de ce qui précède: un PK fait en sorte que vous n'ayez pas à forcer l'ordinateur à interpréter le code généré par l'utilisateur. Si un utilisateur chinois utilise votre code et entre un tas de caractères chinois, votre code n'a soudainement pas besoin de pouvoir travailler avec ceux-ci en interne, mais il peut simplement utiliser le Guid que le système a généré. Si vous avez un utilisateur arabe qui entre en écriture arabe, votre système n'a pas à y faire face en interne, mais peut essentiellement ignorer qu'il est là.

Comme d'autres l'ont dit, un Guid est quelque chose qui peut être stocké en interne dans une taille fixe. Vous savez avec quoi vous travaillez et c'est quelque chose qui peut être universellement utilisé. Vous n'avez pas besoin de créer des règles de conception sur la façon dont vous créez un certain identifiant et l'enregistrez. Si votre système ne prend que les 10 premières lettres d'un nom, il ne voit aucune différence entre Michael Guggenheimer et Michael Gugstein, et il les confondra. Si vous le coupez à n'importe quelle longueur arbitraire, vous pouvez vous retrouver dans la confusion. Si vous limitez l'entrée utilisateur, vous pouvez rencontrer des problèmes avec les limitations utilisateur.

Lorsque je regarde des systèmes existants comme Dynamics CRM, ils utilisent également la clé interne (le PK) pour que l'utilisateur appelle un seul enregistrement. Si un utilisateur a une requête qui n'implique pas l'ID, il renvoie un tableau de réponses possibles et laisse l'utilisateur choisir. S'il y a une chance d'ambiguïté, ils donneront le choix à l'utilisateur.

Enfin, c'est aussi un gage de sécurité à travers l'obscurité. Si vous ne connaissez pas l'ID d'enregistrement, votre seule option est de le deviner. si l'ID est facile à deviner (car les informations dont il est fait sont accessibles au public), tout le monde peut le modifier. Vous pouvez même inciter l'utilisateur à le modifier via des méthodes CSRF ou XSS classiques. Maintenant, évidemment, votre sécurité devrait déjà avoir été prise en compte et atténuée avant de publier la version en direct, mais vous devriez toujours rendre plus difficile les abus potentiels.


1

Lorsque vous émettez un identifiant pour un système externe, vous ne devez donner que des URI, ou alternativement une clé ou un ensemble de clés qui ont les mêmes propriétés qu'un URI, plutôt que d'exposer directement la clé primaire de la base de données (à partir d'ici, je ferai référence à URI ou une clé ou un ensemble de clés qui ont les mêmes propriétés que l'URI comme juste l'URI, en d'autres termes, un URI ci-dessous ne signifie pas nécessairement l'URI RFC 3986).

Un URI peut ou non contenir la clé primaire de l'objet et il peut ou non être composé de clés alternatives. Ça n'a pas vraiment d'importance. Ce qui importe, c'est que seul le système qui génère l'URI est autorisé à diviser ou à combiner l'URI pour comprendre ce qu'est l'objet référé. Les systèmes externes doivent toujours utiliser l'URI comme identifiant opaque. Peu importe si un utilisateur humain peut identifier qu'une partie de l'URI est en fait une clé de substitution de base de données ou qu'elle se compose de plusieurs clés d'entreprise regroupées, ou qu'il s'agit en fait d'une base64 de ces valeurs. Ce ne sont pas pertinents. Ce qui importe, c'est que le système externe ne devrait pas être obligé de comprendre ce que signifie l'identifiant pour utiliser l'identifiant. Les systèmes externes ne devraient jamais être tenus d'analyser les composants au sein de l'identifiant ou de combiner un identifiant avec d'autres identifiants pour faire référence à quelque chose dans votre système.

L'utilisation du GUID remplit certains de ces critères, cependant un identifiant comme le GUID peut être difficile à déréférencer dans l'objet même au sein de votre système, donc les clés qui sont opaques pour égaliser votre système comme un GUID ne doivent être utilisées que si le client analyse l'URI / l'identificateur réellement pose un risque pour la sécurité.

Revenons à votre exemple VoIP, disons qu'un fournisseur VoIP peut être déterminé de manière unique soit par (VoIPProviderID) soit par (Nom, URL) ou par (GUID). Lorsqu'un système externe doit mettre à jour le fournisseur VoIP, il peut simplement transmettre un PUT / provider / by-id / 1234 ou PUT /provider/foo-voip/bar-domain.comou PUT /3F2504E0-4F89-41D3-9A0C-0305E82C3301et votre système comprendra que le système externe veut mettre à jour le VoIPProvider. Ces URI sont générés par votre système et seul votre système doit comprendre que tout cela signifie la même chose. Le système externe doit simplement traiter tout ce qui est dans l'URI comme fondamentalement a PUT <whatever>.

Supposons que vous ayez les données de différents fournisseurs de VoIP stockées dans différentes tables avec différents schémas (ainsi un jeu de clés complètement différent identifie chaque fournisseur de VoIP en fonction de la table dans laquelle il est stocké). Lorsque vous avez un URI, ils peuvent être consultés uniformément par le système externe, sans égard à la façon dont votre système identifie le fournisseur VoIP particulier. Pour le système externe, tout est simplement un pointeur opaque.

Lorsque votre système utilise un URI pour faire référence aux objets de cette manière, vous ne divulguez rien concernant la façon dont vous implémentez votre système. Vous générez l'URI et le client vous le transmet simplement.


0

Je vais devoir viser cette déclaration extrêmement inexacte et naïve:

Peut-être qu'un identifiant convivial est inutile dans ce cas; après tout, les fournisseurs de VoIP sont des entreprises dont les noms ont tendance à être distincts au sens informatique et même assez distincts au sens humain pour des raisons commerciales.

Les noms sont terribles comme clés, car ils changent fréquemment. Une entreprise peut avoir plusieurs noms au cours de sa vie, elle peut fusionner, se scinder, fusionner à nouveau, créer une filiale distincte à des fins fiscales spécifiques qui compte 0 employé mais tous les clients qui embauchent ensuite des employés d'une filiale complètement différente.

Ensuite, nous entrons dans le fait que les noms d'entreprises ne sont même pas uniques à distance, comme le montre le point de repère Apple contre Apple .

Un bon mappeur ou cadre relationnel d'objet doit retirer les clés primaires et les rendre invisibles, mais elles sont là, et elles seront généralement le seul moyen d'identifier de manière unique un objet dans votre base de données.

Pour référence, je préfère la façon dont django gère cela:

class VoipProvider(models.Model):
    name=fields.TextField()
    address=fields.TextField()

class Customer(models.Model):
    name=fields.TextField()
    address=fields.TextField()
    voipProvider=fields.ForeignKeyField(VoipProvider)

De cette façon, les détails d'un fournisseur d'un client peuvent être accessibles en code en utilisant:

myCustomer.voipProvider.name #returns the name of the customers VOIP Provider.

Bien que les clés primaires / étrangères ne soient pas visibles, elles sont là et peuvent être utilisées pour accéder aux éléments, mais sont résumées.


Vous avez absolument raison, mais je suppose que je voulais dire que "dans certains domaines, il existe peut-être un tuple naturel de valeurs qui sont toujours uniques" S'ils changent, mais ils sont toujours uniques, ils identifient toujours un seul enregistrement.
tacos_tacos_tacos

0

Je pense que nous examinons souvent encore incorrectement ce problème du point de vue de la base de données: oh, il n'y a pas de clé naturelle, nous devons donc créer une clé de substitution. Oh non, nous ne pouvons pas exposer la clé de substitution dans les objets du domaine, c'est une fuite, etc.

Cependant, parfois une meilleure attitude est la suivante: si un objet métier (domaine) n'a pas de clé naturelle, alors peut-être devrait-il lui en donner une. Il s'agit d'un problème de domaine commercial double: premièrement, les choses ont besoin d'une identité, même en l'absence de bases de données. Deuxièmement, bien que nous essayions de prétendre que la persistance est une idée abstraite invisible au domaine, la réalité est que la persistance est toujours un concept commercial. Évidemment, il y a des problèmes où la clé naturelle choisie n'est pas prise en charge par la base de données en tant que clé primaire (par exemple, les GUID sur certains systèmes) - dans ce cas, vous devrez ajouter une clé de substitution.

Ainsi, vous vous retrouvez dans un endroit très similaire, par exemple, votre client a un identifiant entier, mais au lieu de vous sentir mal parce que la base de données a fui vers le domaine, vous vous sentez heureux parce que l'entreprise a accepté que tous les clients se voient attribuer un ID, et vous persistez à la base de données, comme vous le devez. Vous êtes toujours libre d'introduire un substitut, par exemple pour prendre en charge le changement de nom de l'ID client.

Cette approche signifie également que si un objet de domaine atteint la couche de persistance et n'a pas d'ID, il s'agit probablement d'une sorte d'objet de valeur, il n'a donc pas besoin d'un ID.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.