Travailler avec une base de données depuis une application

Au début, je décrirai certains problÚmes et fonctionnalités lors de l'utilisation de la base de données, je montrerai des trous dans les abstractions. Ensuite, nous analyserons une abstraction plus simple basée sur l'immunité.


Le lecteur est censĂ© ĂȘtre un peu familier avec les schĂ©mas Active Record , Data Maper , Identity Map et Unit of Work .


Les problĂšmes et les solutions sont envisagĂ©s dans le contexte de projets suffisamment importants qui ne peuvent pas ĂȘtre jetĂ©s et rĂ©Ă©crits rapidement.


Carte d'identité


Le premier problÚme est le problÚme du maintien de l'identité. L'identité est quelque chose qui identifie de maniÚre unique une entité. Dans la base de données, il s'agit de la clé primaire et en mémoire, du lien (pointeur). C'est bien lorsque les liens pointent vers un seul objet.


Pour les bibliothĂšques Ruby ActiveRecord , ce n'est pas le cas:


post_a = Post.find 1 post_b = Post.find 1 post_a.object_id != post_b.object_id # true post_a.title = "foo" post_b.title != "foo" # true 

C'est-à-dire on obtient 2 références à 2 objets différents en mémoire.


Ainsi, nous pouvons perdre des modifications si nous commençons par inadvertance Ă  travailler avec la mĂȘme entitĂ©, mais reprĂ©sentĂ©e par des objets diffĂ©rents.


Hibernate a une session, en fait un cache de premier niveau qui stocke le mappage d'un identifiant d'entitĂ© Ă  un objet en mĂ©moire. Si nous demandons Ă  nouveau la mĂȘme entitĂ©, nous obtiendrons un lien vers un objet existant. C'est-Ă -dire Hibernate implĂ©mente le modĂšle de carte d'identitĂ© .


Transactions longues


Mais que se passe-t-il si nous ne sélectionnons pas par identifiant? Pour éviter que l'état des objets et l'état de la base de données ne soient désynchronisés, Hibernate vider avant de demander une sélection.
c'est-à-dire vide les objets sales dans la base de données afin que la demande lise les données convenues.


Cette approche vous oblige à garder la transaction de base de données ouverte pendant que la transaction commerciale est en cours.
Si la transaction commerciale est longue, le processus responsable de la connexion dans la base de donnĂ©es elle-mĂȘme est Ă©galement inactif. Par exemple, cela peut se produire si une transaction commerciale demande des donnĂ©es sur le rĂ©seau ou effectue des calculs complexes.


N + 1


Le plus gros «trou» dans l'abstraction ORM est peut-ĂȘtre le problĂšme de requĂȘte N + 1.


Exemple sur ruby ​​pour la bibliothùque ActiveRecord:


 posts = Post.all # select * from posts posts.each do |post| like = post.likes.order(id: :desc).first # SELECT * FROM likes WHERE post_id = ? ORDER BY id DESC LIMIT 1 # ... end 

ORM conduit le programmeur à l'idée qu'il travaille simplement avec des objets en mémoire. Mais cela fonctionne avec un service disponible sur le réseau, et sur l'établissement de connexions et de transfert de données
cela prend du temps. MĂȘme si la requĂȘte est exĂ©cutĂ©e 50 ms, 20 requĂȘtes seront exĂ©cutĂ©es pendant une seconde.


Données supplémentaires


Dites pour éviter le problÚme N + 1 décrit ci-dessus, vous écrivez
demande :


 SELECT * FROM posts JOIN LATERAL ( SELECT * FROM likes WHERE post_id = posts.id ORDER BY likes.id DESC LIMIT 1 ) as last_like ON true; 

C'est-Ă -dire en plus des attributs du post, tous les attributs du dernier like sont Ă©galement sĂ©lectionnĂ©s. À quelle entitĂ© ces donnĂ©es correspondent-elles? Dans ce cas, vous pouvez renvoyer un couple de la poste et aimer, car le rĂ©sultat contient tous les attributs nĂ©cessaires.


