Normalisation. Soit nous en souffrons, soit nous écrivons notre propre solution avec de nombreuses vérifications de l'existence d'une entité dans un référentiel commun. Essayons de le comprendre et de résoudre ce problème!
description du problème
Imaginons la séquence suivante:
L'application client demande une liste d'utilisateurs avec une demande à / users et obtient des utilisateurs avec un identifiant de 1 à 10
L'utilisateur avec l'ID 3 change de nom
L'application cliente demande à l'utilisateur avec l'ID 3 en utilisant une demande à / user / 3
Question: Quel nom d'utilisateur avec l'ID 3 sera dans l'application?
Réponse: dépend du composant qui a demandé les données. Le composant qui utilise les données de la demande à / users affichera l'ancien nom. Le composant qui utilise les données de la requête pour / user / 3 affichera le nouveau nom.
Conclusion : Dans ce cas, il existe plusieurs entités de même signification avec différents ensembles de données dans le système.
Question: Pourquoi est-ce mauvais?
Réponse: Dans le meilleur des cas, l'utilisateur verra différents noms d'une personne dans différentes sections du site, dans le pire des cas, il transférera de l'argent aux anciennes coordonnées bancaires.
Options de solution
Actuellement, il existe les solutions suivantes à ce problème:
graphql (apollo relay)
. . , ? , ?
mobx:
class Store {
users = new Map();
async getUsers() {
const users = await fetch(`/users`);
users.forEach((user) => this.users.set(user.id, user));
}
async getUser(id) {
const user = await fetch(`/user/${id}`);
this.users.set(user.id, user);
}
}
mobx , redux .
graphql (apollo relay)
Apollo relay , . graphql apollo, , , .
graphql ? apollo! apollo :
...normalizes query response objects before it saves them to its internal data store.
normalize?
Normalization involves the following steps:
1. The cache generates a unique ID for every identifiable object included in the response.
2. The cache stores the objects by ID in a flat lookup table.
apollo , . Apollo . :
const store = new Map();
const user = {
id: '0',
type: 'user',
name: 'alex',
age: 24,
};
const id = `${user.type}:${user.id}`;
store.set(id, user);
id - . , id, .
Apollo , __typename, graphql?
, . :
id
:
const store = new Map();
const user = {
id: '0',
};
const comment = {
id: '1',
};
store.set(user.id, user);
store.set(comment.id, comment);
// ...
store.get('0'); // user
store.get('1'); // comment
, id . , id / ( - ). , id , .
:
const store = new Map();
const user = {
id: '0',
type: 'user', // <-- new field
};
const comment = {
id: '1',
type: 'comment', // <-- new field
};
function getStoreId(entity) {
return `${entity.type}:${entity.id}`;
}
store.set(getStoreId(user), user);
store.set(getStoreId(comment), comment);
// ...
store.get('user:0'); // user
store.get('comment:1'); // comment
- , . . .
?
. - . .
, :
app.get('/users', (req, res) => {
const users = db.get('users');
const typedUsers = users.map((user) => ({
...user,
type: 'user',
}));
res.json(typedUsers);
});
, :
function getUsers() {
const users = fetch('/users');
const typedUsers = users.map((user) => ({
...user,
type: 'user',
}));
return typedUsers;
}
. Api, , . , .
.
iresine
iresine .
iresine :
iresine react-query:
@iresine/core
const iresine = new Iresine();
const oldRequest = {
users: [oldUser],
comments: {
0: oldComment,
},
};
// new request data have new structure, but it is OK to iresine
const newRequest = {
users: {
0: newUser,
},
comments: [newComment],
};
iresine.parse(oldRequest);
iresine.parse(newRequest);
iresine.get('user:0' /*identifier for old and new user*/) === newRequest.users['0']; // true
iresine.get('comment:0' /*identifier for old and new comment*/) === newRequest.comments['0']; // true
, , @iresine/core :
entityType + ':' + entityId;
@iresine/core type
, id id
. , . apollo:
const iresine = new Iresine({
getId: (entity) => {
if (!entity) {
return null;
}
if (!entity.id) {
return null;
}
if (!entity.__typename) {
return null;
}
return `${entity.__typename}:${entity.id}`;
},
});
id:
const iresine = new Iresine({
getId: (entity) => {
if (!entity) {
return null;
}
if (!entity.id) {
return null;
}
return entity.id;
},
});
@iresine/core , ? :
const user = {
id: '0',
type: 'user',
jobs: [
{
name: 'milkman',
salary: '1$',
},
{
name: 'woodcutter',
salary: '2$',
},
],
};
user , jobs? type id! @iresine/core : , .
@iresine/core , . ! .
@iresine/react-query
react-query , . , iresine.
@iresine/react-query react-query. @iresine/core react-query. react-query , iresine.
import Iresine from '@iresine/core';
import IresineReactQuery from '@iresone/react-query';
import {QueryClient} from 'react-query';
const iresineStore = new IresineStore();
const queryClient = new QueryClient();
new IresineReactQueryWrapper(iresineStore, queryClient);
// now any updates in react-query store will be consumbed by @iresine/core
( ):
. . . , , iresine