Membre: utiliser des ID uniques par rapport à l'objet de domaine

10

Après quelques réponses utiles pour savoir si je devrais utiliser un objet de domaine ou un identifiant unique comme paramètre de méthode / fonction ici Identifiant vs objet de domaine comme paramètre de méthode , j'ai une question similaire concernant les membres (la discussion des questions précédentes n'a pas réussi à couvrir cela). Quels sont les avantages et les inconvénients de l'utilisation d'identifiants uniques en tant que membre par rapport à l'objet en tant que membre. Je demande en référence aux langages fortement typés, comme Scala / C # / Java. Dois-je avoir (1)

User( id: Int, CurrentlyReadingBooksId: List[Int])
Book( id: Int, LoanedToId: Int )

ou (2), préféré à (1) Après avoir traversé: Faut-il définir des types pour tout?

User( id: UserId, CurrentlyReadingBooksId: List[ BookId] )
Book( id: BookId, LoanedToId: UserId )

ou (3)

User( id: Int, CurrentlyReadingBooks: List[Book]) 
Book( id: Int, LoanedTo: User)

Bien que je ne puisse pas penser aux avantages d'avoir l'objet (3), l'un des avantages d'avoir les ID (2) et (1) est que lorsque je crée l'objet utilisateur à partir de la base de données, je n'ai pas à créer l'objet livre, qui peut à son tour dépendre de l'objet utilisateur lui-même, créant une chaîne sans fin. Existe-t-il une solution générique à ce problème à la fois pour le SGBDR et pour No-SQL (s'ils sont différents)?

Sur la base de certaines réponses jusqu'à présent, reformulant ma question: (avec l'utilisation d'ID censés être dans des types enveloppés) 1) Toujours utiliser des ID? 2) Toujours utiliser des objets? 3) Utiliser des identifiants lorsqu'il existe un risque de récursivité dans la sérialisation et la désérialisation, mais utiliser des objets autrement? 4) Autre chose?

EDIT: Si vous répondez que les objets doivent être utilisés toujours ou dans certains cas, veuillez vous assurer de répondre à la plus grande préoccupation que d'autres répondeurs ont postée => Comment obtenir des données de la base de données

0fnt
la source
1
Merci pour la bonne question, hâte de suivre cela avec intérêt. Un peu dommage que votre nom d'utilisateur soit "user18151", les gens avec ce type de nom d'utilisateur sont ignorés par certains :)
bjfletcher
@bjfletcher Merci. J'avais moi-même cette perception tenace, mais je n'ai jamais pensé pourquoi!
0fnt

Réponses:

7

Les objets de domaine en tant qu'identités créent des problèmes complexes / subtils:

Sérialisation / désérialisation

Si vous stockez des objets sous forme de clés, la sérialisation du graphe d'objets sera extrêmement compliquée. Vous obtiendrez des stackoverflowerreurs lors d'une sérialisation naïve vers JSON ou XML à cause de la récursivité. Vous devrez ensuite écrire un sérialiseur personnalisé qui convertit les objets réels pour utiliser leurs identifiants au lieu de sérialiser l'instance d'objet et de créer la récursivité.

Passez des objets pour la sécurité des types mais ne stockez que les ID, vous pouvez alors avoir une méthode d'accesseur qui charge paresseusement l'entité associée lors de son appel. La mise en cache de deuxième niveau prendra en charge les appels suivants.

Fuites de référence subtiles:

Si vous utilisez des objets de domaine dans des constructeurs comme vous en avez, vous créerez des références circulaires qui seront très difficiles à permettre à la mémoire d'être récupérée pour les objets qui ne sont pas activement utilisés.

Situation idéale:

Identifiants opaques vs int / long:

Un iddevrait être un identifiant complètement opaque qui ne porte aucune information sur ce qu'il identifie. Mais il devrait permettre de vérifier qu'il s'agit d'un identifiant valide dans son système.

Les types bruts brisent ceci:

int, longEt Stringsont les types d'identifiants bruts les plus couramment utilisés dans le système SGBDR. Il existe une longue histoire de raisons pratiques qui remontent à des décennies et ce sont toutes des compromis qui s'inscrivent dans l'épargne spaceou l'épargne timeou les deux.

Les identifiants séquentiels sont les pires contrevenants:

Lorsque vous utilisez un identifiant séquentiel, vous intégrez par défaut des informations sémantiques temporelles dans l'identifiant. Ce qui n'est pas mal tant qu'il n'est pas utilisé. Lorsque les gens commencent à écrire une logique métier qui trie ou filtre la qualité sémantique de l'identifiant, ils mettent en place un monde de douleur pour les futurs responsables.