Mais que se passe-t-il si nous sĂ©lectionnons seulement une partie des champs, ou des champs sĂ©lectionnĂ©s qui ne sont pas dans le modĂšle, par exemple, le nombre de publications aiment? Doivent-ils ĂȘtre mappĂ©s sur des entitĂ©s? Peut-ĂȘtre ne leur laisser que des donnĂ©es?


État et identitĂ©


Considérez le code js:


 const alice = { id: 0, name: 'Alice' }; 

Ici, la rĂ©fĂ©rence Ă  l'objet a reçu le nom d' alice . Parce que c'est une constante, alors il n'y a aucun moyen d'appeler Alice un autre objet. Dans le mĂȘme temps, l'objet lui-mĂȘme est restĂ© mutable.


Par exemple, nous pouvons attribuer un identifiant existant:


 const bob = { id: 1, name: 'Bob' }; alice.id = bob.id; 

Permettez-moi de vous rappeler qu'une entitĂ© a 2 identitĂ©s: un lien et une clĂ© primaire dans la base de donnĂ©es. Et les constantes ne peuvent pas arrĂȘter de crĂ©er Alice Bob, mĂȘme aprĂšs avoir enregistrĂ©.


L'objet, le lien auquel nous avons appelé alice , remplit 2 fonctions: il modélise simultanément l'identité et l'état. Un état est une valeur qui décrit une entité à un moment donné.


Mais que se passe-t-il si nous sĂ©parons ces 2 responsabilitĂ©s et utilisons des structures immuables pour l'État?


 function Ref(initialState, validator) { let state = initialState; this.deref = () => state; this.swap = (updater) => { const newState = updater(state); if (! validator(state, newState) ) throw "Invalid state"; state = newState; return newState; }; } const UserState = Immutable.Record({ id: null, name: '' }); const aliceState = new UserState({id: 0, name: 'Alice'}); const alice = new Ref( aliceState, (oldS, newS) => oldS.id === newS.id ); alice.swap( oldS => oldS.set('name', 'Queen Alice') ); alice.swap( oldS => oldS.set('id', 1) ); // BOOM! 

Ref - un conteneur pour un état immuable, permettant son remplacement contrÎlé. Ref modÚles de Ref identifient tout comme nous nommons des objets. Nous appelons la Volga, mais à chaque instant, elle a un état immuable différent.


Stockage


Considérez l'API suivante:


 storage.tx( t => { const alice = t.get(0); const bobState = new UserState({id: 1, name: 'Bob'}); const bob = t.create(bobState); alice.swap( oldS => oldS.update('friends', old => old.push(bob.deref.id)) ); }); 

t.get et t.create renvoient une instance de Ref .


Nous ouvrons la transaction commerciale, trouvons Alice par son identifiant, créons Bob et indiquons qu'Alice considÚre Bob son ami.


L'objet t contrÎle la création de ref .


t peut stocker en lui-mĂȘme le mappage des identifiants d'entitĂ© Ă  l'Ă©tat ref les contenant. C'est-Ă -dire peut implĂ©menter Identity Map. Dans ce cas, t agit comme un cache; Ă  la demande rĂ©pĂ©tĂ©e d'Alice, il n'y aura pas de demande Ă  la base de donnĂ©es.


On peut se souvenir de l'Ă©tat initial des entitĂ©s afin de suivre Ă  la fin de la transaction les changements qui doivent ĂȘtre Ă©crits dans la base de donnĂ©es. C'est-Ă -dire peut mettre en Ɠuvre l' unitĂ© de travail . Ou, si le support d'observateur est ajoutĂ© Ă  Ref , il devient possible de rĂ©initialiser les modifications apportĂ©es Ă  la base de donnĂ©es Ă  chaque modification de ref . Ce sont des approches optimistes et pessimistes pour fixer les changements.


Avec une approche optimiste, vous devez suivre les versions d'état des entités.
Lors de la modification de la base de données, nous devons nous souvenir de la version et lors de la validation des modifications, vérifier que la version de l'entité dans la base de données ne diffÚre pas de la version initiale. Sinon, vous devez répéter la transaction commerciale. Cette approche permet d'utiliser des opérations d'insertion et de suppression de groupe et des transactions de base de données trÚs courtes, ce qui économise des ressources.


Avec une approche pessimiste, une transaction de base de données est entiÚrement cohérente avec une transaction commerciale. C'est-à-dire nous sommes obligés de retirer la connexion du pool au moment de la conclusion de la transaction commerciale.


L'API vous permet d'extraire des entités une par une, ce qui n'est pas trÚs optimal. Parce que nous avons implémenté le modÚle Identity Map , puis nous pouvons entrer la méthode de preload dans l'API:


 storage.tx( t => { t.preload([0, 1, 2, 3]); const alice = t.get(0); // from cache }); 

RequĂȘtes


Si nous ne voulons pas de transactions longues, nous ne pouvons pas effectuer de sélections par une clé arbitraire, car la mémoire peut contenir des objets sales et la sélection renverra un résultat inattendu.


Nous pouvons utiliser Query et récupérer toutes les données (état) en dehors de la transaction et relire les données pendant la transaction.


 const aliceId = userQuery.findByEmail('alice@mail.com'); storage.tx( t => { const alice = t.getOne(aliceId); }); 

Il y a donc une division des responsabilitĂ©s. Pour les requĂȘtes, nous pouvons utiliser des moteurs de recherche pour mettre Ă  l'Ă©chelle la lecture Ă  l'aide de rĂ©pliques. Et l'API de stockage fonctionne toujours avec le stockage principal (maĂźtre). Naturellement, les rĂ©pliques contiendront des donnĂ©es obsolĂštes, la relecture des donnĂ©es dans la transaction rĂ©sout ce problĂšme.


Commandes


Il existe des situations oĂč une opĂ©ration peut ĂȘtre effectuĂ©e sans lire de donnĂ©es. Par exemple, dĂ©duisez des frais mensuels des comptes de tous les clients. Ou insĂ©rez et mettez Ă  jour les donnĂ©es (upsert) en cas de conflit.


En cas de problĂšmes de performances, le bundle de Storage and Query peut ĂȘtre remplacĂ© par une telle commande.


Les communications


Si les entités se réfÚrent de maniÚre aléatoire, il est difficile d'assurer la cohérence lors de leur modification. Les relations tentent de simplifier, de rationaliser, d'abandonner inutiles.


Les agrégats sont un moyen d'organiser les relations. Chaque agrégat a une entité racine et des entités imbriquées. Toute entité externe ne peut se référer qu'à la racine de l'agrégat. La racine garantit l'intégrité de l'ensemble de l'unité. Une transaction ne peut pas franchir une frontiÚre d'agrégation; en d'autres termes, l'ensemble de l'agrégat est impliqué dans la transaction.


Un agrĂ©gat peut, par exemple, ĂȘtre composĂ© du CarĂȘme (racine) et de ses traductions. Ou l'Ordre et ses Positions.


Notre API fonctionne avec des agrĂ©gats entiers. Dans le mĂȘme temps, l'intĂ©gritĂ© rĂ©fĂ©rentielle entre les agrĂ©gats dĂ©pend de l'application. L'API ne prend pas en charge le chargement paresseux des liens.
Mais nous pouvons choisir la direction des relations. Considérez la relation un à plusieurs Utilisateur - Publication. Nous pouvons stocker l'ID utilisateur dans la publication, mais sera-t-il pratique? Nous obtiendrons beaucoup plus d'informations si nous stockons un tableau d'identificateurs de publication dans l'utilisateur.


Conclusion


J'ai souligné les problÚmes lors de l'utilisation de la base de données, montré la possibilité d'utiliser l'immunité.
Le format de l'article ne permet pas de révéler le sujet en détail.


Si vous ĂȘtes intĂ©ressĂ© par cette approche, alors faites attention Ă  mon application de livre Ă  partir de zĂ©ro , qui dĂ©crit la crĂ©ation d'une application Web Ă  partir de zĂ©ro en mettant l'accent sur l'architecture. Il comprend SOLID, Clean Architecture, les modĂšles de travail avec la base de donnĂ©es. Les exemples de code dans le livre et l' application elle-mĂȘme sont Ă©crits dans le langage Clojure, qui est imprĂ©gnĂ© des idĂ©es d'immunitĂ© et de la commoditĂ© du traitement des donnĂ©es.

Source: https://habr.com/ru/post/fr433100/


All Articles