String les champs sont problématiques parce que les concepteurs naïfs intégreront des informations dans le contenu, généralement la sémantique temporelle également.

Il est donc impossible de créer un système de données distribué également, car il 12437379123n'est pas unique au monde. Les chances qu'un autre nœud d'un système distribué crée un enregistrement avec le même numéro est à peu près garantie lorsque vous obtenez suffisamment de données dans un système.

Ensuite, les hacks commencent à travailler autour de lui et le tout se transforme en un tas de désordre fumant.

Ignorant les énormes systèmes distribués ( clusters ), cela devient un véritable cauchemar lorsque vous essayez également de partager les données avec d'autres systèmes. Surtout lorsque l'autre système n'est pas sous votre contrôle.

Vous vous retrouvez avec exactement le même problème, comment rendre votre identifiant globalement unique.

L'UUID a été créé et normalisé pour une raison:

UUIDpeut souffrir de tous les problèmes énumérés ci-dessus en fonction de celui que Versionvous utilisez.

Version 1utilise une adresse MAC et une heure pour créer un identifiant unique. C'est mauvais car il contient des informations sémantiques sur l'emplacement et l'heure. Ce n'est pas en soi un problème, c'est lorsque des développeurs naïfs commencent à s'appuyer sur ces informations pour la logique métier. Cela fuit également des informations qui pourraient être exploitées lors de toute tentative d'intrusion.

Version 2utilise un utilisateur UIDou GIDet domian UIDou GUIà la place du temps à partir de Version 1cela est tout aussi mauvais que Version 1pour les fuites de données et risquer que ces informations soient utilisées dans la logique métier.

Version 3est similaire mais remplace l'adresse MAC et l'heure par un MD5hachage d'un tableau de byte[]quelque chose qui a définitivement une signification sémantique. Il n'y a aucune fuite de données à craindre, le byte[]ne peut pas être récupéré à partir du UUID. Cela vous donne un bon moyen de créer de manière déterministe des UUIDinstances de formulaire et une clé externe quelconque.

Version 4 est basé uniquement sur des nombres aléatoires, ce qui est une bonne solution, il ne contient absolument aucune information sémantique, mais il n'est pas recréable de façon déterministe.

Version 5est comme Version 4mais utilise sha1au lieu de md5.

Clés de domaine et clés de données transactionnelles

Ma préférence pour les identifiants d'objet de domaine est d'utiliser Version 5ou Version 3si l' utilisation est restreinte Version 5pour une raison technique.

Version 3 est idéal pour les données de transaction qui peuvent être réparties sur de nombreuses machines.

Sauf si vous êtes contraint par l'espace, utilisez un UUID:

Ils sont garantis uniques, en vidant les données d'une base de données et en les rechargeant dans une autre, vous n'avez jamais eu à vous soucier des identifiants en double qui référencent réellement différentes données de domaine.

Version 3,4,5 sont complètement opaques et c'est ainsi qu'ils devraient être.

Vous pouvez avoir une seule colonne comme clé primaire avec un UUID, puis vous pouvez avoir des index uniques composés pour ce qui aurait été une clé primaire composite naturelle.

Le stockage ne doit pas nécessairement l'être CHAR(36)non plus. Vous pouvez stocker le UUIDdans un champ natif octet / bit / numéro pour une base de données donnée tant qu'il est toujours indexable.

Héritage

Si vous avez des types bruts et que vous ne pouvez pas les changer, vous pouvez toujours les résumer dans votre code.

L'utilisation de l'un Version 3/5d'entre UUIDvous peut passer le Class.getName()+ en String.valueOf(int)tant que a byte[]et avoir une clé de référence opaque qui est récréative et déterministe.


la source
Je suis vraiment désolé si je n'ai pas été clair dans ma question et je me sens encore pire (ou en fait bien) parce que c'est une excellente réponse bien pensée et que vous y avez clairement consacré du temps. Malheureusement, cela ne correspond pas à ma question, peut-être mérite-t-il une question en soi? "Que dois-je garder à l'esprit lors de la création d'un champ id pour mon objet de domaine"?
0fnt
J'ai ajouté une explication explicite.
Je l'ai maintenant. Merci d'avoir passé du temps sur la réponse.
0fnt
1
De plus, les collecteurs d'ordures générationnels AFAIK (qui, je crois, est le système GC dominant de nos jours) ne devraient pas avoir trop de difficulté à GC pour faire des références circulaires.
0fnt
1
si C-> A -> B -> Aet Best mis dans un Collectionalors Aet que tous ses enfants sont toujours accessibles, ces choses ne sont pas complètement évidentes et peuvent conduire à des fuites subtiles . GCest le moindre des problèmes, la sérialisation et la désérialisation du graphe est un cauchemar de complexité.
2

Oui, il y a des avantages dans les deux sens, et il y a aussi un compromis.

List<int>:

  • Économiser de la mémoire
  • Initialisation plus rapide du type User
  • Si vos données proviennent d'une base de données relationnelle (SQL), vous n'avez pas besoin d'accéder à deux tables pour obtenir des utilisateurs, juste la Userstable

List<Book>:

  • L'accès à un livre est plus rapide de l'utilisateur, le livre a été préchargé en mémoire. C'est bien si vous pouvez vous permettre d'avoir un démarrage plus long afin d'accélérer les opérations ultérieures.
  • Si vos données proviennent d'une base de données du magasin de documents comme HBase ou Cassandra, les valeurs des livres lus sont probablement sur l'enregistrement utilisateur, vous auriez donc pu facilement obtenir les livres "pendant que vous y étiez en train d'obtenir l'utilisateur".

Si vous n'avez aucun problème de mémoire ou de processeur avec List<Book>lequel j'irais , le code qui utilise les Userinstances sera plus propre.

Faire des compromis:

Lorsque vous utilisez Linq2SQL, le code généré pour l'entité User aura un fichier EntitySet<Book>qui est chargé paresseusement lorsque vous y accédez. Cela devrait garder votre code propre et l'instance utilisateur petite (en termes d'empreinte mémoire).

ytoledano
la source
En supposant une sorte de mise en cache, l'avantage de préchargement serait nul. Je n'ai pas utilisé Cassandra / HBase, je ne peux donc pas en parler, mais Linq2SQL est un cas très spécifique (bien que je ne vois pas comment le chargement paresseux empêchera le cas de chaînage infini même dans ce cas spécifique et dans le cas général)
0fnt le
Dans l'exemple Linq2SQL, vous n'obtenez vraiment aucun avantage en termes de performances, juste un code plus propre. Lorsque vous obtenez des entités un à plusieurs à partir d'un magasin de documents comme Cassandra / HBase, la grande majorité du temps de traitement est consacrée à la recherche de l'enregistrement, vous pouvez donc aussi bien obtenir toutes les nombreuses entités pendant que vous y êtes (les livres, dans cet exemple).
ytoledano
Êtes-vous sûr? Même si je stocke le livre et les utilisateurs séparément normalisés? Pour moi, il semble que cela ne devrait être qu'un coût supplémentaire de latence du réseau. Dans tous les cas, comment gérer le cas du SGBDR de manière générique? (J'ai édité la question pour le mentionner clairement)
0fnt
1

Règle générale courte et simple:

Les ID sont utilisés dans les DTO .
Les références d'objet sont généralement utilisées dans les objets de couche Logique de domaine / Logique d'entreprise et Interface utilisateur.

C'est l'architecture commune dans les projets plus grands et suffisamment entreprenants. Vous aurez des mappeurs qui vont et viennent ces deux types d'objets.

herzmeister
la source
Merci d'être passé et d'avoir répondu. Malheureusement, bien que je comprenne la distinction grâce au lien wiki, je ne l'ai jamais vu dans la pratique (étant donné que je n'ai jamais travaillé avec de grands projets à long terme). Auriez-vous un exemple où le même objet était représenté de deux façons pour deux fins différentes?
0fnt le
voici une vraie question concernant la cartographie: stackoverflow.com/questions/9770041/dto-to-entity-mapping-tool - et il y a des articles critiques comme celui-ci: rogeralsing.com/2013/12/01/…
herzmeister
Vraiment utile, merci. Malheureusement, je ne comprends toujours pas comment le chargement des données avec des références circulaires fonctionnerait? Par exemple, si un utilisateur fait référence à un livre et que le livre fait référence au même utilisateur, comment créeriez-vous cet objet?
0fnt le
Examinez le modèle de référentiel . Vous aurez un BookRepositoryet un UserRepository. Vous appellerez toujours myRepository.GetById(...)ou similaire, et le référentiel créera l'objet et chargera ses valeurs à partir d'un magasin de données, ou le récupérera à partir d'un cache. De plus, les objets enfants sont principalement chargés paresseusement, ce qui évite également d'avoir à traiter avec des références circulaires directes au moment de la construction.
herzmeister